zero-plusplus / vscode-autohotkey-debug

https://marketplace.visualstudio.com/items?itemName=zero-plusplus.vscode-autohotkey-debug
53 stars 4 forks source link

Attach to running script issue with window title #330

Open RaptorX opened 2 months ago

RaptorX commented 2 months ago

I had originally reported this on thqby's Language Support Extension but he explained that the one providing this functionality is the debugger extension.

Can you please check this:

Type: Bug

When running the command to attach to a running script I get two errors (they look the same) but if you press continue both times you get a dialog to choose the path to the script you want to connect to.

After that it behaves completely fine.

Current window title

image

Regex against Code window

https://regex101.com/r/b4Qaqw/1

Error 1

image

Error 2

image

Dialog

image

Correct connection

image

Notes

Im not sure what are the contents of the title variable, but i would assume you are getting it from the active window (code.exe in this case?)

But it seems like it needs some work after some recent updates. This was not happening before.


Extension version: 2.4.9 VS Code version: Code 1.92.1 (eaa41d57266683296de7d118f574d0c2652e1fc4, 2024-08-07T20:16:39.455Z) OS version: Windows_NT x64 10.0.22621 Modes:

System Info |Item|Value| |---|---| |CPUs|Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz (4 x 3198)| |GPU Status|2d_canvas: enabled
canvas_oop_rasterization: enabled_on
direct_rendering_display_compositor: disabled_off_ok
gpu_compositing: enabled
multiple_raster_threads: enabled_on
opengl: enabled_on
rasterization: enabled
raw_draw: disabled_off_ok
skia_graphite: disabled_off
video_decode: enabled
video_encode: enabled
vulkan: disabled_off
webgl: enabled
webgl2: enabled
webgpu: enabled
webnn: disabled_off| |Load (avg)|undefined| |Memory (System)|31.88GB (20.16GB free)| |Process Argv|--crash-reporter-id 3e042e59-cb22-4371-968a-6f3e35abe666| |Screen Reader|no| |VM|0%|
A/B Experiments ``` vsliv368cf:30146710 vspor879:30202332 vspor708:30202333 vspor363:30204092 vscod805:30301674 binariesv615:30325510 vsaa593cf:30376535 py29gd2263:31024239 c4g48928:30535728 azure-dev_surveyone:30548225 962ge761:30959799 pythongtdpath:30769146 welcomedialogc:30910334 pythonnoceb:30805159 asynctok:30898717 pythonregdiag2:30936856 pythonmypyd1:30879173 h48ei257:31000450 pythontbext0:30879054 accentitlementsc:30995553 dsvsc016:30899300 dsvsc017:30899301 dsvsc018:30899302 cppperfnew:31000557 dsvsc020:30976470 pythonait:31006305 dsvsc021:30996838 jg8ic977:31013176 pythoncenvpt:31062603 a69g1124:31058053 dvdeprecation:31068756 dwnewjupytercf:31046870 impr_priority:31102340 nativerepl1:31104043 refactort:31108082 pythonrstrctxt:31112756 wkspc-onlycs-c:31111717 wkspc-ranged-t:31118572 ```
zero-plusplus commented 2 months ago

Thanks for the report. The code in question can be found the following.

https://github.com/zero-plusplus/vscode-autohotkey-debug/blob/d00790d5eb8734069f39ec2edd7fab213d7d81fb/src/util/getRunningAhkScriptList.ts#L26-L38

To explain, title represents the title of the running AutoHotkey main windows, and is a variable intended to extract the path to the scripts contained within it.

