Skip to content

Commit 9bb7413

Browse files
committed
Fix: Opening a solution with the same name but a different path did not launch a new instance (#5)
An elevated (Run as Administrator) Visual Studio instance always launches a new instance because its command line cannot be retrieved.
1 parent 9cadd04 commit 9bb7413

File tree

10 files changed

+409
-37
lines changed

10 files changed

+409
-37
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// using System;
2+
// using System.Runtime.InteropServices;
3+
// using System.Runtime.Versioning;
4+
// using System.Security;
5+
6+
// public static class Marshal2
7+
// {
8+
// internal const string OLEAUT32 = "oleaut32.dll";
9+
// internal const string OLE32 = "ole32.dll";
10+
11+
// [System.Security.SecurityCritical] // auto-generated_required
12+
// public static Object GetActiveObject(string progID)
13+
// {
14+
// Object obj = null;
15+
// Guid clsid;
16+
17+
// // Call CLSIDFromProgIDEx first then fall back on CLSIDFromProgID if
18+
// // CLSIDFromProgIDEx doesn't exist.
19+
// try
20+
// {
21+
// CLSIDFromProgIDEx(progID, out clsid);
22+
// }
23+
// // catch
24+
// catch (Exception ex)
25+
// {
26+
// CLSIDFromProgID(progID, out clsid);
27+
// }
28+
29+
// GetActiveObject(ref clsid, IntPtr.Zero, out obj);
30+
// return obj;
31+
// }
32+
33+
// //[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)]
34+
// [DllImport(OLE32, PreserveSig = false)]
35+
// [ResourceExposure(ResourceScope.None)]
36+
// [SuppressUnmanagedCodeSecurity]
37+
// [System.Security.SecurityCritical] // auto-generated
38+
// private static extern void CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] string progId, out Guid clsid);
39+
40+
// //[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)]
41+
// [DllImport(OLE32, PreserveSig = false)]
42+
// [ResourceExposure(ResourceScope.None)]
43+
// [SuppressUnmanagedCodeSecurity]
44+
// [System.Security.SecurityCritical] // auto-generated
45+
// private static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] string progId, out Guid clsid);
46+
47+
// //[DllImport(Microsoft.Win32.Win32Native.OLEAUT32, PreserveSig = false)]
48+
// [DllImport(OLEAUT32, PreserveSig = false)]
49+
// [ResourceExposure(ResourceScope.None)]
50+
// [SuppressUnmanagedCodeSecurity]
51+
// [System.Security.SecurityCritical] // auto-generated
52+
// private static extern void GetActiveObject(ref Guid rclsid, IntPtr reserved, [MarshalAs(UnmanagedType.Interface)] out Object ppunk);
53+
54+
// }
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// using System;
2+
// using System.Collections.Generic;
3+
// using System.Runtime.InteropServices;
4+
// using EnvDTE;
5+
// using Microsoft.VisualStudio.OLE.Interop;
6+
7+
// namespace WorkspaceLauncherForVSCode.Classes
8+
// {
9+
// internal static class VisualStudioDteHelper
10+
// {
11+
// internal static List<string> GetAllOpenSolutionPaths()
12+
// {
13+
// List<string> solutionPaths = new();
14+
15+
// IRunningObjectTable rot;
16+
// IEnumMoniker enumMoniker;
17+
// GetRunningObjectTable(0, out rot);
18+
// rot.EnumRunning(out enumMoniker);
19+
20+
// IMoniker[] monikers = new IMoniker[1];
21+
// while (enumMoniker.Next(1, monikers, out var fetched) == 0)
22+
// {
23+
// rot.GetObject(monikers[0], out object? comObject);
24+
// if (comObject is not DTE dte)
25+
// continue;
26+
27+
// try
28+
// {
29+
// var solution = dte.Solution?.FullName;
30+
// if (!string.IsNullOrEmpty(solution))
31+
// {
32+
// solutionPaths.Add(solution);
33+
// }
34+
// }
35+
// catch
36+
// {
37+
// // Some DTEs may be busy/unavailable
38+
// }
39+
// }
40+
41+
// return solutionPaths;
42+
// }
43+
44+
// internal static List<(string SolutionPath, IntPtr Hwnd)> GetAllDteSolutionWindows()
45+
// {
46+
// List<(string, IntPtr)> result = new();
47+
48+
// GetRunningObjectTable(0, out var rot);
49+
// rot.EnumRunning(out var enumMoniker);
50+
51+
// IMoniker[] monikers = new IMoniker[1];
52+
// while (enumMoniker.Next(1, monikers, out var fetched) == 0)
53+
// {
54+
// rot.GetObject(monikers[0], out object? comObject);
55+
// if (comObject is not DTE dte)
56+
// continue;
57+
58+
// try
59+
// {
60+
// var solutionPath = dte.Solution?.FullName;
61+
// var hwnd = new IntPtr(dte.MainWindow.HWnd);
62+
63+
// if (!string.IsNullOrEmpty(solutionPath))
64+
// {
65+
// result.Add((solutionPath, hwnd));
66+
// }
67+
// }
68+
// catch
69+
// {
70+
// // ignore inaccessible instances
71+
// }
72+
// }
73+
74+
// return result;
75+
// }
76+
77+
// [DllImport("ole32.dll")]
78+
// private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
79+
80+
// [DllImport("ole32.dll")]
81+
// private static extern int CreateBindCtx(int reserved, out IBindCtx ppbc);
82+
// }
83+
// }

WorkspaceLauncherForVSCode/Commands/OpenSolutionCommand.cs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Modifications copyright (c) 2025 tanchekwei
22
// Licensed under the MIT License. See the LICENSE file in the project root for details.
3-
using System.Linq;
3+
using System.Collections.Generic;
44
using System.Threading.Tasks;
55
using Microsoft.CmdPal.Ext.System.Helpers;
66
using Microsoft.CommandPalette.Extensions.Toolkit;
77
using WorkspaceLauncherForVSCode.Components;
8-
using WorkspaceLauncherForVSCode.Enums;
8+
using WorkspaceLauncherForVSCode.Helpers;
99
using WorkspaceLauncherForVSCode.Interfaces;
1010

1111
namespace WorkspaceLauncherForVSCode.Commands;
@@ -50,28 +50,46 @@ public override CommandResult Invoke()
5050
return CommandResult.Dismiss();
5151
}
5252

