ModuleBuilder icon indicating copy to clipboard operation
ModuleBuilder copied to clipboard

Debugging Modules

Open markekraus opened this issue 5 years ago • 6 comments

Internally we have begun using a module builder that is heavily inspired by this project and tweaked a bit for our environment. One piece of feedback I received was that arranging the code so that classes and functions live in separate files makes debugging harder.

Assuming you were working with a monolithic psm1, It would be possible to debug and refactor within the same file with VS Code. With code living in separate files you have to build the module, set your breakpoints in the compiled psm1, follow the code to its bug, then fix the bug in a similar location in the actual source file.

While tooling for converting source line to compiled line and vice versa is somewhat helpful here, it does make the debugging experience more cumbersome.

I have tried to consider a way in which you can debug and code in the source files, but I'm at a loss. One thing I considered was allowing a Debug Build which would create a psm1 that dot sources the function files. This is doable but would only work for functions because classes have to live in the psm1 to support using each other (class a uses class b and class b uses class a). As I vaguely recall there are other caveats to dot sourcing classes.

Or maybe there is some way to allow for edits in the compiled psm1 to be pushed back to the source file or something.

Anyway, I never ran into this issue because my module coding workflows don't rely heavily on the debugging engine. However, a few of my co-workers do rely on it.

I'm looking for feedback on how we either document debugging modules built from this project or tools to make it much easier.

markekraus avatar Nov 16 '18 13:11 markekraus

A dirty hack I use sometimes is to import those classes in the calling session, i.e. like ScriptsToProcess...

I like when you can load the module in development without compiling for some quick and dirty fixing, but it's not as thorough and rigorous as building the module.

gaelcolas avatar Nov 22 '18 12:11 gaelcolas

The answers to this really depend on how you want to set breakpoints and debug. Are you debugging in a console or by pressing F5 in an IDE? How do the breakpoints get into the session?

The fact is that we don't have a perfect answer, because we're not doing something the PowerShell team invented, we're trying to figure out how to make modules and projects at a bigger scope. If you're dealing with C# developers and they want the C# experience, they cannot have it. Remind them that PowerShell is in the dark ages. There's no compilation, no pre-compiler async tricks, no threading, no generics...

We have to choose:

The old way:

Pretend the whole module is one C# "class" and each function is just a method, and classes are uhm, internal classes? Write everything in a single file and navigate to functions purely using tools in your IDE. In this model, you can edit, test, and debug directly in the source file (like we all used to do, 8 or 9 years ago). You can make every debugging session a whole new runspace.

Note that this is still how the people working on VS Code do things, and the Plaster templates they ship create single-file modules, this is going to reduce the number of places where your tools are just not good enough.

Our way:

Refuse to rely on IDEs. Write separate files per function and then build-module, import-module -force so that if you want to, you can run your pester tests and debug them in a console. There, you set breakpoints with the command-line tools, so it doesn't make much difference whether it's psm1 or source.

But if people do want to work with the live source, you can still do so at the cost of using the global scope instead of module scope: just dot-source everything into the global scope. Breakpoints on source code work, it's no problem, you just have to re-dot-source everything as you change it. You can't dot source classes into a psm1, but you can dot-source them into the global/prompt scope.

We can keep working on tooling.

There's no reason we can't copy breakpoints from the "source" location to the "output" if the breakpoints are defined in such away that we can Get-PSBreakpoint ... or something.

We could even wrap the PSBreakpoint commands so they map back and forth between source and output and always set both...

We can also re-generate source from edited psm1 files (the same #region blocks we use to map line numbers will work to let us "decompile" the files).

Jaykul avatar Nov 23 '18 05:11 Jaykul

@markekraus I add a custom psm1 to my dev-version of the code, this psm1 dot-sources all my files and ends with an Export-ModuleMember.

This way I can run Import-Module with the path to my psd1 and the module loads fine without being compiled. I can simply put a breakpoint in any of my dev-files and either:

  1. start a new interactive debug session with the debugger
  2. load the module
  3. run command
  4. hit breakpoint

OR I create a debug.ps1 where I put the code to load module and run a command, then I can just use the "launch this file" debug option.

I put together a very quick and dirty example here: https://github.com/SimonWahlin/DemoModule

SimonWahlin avatar Jan 23 '19 16:01 SimonWahlin

I take two primary approaches to debugging in VSCode depending on what I'm currently doing.

  1. If I'm looking for a problem in the existing code base I'll use the tests as @Jaykul suggested and debug those. This seems like the right way to go assuming that you have good coverage and you have modularized your tests. I usually do one test file per function if the project has a large number of tests.

  2. If I'm developing a new function, then I'd likely do that in a source file that includes the tests and appropriate mocks so I can just debug right in the file. This works for most cases.

I feel @markekraus pain on the compiled .psm1 issue - it stinks to find an issue in the compiled file, fix it in the source file and then rebuild to test again. Still, hooking all this up in VSCode tasks makes it much simpler, and I'm sure you can do similar things in the ISE or other IDEs to help as well.

Of course, I've also just done ad-hoc debugging using an ignored debug handler file where I can import the built module and step through.

@Jaykul I like the idea of "importing" the breakpoints into the compiled module. I'm not sure I understand how the source and compiled files map lines back and forth, but if that works then maybe a refactor action could be added when you fix a line in the compiled module? That way you could choose whether to add that line back to source? I might be assuming everyone is using VSCode, which I'm sure isn't universal.

mattmcnabb avatar Jul 11 '19 15:07 mattmcnabb

@mattmcnabb sorry to answer after so long ...

Have you looked at Convert-LineNumber? I missed an alias I should have had ("Script") but if you add it to the type using:

Update-TypeData -TypeName System.Management.Automation.LineBreakpoint -MemberType AliasProperty -MemberName ScriptName -Value Script

Then if you have breakpoints defined in the compiled module, you could convert them to the source locations like this:

Get-Breakpoint | Convert-LineNumber

I haven't written it the other way 'round yet, but it would be trivial

Jaykul avatar Oct 06 '19 02:10 Jaykul

No worries - I had forgotten all about this 😆

That's a cool approach. I'll try it out sometime and see if it fixes me.

mattmcnabb avatar Oct 06 '19 03:10 mattmcnabb