diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml new file mode 100644 index 00000000..8fc03e6e --- /dev/null +++ b/.github/workflows/dotnet-build.yml @@ -0,0 +1,35 @@ +name: Dotnet Build + +on: + push: + branches: + - '**' + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + env: + BUILD_CONFIGURATION: Release + SDK_VERSION: 10.0.201 + steps: + - name: Checkout repository + shell: pwsh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git init + git remote add origin "https://x-access-token:${env:GITHUB_TOKEN}@github.com/${env:GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${env:GITHUB_SHA}" + git checkout --force FETCH_HEAD + - name: Install .NET SDK + shell: pwsh + run: | + $installScript = Join-Path $env:RUNNER_TEMP "dotnet-install.ps1" + Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript + & $installScript -Version "${env:SDK_VERSION}" -InstallDir "$env:RUNNER_TEMP\dotnet" + Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\dotnet" + - name: Restore + run: dotnet restore SystemTrayMenu.csproj + - name: Build + run: dotnet build SystemTrayMenu.csproj -c ${{ env.BUILD_CONFIGURATION }} --no-restore diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml new file mode 100644 index 00000000..cd83573d --- /dev/null +++ b/.github/workflows/dotnet-release.yml @@ -0,0 +1,66 @@ +name: Dotnet Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: windows-latest + env: + BUILD_CONFIGURATION: Release + SDK_VERSION: 10.0.201 + OUTPUT_DIR: bin\AnyCPU\Release\net10.0-windows10.0.22000.0\win-x64 + steps: + - name: Checkout repository + shell: pwsh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git init + git remote add origin "https://x-access-token:${env:GITHUB_TOKEN}@github.com/${env:GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${env:GITHUB_SHA}" + git checkout --force FETCH_HEAD + - name: Install .NET SDK + shell: pwsh + run: | + $installScript = Join-Path $env:RUNNER_TEMP "dotnet-install.ps1" + Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript + & $installScript -Version "${env:SDK_VERSION}" -InstallDir "$env:RUNNER_TEMP\dotnet" + Add-Content -Path $env:GITHUB_PATH -Value "$env:RUNNER_TEMP\dotnet" + - name: Restore + run: dotnet restore SystemTrayMenu.csproj + - name: Build + run: dotnet build SystemTrayMenu.csproj -c ${{ env.BUILD_CONFIGURATION }} --no-restore + - name: Create release archive + shell: pwsh + run: | + $archiveName = "SystemTrayMenu-${env:GITHUB_REF_NAME}-win-x64.zip" + $archivePath = Join-Path $env:RUNNER_TEMP $archiveName + if (Test-Path $archivePath) + { + Remove-Item $archivePath -Force + } + + Compress-Archive -Path "${env:OUTPUT_DIR}\*" -DestinationPath $archivePath + Add-Content -Path $env:GITHUB_ENV -Value "RELEASE_ASSET_PATH=$archivePath" + Add-Content -Path $env:GITHUB_ENV -Value "RELEASE_ASSET_NAME=$archiveName" + - name: Publish GitHub Release + shell: pwsh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view "${env:GITHUB_REF_NAME}" --repo "${env:GITHUB_REPOSITORY}" *> $null + if ($LASTEXITCODE -eq 0) + { + gh release upload "${env:GITHUB_REF_NAME}" "${env:RELEASE_ASSET_PATH}#${env:RELEASE_ASSET_NAME}" --repo "${env:GITHUB_REPOSITORY}" --clobber + } + else + { + gh release create "${env:GITHUB_REF_NAME}" "${env:RELEASE_ASSET_PATH}#${env:RELEASE_ASSET_NAME}" --repo "${env:GITHUB_REPOSITORY}" --generate-notes + } diff --git a/.github/workflows/dotnetframework.yml b/.github/workflows/dotnetframework.yml deleted file mode 100644 index 0a316f58..00000000 --- a/.github/workflows/dotnetframework.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: NetFramework - -on: [push] -jobs: - build: - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - name: Setup Nuget.exe - uses: nuget/setup-nuget@v1 - - name: Nuget Restore - run: nuget restore SystemTrayMenu.sln - - name: Setup MSBuild.exe - uses: microsoft/setup-msbuild@v1 - - name: Build with MSBuild - run: msbuild SystemTrayMenu.sln -p:Configuration=Release diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..6cbde431 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..bdaaa3e4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,72 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "SystemTrayMenu: Rebuild + Debug", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "stm: build Debug", + "program": "${workspaceFolder}/bin/AnyCPU/Debug/net10.0-windows10.0.22000.0/win-x64/SystemTrayMenu.exe", + "cwd": "${workspaceFolder}", + "env": { + "STM_DEBUG_ALLOW_MULTI_INSTANCE": "1" + }, + "stopAtEntry": false, + "justMyCode": true, + "console": "internalConsole", + "internalConsoleOptions": "neverOpen", + "logging": { + "moduleLoad": false, + "programOutput": true, + "exceptions": true, + "diagnosticsLog": { + "protocolMessages": false + }, + "browserStdOut": false, + "browserStdErr": false + } + }, + { + "name": "SystemTrayMenu: Build + Debug", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "stm: build Debug", + "program": "${workspaceFolder}/bin/AnyCPU/Debug/net10.0-windows10.0.22000.0/win-x64/SystemTrayMenu.exe", + "cwd": "${workspaceFolder}", + "env": { + "STM_DEBUG_ALLOW_MULTI_INSTANCE": "1" + }, + "stopAtEntry": false, + "justMyCode": true, + "console": "internalConsole", + "internalConsoleOptions": "neverOpen", + "logging": { + "moduleLoad": false, + "programOutput": true, + "exceptions": true, + "diagnosticsLog": { + "protocolMessages": false + }, + "browserStdOut": false, + "browserStdErr": false + } + }, + { + "name": "SystemTrayMenu: Attach to Process", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}", + "justMyCode": true, + "logging": { + "moduleLoad": false, + "programOutput": true, + "exceptions": true, + "diagnosticsLog": { + "protocolMessages": false + }, + "browserStdOut": false, + "browserStdErr": false + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4980f2b7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "dotnet.defaultSolution": "SystemTrayMenu.slnx", + "debug.internalConsoleOptions": "neverOpen", + "omnisharp.enableRoslynAnalyzers": true, + "omnisharp.enableEditorConfigSupport": true, + "csharp.suppressBuildAssetsNotification": true, + "files.exclude": { + "**/bin": true, + "**/obj": true + }, + "search.exclude": { + "**/bin": true, + "**/obj": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..7572ac65 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,106 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "stm: stop running instances", + "type": "shell", + "command": "pwsh", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$processes = @(Get-Process -Name SystemTrayMenu -ErrorAction SilentlyContinue); foreach ($process in $processes) { Stop-Process -Id $process.Id -Force }; exit 0" + ], + "problemMatcher": [] + }, + { + "label": "stm: restore", + "type": "shell", + "command": "dotnet", + "args": [ + "restore", + "${workspaceFolder}/SystemTrayMenu.csproj" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "stm: build Debug", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}/SystemTrayMenu.csproj", + "-c", + "Debug" + ], + "dependsOrder": "sequence", + "dependsOn": [ + "stm: stop running instances", + "stm: restore" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "stm: watch run (Hot Reload)", + "type": "shell", + "command": "pwsh", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$env:STM_DEBUG_ALLOW_MULTI_INSTANCE='1'; dotnet watch --project \"${workspaceFolder}/SystemTrayMenu.csproj\" run -c Debug" + ], + "dependsOrder": "sequence", + "dependsOn": [ + "stm: stop running instances", + "stm: restore" + ], + "isBackground": true, + "group": "none", + "problemMatcher": [ + { + "owner": "dotnet-watch", + "fileLocation": "absolute", + "pattern": [ + { + "regexp": "^(.*)\\((\\d+),(\\d+)\\):\\s*(error|warning)\\s*([A-Z]+\\d+):\\s*(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "code": 5, + "message": 6 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*dotnet watch.*", + "endsPattern": ".*(Hot reload enabled|Watching for file changes).*" + } + } + ] + }, + { + "label": "stm: watch test", + "type": "shell", + "command": "dotnet", + "args": [ + "watch", + "--project", + "${workspaceFolder}/SystemTrayMenu.csproj", + "test", + "-c", + "Debug" + ], + "isBackground": true, + "group": "test", + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Business/IpcPipe.cs b/Business/IpcPipe.cs index 9e51d459..d9060aa3 100644 --- a/Business/IpcPipe.cs +++ b/Business/IpcPipe.cs @@ -141,7 +141,7 @@ internal string ReadString() // Receive message byte[] inBuffer = new byte[len]; - ioStream.Read(inBuffer, 0, len); + ioStream.ReadExactly(inBuffer, 0, len); return streamEncoding.GetString(inBuffer); } diff --git a/Helpers/DirectoryHelpers.cs b/Helpers/DirectoryHelpers.cs index c3066039..01b67132 100644 --- a/Helpers/DirectoryHelpers.cs +++ b/Helpers/DirectoryHelpers.cs @@ -7,7 +7,6 @@ namespace SystemTrayMenu.Helpers using System; using System.Collections.Generic; using System.ComponentModel; - using System.Data; using System.Diagnostics; using System.IO; using System.Linq; @@ -19,6 +18,7 @@ internal static class DirectoryHelpers internal static void DiscoverItems(BackgroundWorker? worker, string path, ref MenuData menuData) { bool isNetworkRoot = false; + Dictionary directoryContentCache = new(StringComparer.InvariantCultureIgnoreCase); try { isNetworkRoot = FileLnk.IsNetworkRoot(path); @@ -28,7 +28,7 @@ internal static void DiscoverItems(BackgroundWorker? worker, string path, ref Me } else { - DiscoverLocalDirectories(worker, path, ref menuData); + DiscoverLocalDirectories(worker, path, ref menuData, directoryContentCache); } } catch (Exception ex) @@ -50,7 +50,7 @@ internal static void DiscoverItems(BackgroundWorker? worker, string path, ref Me { foreach (var additionalPath in GetAddionalPathsForMainMenu()) { - GetDirectoriesAndFilesRecursive(ref menuData, additionalPath.Path, additionalPath.OnlyFiles, additionalPath.Recursive); + GetDirectoriesAndFilesRecursive(ref menuData, additionalPath.Path, additionalPath.OnlyFiles, additionalPath.Recursive, directoryContentCache); } } @@ -185,7 +185,11 @@ private static void DiscoverNetworkRootDirectories(string path, ref MenuData men } } - private static void DiscoverLocalDirectories(BackgroundWorker? worker, string path, ref MenuData menuData) + private static void DiscoverLocalDirectories( + BackgroundWorker? worker, + string path, + ref MenuData menuData, + Dictionary directoryContentCache) { if (!Directory.Exists(path)) { @@ -201,6 +205,11 @@ private static void DiscoverLocalDirectories(BackgroundWorker? worker, string pa return; } + if (!ShouldDisplayDirectory(directory, directoryContentCache)) + { + continue; + } + menuData.RowDatas.Add(new RowData(true, false, menuData.Level, directory)); } @@ -219,7 +228,8 @@ private static void GetDirectoriesAndFilesRecursive( ref MenuData menuData, string path, bool onlyFiles, - bool recursiv) + bool recursiv, + Dictionary directoryContentCache) { try { @@ -230,6 +240,11 @@ private static void GetDirectoriesAndFilesRecursive( foreach (string directory in Directory.GetDirectories(path)) { + if (!ShouldDisplayDirectory(directory, directoryContentCache)) + { + continue; + } + if (!onlyFiles) { menuData.RowDatas.Add(new RowData(true, true, menuData.Level, directory)); @@ -237,7 +252,7 @@ private static void GetDirectoriesAndFilesRecursive( if (recursiv) { - GetDirectoriesAndFilesRecursive(ref menuData, directory, onlyFiles, recursiv); + GetDirectoriesAndFilesRecursive(ref menuData, directory, onlyFiles, recursiv, directoryContentCache); } } } @@ -247,6 +262,61 @@ private static void GetDirectoriesAndFilesRecursive( } } + private static bool ShouldDisplayDirectory(string directoryPath, Dictionary directoryContentCache) + { + if (directoryContentCache.TryGetValue(directoryPath, out bool shouldDisplayDirectory)) + { + return shouldDisplayDirectory; + } + + shouldDisplayDirectory = HasDisplayableChildItems(directoryPath, directoryContentCache); + directoryContentCache[directoryPath] = shouldDisplayDirectory; + return shouldDisplayDirectory; + } + + private static bool HasDisplayableChildItems(string directoryPath, Dictionary directoryContentCache) + { + if (IsEntryHidden(directoryPath)) + { + directoryContentCache[directoryPath] = false; + return false; + } + + try + { + foreach (string file in GetFilesBySearchPattern(directoryPath, Config.SearchPattern)) + { + if (!IsEntryHidden(file)) + { + directoryContentCache[directoryPath] = true; + return true; + } + } + + foreach (string childDirectory in Directory.GetDirectories(directoryPath)) + { + if (ShouldDisplayDirectory(childDirectory, directoryContentCache)) + { + directoryContentCache[directoryPath] = true; + return true; + } + } + } + catch (Exception ex) + { + Log.Warn($"HasDisplayableChildItems path:'{directoryPath}'", ex); + } + + directoryContentCache[directoryPath] = false; + return false; + } + + private static bool IsEntryHidden(string path) + { + FolderOptions.ReadHiddenAttributes(path, out _, out bool isDirectoryToHide); + return isDirectoryToHide; + } + private static List GetFilesBySearchPattern(string path, string searchPatternCombined) { string[] searchPatterns = searchPatternCombined.Split('|'); diff --git a/Helpers/Updater/JsonParser.cs b/Helpers/Updater/JsonParser.cs index 3e169740..e0147b2a 100644 --- a/Helpers/Updater/JsonParser.cs +++ b/Helpers/Updater/JsonParser.cs @@ -29,6 +29,7 @@ namespace SystemTrayMenu.Helpers.Updater using System.Collections; using System.Collections.Generic; using System.Reflection; + using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; @@ -450,7 +451,7 @@ private static Dictionary CreateMemberNameDictionary(T[] members) private static object ParseObject(Type type, string json) { - object instance = FormatterServices.GetUninitializedObject(type); + object instance = RuntimeHelpers.GetUninitializedObject(type); // The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON List elems = Split(json); diff --git a/Packaging/Package.appxmanifest b/Packaging/Package.appxmanifest index b6dc1c68..20254cb2 100644 --- a/Packaging/Package.appxmanifest +++ b/Packaging/Package.appxmanifest @@ -10,7 +10,7 @@ + Version="3.0.0.0" /> SystemTrayMenu diff --git a/Packaging/Packaging.wapproj b/Packaging/Packaging.wapproj index 379eaa07..876be721 100644 --- a/Packaging/Packaging.wapproj +++ b/Packaging/Packaging.wapproj @@ -52,24 +52,26 @@ en-US false ..\SystemTrayMenu.csproj - False True True x86|x64 https://github.com/Hofknecht/SystemTrayMenu/releases 0 Always + $(NoWarn);NU1701 + + + x64 - - - + Platform=x64 + TargetFramework=net10.0-windows10.0.22000.0 + true Designer - @@ -125,7 +127,6 @@ - \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index fafdd92a..25df4616 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -39,5 +39,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] +[assembly: AssemblyVersion("3.0.0.0")] +[assembly: AssemblyFileVersion("3.0.0.0")] diff --git a/SystemTrayMenu.csproj b/SystemTrayMenu.csproj index 092f29e9..e01711da 100644 --- a/SystemTrayMenu.csproj +++ b/SystemTrayMenu.csproj @@ -2,12 +2,10 @@ - - net7.0-windows10.0.22000.0 + net10.0-windows10.0.22000.0 - win-x64 - linux-x64 - True + win-x64 + true enable true x64 @@ -26,7 +24,7 @@ false true 0 - 1.0.0.%2a + 3.0.0.%2a true false taskkill /f /fi "pid gt 0" /im SystemTrayMenu.exe >nul @@ -35,7 +33,6 @@ EXIT 0 icon.png hofknecht.eu/systemtraymenu/ LICENSE - SystemTrayMenu 10.0.22000.0 README.md @@ -87,15 +84,10 @@ EXIT 0 - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -106,24 +98,4 @@ EXIT 0 - - - {F935DC20-1CF0-11D0-ADB9-00C04FD58A0B} - 1 - 0 - 0 - tlbimp - False - True - - - {50A7E9B0-70EF-11D1-B75A-00A0C90564FE} - 1 - 0 - 0 - tlbimp - False - True - - \ No newline at end of file diff --git a/SystemTrayMenu.slnx b/SystemTrayMenu.slnx new file mode 100644 index 00000000..babecbdf --- /dev/null +++ b/SystemTrayMenu.slnx @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UserInterface/Menu.xaml.cs b/UserInterface/Menu.xaml.cs index f23b95be..af99de2c 100644 --- a/UserInterface/Menu.xaml.cs +++ b/UserInterface/Menu.xaml.cs @@ -169,7 +169,7 @@ internal Menu(RowData? rowDataParent, string path) { if (Clipboard.ContainsText(TextDataFormat.Text)) { - textBoxSearch.SelectedText = Clipboard.GetData(DataFormats.Text).ToString(); + textBoxSearch.SelectedText = Clipboard.GetData(DataFormats.Text)?.ToString() ?? string.Empty; } }), }); diff --git a/UserInterface/ShellContextMenu.cs b/UserInterface/ShellContextMenu.cs index 50cd6291..7cef67c0 100644 --- a/UserInterface/ShellContextMenu.cs +++ b/UserInterface/ShellContextMenu.cs @@ -796,8 +796,8 @@ public void ShowContextMenu(Point pointScreen) CmdFirst, CmdLast, CMF.EXPLORE | CMF.NORMAL | ((Keyboard.Modifiers & ModifierKeys.Shift) != 0 ? CMF.EXTENDEDVERBS : 0)); - Marshal.QueryInterface(iContextMenuPtr, ref iidIContextMenu2, out iContextMenuPtr2); - Marshal.QueryInterface(iContextMenuPtr, ref iidIContextMenu3, out iContextMenuPtr3); + Marshal.QueryInterface(iContextMenuPtr, in iidIContextMenu2, out iContextMenuPtr2); + Marshal.QueryInterface(iContextMenuPtr, in iidIContextMenu3, out iContextMenuPtr3); oContextMenu2 = (IContextMenu2)Marshal.GetTypedObjectForIUnknown(iContextMenuPtr2, typeof(IContextMenu2)); oContextMenu3 = (IContextMenu3)Marshal.GetTypedObjectForIUnknown(iContextMenuPtr3, typeof(IContextMenu3)); diff --git a/Utilities/File/FileLnk.cs b/Utilities/File/FileLnk.cs index ce84a6fd..e577c832 100644 --- a/Utilities/File/FileLnk.cs +++ b/Utilities/File/FileLnk.cs @@ -6,8 +6,9 @@ namespace SystemTrayMenu.Utilities { using System; using System.IO; + using System.Reflection; + using System.Runtime.InteropServices; using System.Threading; - using Shell32; internal class FileLnk { @@ -56,38 +57,53 @@ private static string GetShortcutFileNamePath(object shortcutFilename, out bool isFolder = false; try { - string? pathOnly = Path.GetDirectoryName((string)shortcutFilename); - string? filenameOnly = Path.GetFileName((string)shortcutFilename); - - Shell shell = new(); - Folder folder = shell.NameSpace(pathOnly); - if (folder == null) + string shortcutPath = (string)shortcutFilename; + Type? shellType = Type.GetTypeFromProgID("WScript.Shell"); + if (shellType == null) { - Log.Info($"{nameof(GetShortcutFileNamePath)} folder == null for path:'{shortcutFilename}'"); + Log.Info($"{nameof(GetShortcutFileNamePath)} WScript.Shell COM type not found for path:'{shortcutFilename}'"); return resolvedFilename; } - FolderItem folderItem = folder.ParseName(filenameOnly); - if (folderItem == null) + object shell = Activator.CreateInstance(shellType)!; + object? shortcut = shellType.InvokeMember( + "CreateShortcut", + BindingFlags.InvokeMethod, + binder: null, + target: shell, + args: new object[] { shortcutPath }); + if (shortcut == null) { - Log.Info($"{nameof(GetShortcutFileNamePath)} folderItem == null for path:'{shortcutFilename}'"); + Log.Info($"{nameof(GetShortcutFileNamePath)} CreateShortcut returned null for path:'{shortcutFilename}'"); return resolvedFilename; } - ShellLinkObject link = (ShellLinkObject)folderItem.GetLink; - isFolder = link.Target.IsFolder; - if (string.IsNullOrEmpty(link.Path)) + Type shortcutType = shortcut.GetType(); + object? targetPathObject = shortcutType.InvokeMember( + "TargetPath", + BindingFlags.GetProperty, + binder: null, + target: shortcut, + args: null); + string targetPath = targetPathObject as string ?? string.Empty; + if (!string.IsNullOrEmpty(targetPath)) { - // https://github.com/Hofknecht/SystemTrayMenu/issues/242 - // do not set CLSID key (GUID) shortcuts as resolvedFilename - if (!link.Target.Path.Contains("::{")) + // Keep previous behavior: skip virtual-shell CLSID paths. + if (!targetPath.Contains("::{", StringComparison.InvariantCulture)) { - resolvedFilename = link.Target.Path; + resolvedFilename = targetPath; + isFolder = Directory.Exists(targetPath); } } - else + + if (Marshal.IsComObject(shortcut)) + { + _ = Marshal.FinalReleaseComObject(shortcut); + } + + if (Marshal.IsComObject(shell)) { - resolvedFilename = link.Path; + _ = Marshal.FinalReleaseComObject(shell); } } catch (UnauthorizedAccessException) diff --git a/Utilities/FolderOptions.cs b/Utilities/FolderOptions.cs index 55cffd6c..d14c9f4a 100644 --- a/Utilities/FolderOptions.cs +++ b/Utilities/FolderOptions.cs @@ -8,29 +8,52 @@ namespace SystemTrayMenu.Utilities using System.IO; using System.Reflection; using System.Runtime.InteropServices; - using Shell32; internal static class FolderOptions { private static bool hideHiddenEntries; private static bool hideSystemEntries; - private static IShellDispatch4? iShellDispatch4; internal static void Initialize() { try { - iShellDispatch4 = (IShellDispatch4?)Activator.CreateInstance( - Type.GetTypeFromProgID("Shell.Application")!); + Type? shellType = Type.GetTypeFromProgID("Shell.Application"); + if (shellType == null) + { + Log.Info("Get Shell COM type failed"); + return; + } + + object shell = Activator.CreateInstance(shellType)!; // Using SHGetSetSettings would be much better in performance but the results are not accurate. // We have to go for the shell interface in order to receive the correct settings: // https://docs.microsoft.com/en-us/windows/win32/shell/ishelldispatch4-getsetting const int SSF_SHOWALLOBJECTS = 0x00000001; - hideHiddenEntries = !(iShellDispatch4?.GetSetting(SSF_SHOWALLOBJECTS) ?? false); + object? showAllObjectsObject = shellType.InvokeMember( + "GetSetting", + BindingFlags.InvokeMethod, + binder: null, + target: shell, + args: new object[] { SSF_SHOWALLOBJECTS }); + bool showAllObjects = showAllObjectsObject is bool currentShowAllObjects && currentShowAllObjects; + hideHiddenEntries = !showAllObjects; const int SSF_SHOWSUPERHIDDEN = 0x00040000; - hideSystemEntries = !(iShellDispatch4?.GetSetting(SSF_SHOWSUPERHIDDEN) ?? false); + object? showSuperHiddenObject = shellType.InvokeMember( + "GetSetting", + BindingFlags.InvokeMethod, + binder: null, + target: shell, + args: new object[] { SSF_SHOWSUPERHIDDEN }); + bool showSuperHidden = showSuperHiddenObject is bool currentShowSuperHidden && currentShowSuperHidden; + hideSystemEntries = !showSuperHidden; + + if (Marshal.IsComObject(shell)) + { + _ = Marshal.FinalReleaseComObject(shell); + } } catch (Exception ex) { diff --git a/Utilities/GenerateDriveShortcuts.cs b/Utilities/GenerateDriveShortcuts.cs index 2b5bdece..825317ef 100644 --- a/Utilities/GenerateDriveShortcuts.cs +++ b/Utilities/GenerateDriveShortcuts.cs @@ -8,7 +8,8 @@ namespace SystemTrayMenu.Utilities using System.Collections.Generic; using System.IO; using System.Linq; - using IWshRuntimeLibrary; + using System.Reflection; + using System.Runtime.InteropServices; internal class GenerateDriveShortcuts { @@ -44,14 +45,57 @@ public static void Start() private static void CreateShortcut(string linkPath, string targetPath) { - WshShell shell = new(); - IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(linkPath); - shortcut.Description = "Generated by SystemTrayMenu"; - shortcut.TargetPath = targetPath; - try { - shortcut.Save(); + Type? shellType = Type.GetTypeFromProgID("WScript.Shell"); + if (shellType == null) + { + Log.Info("CreateShortcut WScript.Shell COM type not found"); + return; + } + + object shell = Activator.CreateInstance(shellType)!; + object? shortcut = shellType.InvokeMember( + "CreateShortcut", + BindingFlags.InvokeMethod, + binder: null, + target: shell, + args: new object[] { linkPath }); + if (shortcut == null) + { + Log.Info($"CreateShortcut returned null for path:'{linkPath}'"); + return; + } + + Type shortcutType = shortcut.GetType(); + _ = shortcutType.InvokeMember( + "Description", + BindingFlags.SetProperty, + binder: null, + target: shortcut, + args: new object[] { "Generated by SystemTrayMenu" }); + _ = shortcutType.InvokeMember( + "TargetPath", + BindingFlags.SetProperty, + binder: null, + target: shortcut, + args: new object[] { targetPath }); + _ = shortcutType.InvokeMember( + "Save", + BindingFlags.InvokeMethod, + binder: null, + target: shortcut, + args: null); + + if (Marshal.IsComObject(shortcut)) + { + _ = Marshal.FinalReleaseComObject(shortcut); + } + + if (Marshal.IsComObject(shell)) + { + _ = Marshal.FinalReleaseComObject(shell); + } } catch (Exception ex) { diff --git a/Utilities/Log.cs b/Utilities/Log.cs index 406676db..bad6e179 100644 --- a/Utilities/Log.cs +++ b/Utilities/Log.cs @@ -10,10 +10,10 @@ namespace SystemTrayMenu.Utilities using System.Diagnostics; using System.IO; using System.Reflection; + using System.Runtime.InteropServices; using System.Threading; using System.Windows; using Clearcove.Logging; - using IWshRuntimeLibrary; using File = System.IO.File; internal static class Log @@ -169,16 +169,50 @@ internal static void ProcessStart( .Equals(".lnk", StringComparison.InvariantCultureIgnoreCase); if (isLink) { - WshShell shell = new(); - IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(fileName); - bool startAsAdmin = shortcut.WindowStyle == 3; - if (startAsAdmin) + Type? shellType = Type.GetTypeFromProgID("WScript.Shell"); + if (shellType != null) { - verb = "runas"; + object shell = Activator.CreateInstance(shellType)!; + object? shortcut = shellType.InvokeMember( + "CreateShortcut", + BindingFlags.InvokeMethod, + binder: null, + target: shell, + args: new object[] { fileName }); + if (shortcut == null) + { + Log.Info($"CreateShortcut returned null for path:'{fileName}'"); + goto StartProcess; + } + + Type shortcutType = shortcut.GetType(); + object? windowStyleObject = shortcutType.InvokeMember( + "WindowStyle", + BindingFlags.GetProperty, + binder: null, + target: shortcut, + args: null); + int windowStyle = windowStyleObject is int style ? style : 0; + bool startAsAdmin = windowStyle == 3; + if (startAsAdmin) + { + verb = "runas"; + } + + if (Marshal.IsComObject(shortcut)) + { + _ = Marshal.FinalReleaseComObject(shortcut); + } + + if (Marshal.IsComObject(shell)) + { + _ = Marshal.FinalReleaseComObject(shell); + } } } } + StartProcess: using Process p = new() { StartInfo = new ProcessStartInfo(fileName) diff --git a/Utilities/SingleAppInstance.cs b/Utilities/SingleAppInstance.cs index c8be514c..4727e48c 100644 --- a/Utilities/SingleAppInstance.cs +++ b/Utilities/SingleAppInstance.cs @@ -14,12 +14,19 @@ internal static class SingleAppInstance private const string IpcServiceName = nameof(SingleAppInstance); private const string IpcWakeupCmd = "wakeup"; private const string IpcWakeupResponseOK = "OK"; + private const string DebugAllowMultiInstanceEnvironmentVariable = "STM_DEBUG_ALLOW_MULTI_INSTANCE"; private static IpcPipe? ipcPipe; internal static event Action? Wakeup; internal static bool Initialize() { + if (ShouldSkipSingleInstance()) + { + Log.Info("Skip single instance check for debug session"); + return true; + } + bool success = true; try @@ -92,5 +99,13 @@ internal static void Unload() { ipcPipe?.Dispose(); } + + private static bool ShouldSkipSingleInstance() + { + string? environmentValue = Environment.GetEnvironmentVariable(DebugAllowMultiInstanceEnvironmentVariable); + return Debugger.IsAttached || + string.Equals(environmentValue, "1", StringComparison.OrdinalIgnoreCase) || + string.Equals(environmentValue, "true", StringComparison.OrdinalIgnoreCase); + } } } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 896aa60f..a96f7c31 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,7 +20,7 @@ steps: - task: UseDotNet@2 inputs: packageType: sdk - version: 6.0.x + version: 10.0.x - task: DotNetCoreCLI@2 inputs: @@ -40,7 +40,7 @@ steps: - task: ArchiveFiles@2 displayName: 'Archive $(Build.ArtifactStagingDirectory)' inputs: - rootFolderOrFile: '$(Build.SourcesDirectory)\bin\AnyCPU\Release\net6.0-windows10.0.22000.0\publish\win-x64\' + rootFolderOrFile: '$(Build.SourcesDirectory)\bin\AnyCPU\Release\net10.0-windows10.0.22000.0\publish\win-x64\' includeRootFolder: false archiveFile: '$(Build.ArtifactStagingDirectory)/SystemTrayMenu-$(AssemblyInfo.AssemblyVersion).zip' diff --git a/log-APCAPOYKIDLPMOO.txt b/log-APCAPOYKIDLPMOO.txt new file mode 100644 index 00000000..46457ecb --- /dev/null +++ b/log-APCAPOYKIDLPMOO.txt @@ -0,0 +1,8 @@ +2026/03/24 15:41:48.470 2 INFO Skip single instance check for debug session +2026/03/24 15:41:48.706 2 INFO Application Start SystemTrayMenu.dll | 2.0.0.0 | ScalingFactor=1 +2026/03/24 16:05:45.295 2 INFO Skip single instance check for debug session +2026/03/24 16:05:45.474 2 INFO Application Start SystemTrayMenu.dll | 2.0.0.0 | ScalingFactor=1 +2026/03/24 16:06:20.464 2 INFO Restart by 'ByConfigChange' +2026/03/24 16:06:20.949 2 INFO Skip single instance check for debug session +2026/03/24 16:06:21.057 2 INFO Application Start SystemTrayMenu.dll | 2.0.0.0 | ScalingFactor=1 +2026/03/24 16:07:09.375 2 INFO Hotkey registration cannot unregister key Control, Windows with modifiers Control, Windows