53-
OpenWindows.Instance.UpdateOpenWindowsList();
54-
var openVSWindows = OpenWindows.Instance.Windows.Where(w => w.Process.Name == "devenv");
55-
56-
var solutionName = System.IO.Path.GetFileNameWithoutExtension(Workspace.Path);
57-
58-
foreach (var window in openVSWindows)
53+
OpenWindows.Instance.UpdateVisualStudioWindowsList();
54+
var visualStudioWindows = OpenWindows.Instance.Windows;
55+
var matchedElevatedVisualStudioWindows = new List<Window>();
56+
foreach (var window in visualStudioWindows)
5957
{
60-
if (window.Title.Contains(solutionName))
58+
if (window.IsProcessElevated)
6159
{
62-
window.SwitchToWindow();
63-
return PageCommandResultHandler.HandleCommandResult(page);
60+
//var solutionName = System.IO.Path.GetFileNameWithoutExtension(Workspace.Path);
61+
//if (window.Title.Contains(solutionName))
62+
//{
63+
// matchedElevatedVisualStudioWindows.Add(window);
64+
//}
65+
}
66+
else
67+
{
68+
string? commandLine = NativeProcessCommandLine.GetCommandLine(window.Process.Process);
69+
string? solutionPath = NativeProcessCommandLine.ExtractSolutionPath(commandLine);
70+
if (solutionPath == Workspace.WindowsPath)
71+
{
72+
window.SwitchToWindow();
73+
return PageCommandResultHandler.HandleCommandResult(page);
74+
}
6475
}
6576
}
6677