This code was written in an era when knowledge of regular expressions was limited, so this may have caused some problems. (For example, from what I can see, it doesn't seem to work correctly if there are spaces in path.)

Perhaps the situation described above is in a state where two AutoHotkey instances failed to be detected, and the remaining five instances that could be detected are shown in the dialog.


Perhaps this problem can be worked around by ensuring that script paths do not contain spaces.

Please understand that I am currently prioritizing work on v2.0.0, so non-fatal issues are being put on the back burner. Of course v2.0.0 will take this issue into account.

Since it is difficult to write a strict regular expression that can reliably detect paths, I think it would be good to have an option to allow the user to change the regular expression when a problem occurs.

Please comment if you have any ideas.

RaptorX commented 2 months ago

I think I now have a better idea of what is going on.

Here was the list of my running scripts that matched your first condition (WinGetTitle("ahk_id " id))

D:\path\Desktop\test.ahk - AutoHotkey v2.0.18
D:\path\Documents\AutoHotkey\v2\EdeCheck\EdeCheck.ahk - AutoHotkey v2.0.18
--> S:\Prompt Assistant\bin\Prompt Assistant.exe
S:\WindowSnipping\WindowSnipping.ahk - AutoHotkey v1.1.37.02
D:\path\Documents\AutoHotkey\v1\AHK-Toolkit\AHK-ToolKit.ahk - AutoHotkey v1.1.37.02
--> S:\Newsletter\PrettyLinks\bin\Pretty Links Manager.exe
S:\Clip_Share\V2\Dev\ClipShareV2.ahk - AutoHotkey v2.0.18
C:\Users\User\AppData\Local\Temp\hslauncher.code - AutoHotkey v1.1.37.02

You will notice that two of those scripts are compiled scripts. Those were the two errors that I was getting and that you hinted at in your previous comment.

They were not matching because of the assumption that all of them would have \s- somewhere, but as you can see, executables don't have that.

That means that, this might also fail in certain conditions too. Im not sure what those conditions might be, but it is the same assumption:

if (title ~= "^\\*(?=\\s-)") {
    continue
}

I would go with this:

You can check how this regex matching looks like by clicking here.

zero-plusplus commented 2 months ago

Thanks. I had not thought about compiled scripts. This cannot be attached (and even if it could, it would be debugging with compiled scripts), so it needs to be excluded.

The way to extract paths with regular expressions has its limitations. For example, your regular expression will have problems with the path C:\test\script - AutoHotkey.ahk.

Attempting to handle all cases would require the exact representation of Windows paths in regular expressions. This is impractical due to the high cost of implementation and maintenance.

Then, noting that the end of the path is always blank, I came up with the following extraction method.

  1. extract from the first character to the nth space (The default value of n is 1)
  2. determine if the extracted path is valid, and if not, increment n and return to step 1
  3. If there is an exact match between a valid path and the target string, it is assumed to be a compiled script and ignored. If not, the extraction is considered complete

I think this would allow for simple and reliable extraction without the use of regular expressions.

In addition, the extraction process was implemented within AutoHotkey, which caused errors, so these will be changed to be handled on the extension side.

Are there any problems with this implementation approach?

RaptorX commented 2 months ago

The way to extract paths with regular expressions has its limitations. For example, your regular expression will have problems with the path C:\test\script - AutoHotkey.ahk.

If you are getting the title out of the hidden window, I think it could be safely assumed that you will never end up with that situation because the hidden windows will always have the autohotkey version at the end as such: C:\test\script - AutoHotkey.ahk - AutoHotkey v2.0.18 and the regular expression will match correctly.

But with this fix it would work regardless: ^(?<path>.+?\.\w+\s?)(?:\s-\sAutoHotkey|$)

You can check here.

I would use this:

processPathList := ''
for id in WinGetList('ahk_class AutoHotkey') {
    title := WinGetTitle(id)
    if (title ~= '^\\*(?=\\s-)|.exe')
    || !RegExMatch(title, '^(?<path>.+?\.\w+\s?)(?:\s-\sAutoHotkey|$)', &matched)
        continue

    processPathList .= matched.path '\`n'
}
FileOpen('*', 'w').write(RTrim(processPathList, '\`n'))

This code:

This is the end result after running it:

D:\path\Desktop\test.ahk\
D:\path\Documents\AutoHotkey\v2\EdeCheck\EdeCheck.ahk\
S:\WindowSnipping\WindowSnipping.ahk\
D:\path\Documents\AutoHotkey\v1\AHK-Toolkit\AHK-ToolKit.ahk\
S:\Clip_Share\V2\Dev\ClipShareV2.ahk\
C:\Users\User\AppData\Local\Temp\hslauncher.code\

As you mentioned there are still assumptions made (uncompiled scripts have the autohotkey version in their title) but this is an assumption that the extension could work with.

I would say: the extension doesnt need to be perfect but if it works 95-99% of the time, it will be good for what we are trying to do. The worst that can happen is that the list wont display all running scripts, but no warning/error would be thrown.

RaptorX commented 2 months ago

An alternate approach might be to use the WMI object to get the command line information of the running scripts.

That would make the approach more stable because we know that you will always have the Autohotkey executable in the first path and the path of the script (no matter what it looks like) in the second path.

wmi := ComObject("WbemScripting.SWbemLocator").ConnectServer()
for process in wmi.ExecQuery("Select * from Win32_Process") {

    if !(process.ExecutablePath ~= "AutoHotkey")
    || !RegExMatch(process.CommandLine, '(\w:\\.*?)"?$', &Matched, 5)
        continue

    OutputDebug 'command line: ' process.CommandLine '`nmatched: ' Matched[1] '`n`n'
}

This works for me independent of the script name. Notice how in the regex match I am starting at the 5th position to force never matching the first path.

Here's a sample output of what i got:

command line: "C:\Program Files\AutoHotkey\v1.1.37.02\AutoHotkeyU32.exe" /CP65001 "D:\path\Documents\AutoHotkey\v1\AHK-Toolkit\AHK-ToolKit.ahk"
matched: D:\path\Documents\AutoHotkey\v1\AHK-Toolkit\AHK-ToolKit.ahk

command line: "C:\Program Files\AutoHotkey\v1.1.37.02\AutoHotkeyU32.exe" "C:\Users\User\AppData\Local\Temp\hslauncher.code"
matched: C:\Users\User\AppData\Local\Temp\hslauncher.code

command line: "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" "D:\path\Documents\AutoHotkey\v2\EdeCheck\EdeCheck.ahk"
matched: D:\path\Documents\AutoHotkey\v2\EdeCheck\EdeCheck.ahk

command line: "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" "S:\Clip_Share\V2\Dev\ClipShareV2.ahk"
matched: S:\Clip_Share\V2\Dev\ClipShareV2.ahk

command line: "C:\Program Files\AutoHotkey\v1.1.37.02\AutoHotkeyU32.exe" /CP65001 "S:\WindowSnipping\WindowSnipping.ahk"
matched: S:\WindowSnipping\WindowSnipping.ahk

command line: "c:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" /Debug=127.0.0.1:9002 /ErrorStdOut D:\path\Desktop\test.ahk
matched: D:\path\Desktop\test.ahk

We would need to think about situations that might make this approach fail. I havent come up any yet but surely we could find some haha.

zero-plusplus commented 2 months ago

I would say: the extension doesnt need to be perfect but if it works 95-99% of the time, it will be good for what we are trying to do.

You are right. However, I would guess that in my experience, when a problem occurs, that user may spend a great deal of time identifying the problem. This is even more so if that person is using an unusual use case.

Therefore, I want ideas to remove as much of the problem as possible.

From that perspective, the method I suggested above seems to be the better way. (Essentially, your way is more reasonable.)

The specific code would be as follows.

Changed AutoHotkey script to a simple one that only gets the title to avoid unnecessary errors.

A_DetectHiddenWindows := true 
idList := WinGetList("ahk_class AutoHotkey") 

ahkMainWindowTitles := "" 
for i, id in idList { 
  title := WinGetTitle("ahk_id " id) 
  ahkMainWindowTitles .= title "`n" 
}
FileOpen("*", "w").write(RTrim(ahkMainWindowTitles, "`n"))

I think the pseudo code in typescript would look like this. (I have not run it, so there may be many mistakes.)

const getProcessPathList = (): string[] => {
  return getAhkMainWindowTitles().flatMap((title) => {
    // Script main window format
    //   %A_ScriptFullPath% - AutoHotkey v%A_AhkVersion%
    // Format of the main window of the compiled script
    //   %A_ScriptDir%\%{Script Name}%.exe
    let processPath = '';

    // It is difficult to capture Windows file paths accurately with regular expressions.
    // Therefore, taking advantage of the idea that a path in a script is always followed by a space,
    // I use a method to concatenate an array separated by a space while determining if it is a valid path or not.
    const titleParts = title.split('\s');
    for (let i = 0; i < titleParts.length; i++) {
      const part = titleParts[i];
      processPath += part;
      if (fileExists(part)) {
        // The compiled script will show only its path in the title, so the title and path match exactly
        const isCompiled = processPath === title;
        if (isCompiled)  {
          return [];
        }
        return [ processPath ];
      }
    }
    return [];
  })
}

By the next release, I will improve it so that paths that fail to parse will be displayed in the debug console. This way, even if path extraction fails, you can copy and paste the path from the console to the dialog (or put it directly in program of launch.json) to attach to it.


The way you used wmi is interesting.

However, wmi is deprecated in win11, so it does not seem usable for the future.

I am not familiar with it and may be wrong, but PowerShell may be able to do the same thing as wmi. But, I believe that scripts could not be executed without the user's permission, so this also seems difficult.

RaptorX commented 2 months ago

You are right. However, I would guess that in my experience, when a problem occurs, that user may spend a great deal of time identifying the problem. This is even more so if that person is using an unusual use case.

Therefore, I want ideas to remove as much of the problem as possible.

Completely agree with that.

By the next release, I will improve it so that paths that fail to parse will be displayed in the debug console. This way, even if path extraction fails, you can copy and paste the path from the console to the dialog (or put it directly in program of launch.json) to attach to it.

Thats perfect! Your approach seems to be the best (and simpler) solution to the problem. I will be waiting for the next release :))

The way you used wmi is interesting.

However, wmi is deprecated in win11, so it does not seem usable for the future.

This seems to be a misunderstanding. The only thing that is being deprecated is the WMIC. Here is a nice read on that topic.

Heres a quote from that link: Note: This deprecation only applies to the command-line management tool. WMI itself is not affected.

Windows Management Instrumentation (WMI) <-- main infrastructure | WMI command line (WMIC) <-- this is being deprecated. | CIM | COM Object | ... Other Interfaces

There is another infrastructure called Windows Management Infrastructure MI which will eventually "replace" WMI but according to their information:

The next-generation of WMI, known as the Windows Management Infrastructure (MI), is currently available. MI is fully compatible with previous versions of WMI, and provides a host of features and benefits that make designing and developing providers and clients easier than ever. For example, many newer providers are written using the MI framework, but can be accessed using WMI scripts and applications. For more information about the differences between the two technologies, see Why Use MI?


I am not familiar with it and may be wrong, but PowerShell may be able to do the same thing as wmi. But, I believe that scripts could not be executed without the user's permission, so this also seems difficult.

PowerShell CMDLets is exactly what is getting deprecated.

The WMI cmdlets are deprecated and are not available in PowerShell 6+, but are covered here as you may encounter them in older scripts running on Windows PowerShell. For new development, use the CIM cmdlets instead.