78+
//if (matchedElevatedVisualStudioWindows.Count > 0)
79+
//{
80+
// TODO: Waiting on https://github.com/microsoft/PowerToys/pull/38025 for GotoPage support.
81+
// Since we can't retrieve the command line from elevated processes, we can't determine their solution path.
82+
// If any elevated Visual Studio windows have a title that contains the solution name,
83+
// consider navigating to a selection page to let the user choose which window to switch to or reopen the solution.
84+
//}
85+
6786
if (Workspace.VSInstance != null)
6887
{
6988
OpenInShellHelper.OpenInShell(Workspace.VSInstance.InstancePath, Workspace.Path, runAs: _elevated ? OpenInShellHelper.ShellRunAsType.Administrator : OpenInShellHelper.ShellRunAsType.None);
7089
}
7190

7291
if (page != null)
7392
{
74-
// Update frequency
7593
Task.Run(() => page.UpdateFrequencyAsync(Workspace.Path));
7694
}
7795

WorkspaceLauncherForVSCode/Commands/OpenVisualStudioCodeCommand.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public override CommandResult Invoke()
8080

8181
OpenInShellHelper.OpenInShell(Workspace.VSCodeInstance.ExecutablePath, arguments, runAs: _elevated ? OpenInShellHelper.ShellRunAsType.Administrator : OpenInShellHelper.ShellRunAsType.None);
8282

83-
// Update frequency
8483
Task.Run(() => page.UpdateFrequencyAsync(Workspace.Path));
8584

8685
return PageCommandResultHandler.HandleCommandResult(page);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Modifications copyright (c) 2025 tanchekwei
2+
// Licensed under the MIT License. See the LICENSE file in the project root for details.
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using Microsoft.CmdPal.Ext.System.Helpers;
7+
using Microsoft.CommandPalette.Extensions;
8+
using Microsoft.CommandPalette.Extensions.Toolkit;
9+
using WorkspaceLauncherForVSCode.Classes;
10+
using WorkspaceLauncherForVSCode.Components;
11+
using WorkspaceLauncherForVSCode.Enums;
12+
using WorkspaceLauncherForVSCode.Interfaces;
13+
using WorkspaceLauncherForVSCode.Pages;
14+
15+
namespace WorkspaceLauncherForVSCode.Commands;
16+
17+
internal partial class SwitchWindowCommand : InvokableCommand
18+
{
19+
private readonly Window _window;
20+
21+
public SwitchWindowCommand(Window window)
22+
{
23+
_window = window;
24+
}
25+
26+
public override CommandResult Invoke()
27+
{
28+
_window.SwitchToWindow();
29+
return CommandResult.Dismiss();
30+
}
31+
}

WorkspaceLauncherForVSCode/Components/OpenWindows.cs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Runtime.InteropServices;
68
using WorkspaceLauncherForVSCode.Helpers;
79

810
namespace WorkspaceLauncherForVSCode.Components
@@ -26,17 +28,17 @@ internal static OpenWindows Instance
2628

2729
private OpenWindows() { }
2830

29-
internal void UpdateOpenWindowsList()
31+
internal void UpdateVisualStudioWindowsList()
3032
{
3133
lock (_enumWindowsLock)
3234
{
3335
windows.Clear();
34-
EnumWindowsProc callbackptr = new EnumWindowsProc(WindowEnumerationCallBack);
36+
EnumWindowsProc callbackptr = new EnumWindowsProc(VisualStudioWindowEnumerationCallBack);
3537
_ = NativeMethods.EnumWindows(callbackptr, IntPtr.Zero);
3638
}
3739
}
3840

39-
private bool WindowEnumerationCallBack(IntPtr hwnd, IntPtr lParam)
41+
private bool VisualStudioWindowEnumerationCallBack(IntPtr hwnd, IntPtr lParam)
4042
{
4143
Window newWindow = new Window(hwnd);
4244

@@ -45,10 +47,54 @@ private bool WindowEnumerationCallBack(IntPtr hwnd, IntPtr lParam)
4547
newWindow.ClassName != "Windows.UI.Core.CoreWindow" &&
4648
!newWindow.IsCloaked)
4749
{
48-
windows.Add(newWindow);
50+
if (string.Equals(newWindow.Process?.Process.ProcessName, "devenv", StringComparison.OrdinalIgnoreCase))
51+
{
52+
if (newWindow.Process?.Process is Process proc && IsProcessElevated(proc.Id))
53+
{
54+
newWindow.IsProcessElevated = true;
55+
}
56+
windows.Add(newWindow);
57+
}
4958
}
5059