I am using a COM Object which as far as I understand is not being deprecated.

zero-plusplus commented 2 months ago

Thanks for the info on wmi.

I had not known the powershell term “cmdlet” and had mistakenly interpreted the deprecation of the wmi cmdlet to mean that wmi itself was deprecated.

I will keep in mind the use of the com object as an option for future solutions to the problem. I really enjoy this kind of increase in knowledge. Thanks for explaining!


The next release may take much longer.

The development of 1.10.0, which was a relatively large update, took three months (of which one month was spent fixing bugs and testing), and v2.0.0, which is currently under development, will take even longer due to a complete rewrite.

I am rewriting it so that it can be tested automatically, so I expect that bug fixing and testing will be greatly shortened after release.

Also, I would like to be able to solve hard-to-identify problems, such as bugs that prevent debugging until the vscode is restarted, which has been present since the beginning.

Please understand that the release will be delayed for this reason.

Real-time development status can be checked here. The speed of development is considerablyerably slower due to research into automated testing and ease of maintenance...

RaptorX commented 2 months ago

Sounds great!

I do think that simple fixes like the above could be pushed as minor updates. If you are working with a git repository is very simple to just create a branch, fix the issue, merge it to main and push it, while not affecting the other development branch. :)

But I will gladly wait for the main update because this is by far the best debugger plugin for AHK at the moment.

I do have one suggestion which I will create a new issue about. Should that be in the Issues tab marked as [WISH] or is maybe better for you if I post it in the Discussion tab?

zero-plusplus commented 2 months ago

I thought about a minor update, but debugging the v1.11.0 source code in the current environment makes it difficult to test because it no longer steps correctly (Probably due to outdated source map).

This time, I need to test it because there will be more modifications than just regular expressions. However, I gave up on the minor update because I want to use that time to develop v2.0.0.


But I will gladly wait for the main update because this is by far the best debugger plugin for AHK at the moment.

Thank you. I will add more features as my increased programming knowledge allows me to do more and I am no longer satisfied with my current extension.

I do have one suggestion which I will create a new issue about. Should that be in the Issues tab marked as [WISH] or is maybe better for you if I post it in the Discussion tab?

Either one is fine. They can be inter-converted as needed. If I may say so, Issues may be easier to read because it is a single tree.