5160
return true;
5261
}
62+
63+
private bool IsProcessElevated(int processId)
64+
{
65+
IntPtr hProcess = NativeMethods.OpenProcess(0x1000 /* PROCESS_QUERY_INFORMATION */, false, processId);
66+
if (hProcess == IntPtr.Zero)
67+
return false;
68+
69+
IntPtr hToken = IntPtr.Zero;
70+
try
71+
{
72+
if (!NativeMethods.OpenProcessToken(hProcess, 0x0008 /* TOKEN_QUERY */, out hToken))
73+
return false;
74+
75+
uint tokenInfoLength = (uint)Marshal.SizeOf(typeof(int));
76+
IntPtr elevationPtr = Marshal.AllocHGlobal((int)tokenInfoLength);
77+
try
78+
{
79+
if (NativeMethods.GetTokenInformation(hToken, NativeMethods.TokenElevation, elevationPtr, tokenInfoLength, out _))
80+
{
81+
int elevation = Marshal.ReadInt32(elevationPtr);
82+
return elevation != 0;
83+
}
84+
}
85+
finally
86+
{
87+
Marshal.FreeHGlobal(elevationPtr);
88+
}
89+
}
90+
finally
91+
{
92+
if (hToken != IntPtr.Zero)
93+
NativeMethods.CloseHandle(hToken);
94+
NativeMethods.CloseHandle(hProcess);
95+
}
96+
97+
return false;
98+
}
5399
}
54-
}
100+
}

WorkspaceLauncherForVSCode/Components/Window.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ internal string Title
3737

3838
internal IntPtr Hwnd => hwnd;
3939
internal WindowProcess Process => processInfo;
40+
internal bool IsProcessElevated { get; set; }
4041
internal bool Visible => NativeMethods.IsWindowVisible(Hwnd);
4142
internal bool IsCloaked => GetWindowCloakState() != WindowCloakState.None;
4243
internal bool IsWindow => NativeMethods.IsWindow(Hwnd);
@@ -71,7 +72,7 @@ internal void SwitchToWindow()
7172

7273
public override string ToString()
7374
{
74-
return Title + " (" + processInfo.Name?.ToUpper(CultureInfo.CurrentCulture) + ")";
75+
return Title + " (" + processInfo.Process.ProcessName?.ToUpper(CultureInfo.CurrentCulture) + ")";
7576
}
7677

7778
internal WindowSizeState GetWindowSizeState()

WorkspaceLauncherForVSCode/Components/WindowProcess.cs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,13 @@ namespace WorkspaceLauncherForVSCode.Components
1010
internal sealed class WindowProcess
1111
{
1212
private uint processId;
13-
private string? processName;
14-
13+
private readonly Process? process;
1514
internal uint ProcessId => processId;
16-
internal string? Name => processName;
17-
15+
internal Process? Process => process;
1816
internal WindowProcess(IntPtr hwnd)
19-
{
20-
processId = GetProcessIDFromWindowHandle(hwnd);
21-
processName = GetProcessNameFromProcessID(processId);
22-
}
23-
24-
private static uint GetProcessIDFromWindowHandle(IntPtr hwnd)
2517
{
2618
_ = NativeMethods.GetWindowThreadProcessId(hwnd, out var processId);
27-
return processId;
28-
}
29-
30-
private static string? GetProcessNameFromProcessID(uint processId)
31-
{
32-
var process = Process.GetProcessById((int)processId);
33-
return process?.ProcessName;
19+
process = Process.GetProcessById((int)processId);
3420
}
3521
}
36-
}
22+
}

0 commit comments

Comments
 (0)