[AOT][CmdPal] Enable NativeAOT Compatibility for CmdPal Apps Extension (#39678)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces necessary changes to make the CmdPal.Apps extension
compatible with NativeAOT publishing. The main updates include:

1. Project configuration updates: Added NativeAOT-related properties to
the .csproj.

2. Native interop adjustments:

> - Introduced NativeMethods.json and used
[CsWin32](https://github.com/microsoft/cswin32) to generate P/Invoke
bindings.
> - Replaced some DllImport declarations with source-generated
[LibraryImport] for improved AOT support.
This commit is contained in:
leileizhang
2025-06-04 19:30:11 +08:00
committed by GitHub
parent ddbb6161e3
commit 79958975b4
26 changed files with 477 additions and 700 deletions

View File

@@ -14,6 +14,7 @@ AColumn
acrt acrt
ACTIVATEAPP ACTIVATEAPP
activationaction activationaction
ACTIVATEOPTIONS
ACVS ACVS
adaptivecards adaptivecards
ADate ADate
@@ -1588,6 +1589,7 @@ steamapps
STGC STGC
STGM STGM
STGMEDIUM STGMEDIUM
STGMREAD
STICKYKEYS STICKYKEYS
sticpl sticpl
storelogo storelogo
@@ -1692,6 +1694,7 @@ TLayout
tlb tlb
tlbimp tlbimp
tlc tlc
TGM
TNP TNP
Toolhelp Toolhelp
toolkitconverters toolkitconverters

View File

@@ -7,6 +7,6 @@
<CsWinRTAotWarningLevel>2</CsWinRTAotWarningLevel> <CsWinRTAotWarningLevel>2</CsWinRTAotWarningLevel>
<!-- Suppress DynamicallyAccessedMemberTypes.PublicParameterlessConstructor in fallback code path of Windows SDK projection --> <!-- Suppress DynamicallyAccessedMemberTypes.PublicParameterlessConstructor in fallback code path of Windows SDK projection -->
<WarningsNotAsErrors>IL2081;$(WarningsNotAsErrors)</WarningsNotAsErrors> <WarningsNotAsErrors>IL2081;CsWinRT1028;$(WarningsNotAsErrors)</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -71,7 +71,7 @@ public class AllAppsSettings : JsonSettingsManager
internal static string SettingsJsonPath() internal static string SettingsJsonPath()
{ {
string directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
// now, the state is just next to the exe // now, the state is just next to the exe

View File

@@ -8,7 +8,12 @@ using System.Threading.Tasks;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Services.Maps;
using Windows.Win32;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using WyHash; using WyHash;
namespace Microsoft.CmdPal.Ext.Apps; namespace Microsoft.CmdPal.Ext.Apps;
@@ -27,26 +32,31 @@ internal sealed partial class AppCommand : InvokableCommand
internal static async Task StartApp(string aumid) internal static async Task StartApp(string aumid)
{ {
var appManager = new ApplicationActivationManager();
const ActivateOptions noFlags = ActivateOptions.None;
await Task.Run(() => await Task.Run(() =>
{ {
try unsafe
{ {
appManager.ActivateApplication(aumid, /*queryArguments*/ string.Empty, noFlags, out var unusedPid); IApplicationActivationManager* appManager = null;
} try
catch (System.Exception ex) {
{ PInvoke.CoCreateInstance(typeof(ApplicationActivationManager).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out appManager).ThrowOnFailure();
Logger.LogError(ex.Message); using var handle = new SafeComHandle((IntPtr)appManager);
appManager->ActivateApplication(
aumid,
string.Empty,
ACTIVATEOPTIONS.AO_NONE,
out var unusedPid);
}
catch (System.Exception ex)
{
Logger.LogError(ex.Message);
}
} }
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
internal static async Task StartExe(string path) internal static async Task StartExe(string path)
{ {
var appManager = new ApplicationActivationManager();
// const ActivateOptions noFlags = ActivateOptions.None;
await Task.Run(() => await Task.Run(() =>
{ {
try try

View File

@@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" /> <Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup> <PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Apps</RootNamespace> <RootNamespace>Microsoft.CmdPal.Ext.Apps</RootNamespace>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath> <OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<DisableRuntimeMarshalling>true</DisableRuntimeMarshalling>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -49,4 +51,9 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput> <LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<AdditionalFiles Include="NativeMethods.txt" />
<AdditionalFiles Include="NativeMethods.json" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false,
"comInterop": {
"preserveSigMethods": [ "*" ]
}
}

View File

@@ -1,19 +1,20 @@
GetPhysicallyInstalledSystemMemory IStream
GlobalMemoryStatusEx
GetSystemInfo
CoCreateInstance CoCreateInstance
SetForegroundWindow IApplicationActivationManager
IsIconic ApplicationActivationManager
RegisterHotKey
SetWindowLongPtr
CallWindowProc
ShowWindow
SetForegroundWindow
SetFocus
SetActiveWindow
MonitorFromWindow
GetMonitorInfo
SHCreateStreamOnFileEx SHCreateStreamOnFileEx
CoAllowSetForegroundWindow SHCreateItemFromParsingName
SHCreateStreamOnFileEx IShellItem
SHLoadIndirectString ISequentialStream
SHLoadIndirectString
IAppxFactory
AppxFactory
IAppxManifestReader
IAppxManifestApplicationsEnumerator
IAppxManifestApplication
IAppxManifestProperties
IShellLinkW
ShellLink
IPersistFile
CoTaskMemFree
IUnknown

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
// Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application
[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781")]
[ComImport]
public class AppxFactory
{
}

View File

@@ -2,42 +2,75 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.UI.Xaml.Controls;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Storage.Packaging.Appx;
using Windows.Win32.System.Com; using Windows.Win32.System.Com;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs; namespace Microsoft.CmdPal.Ext.Apps.Programs;
public static class AppxPackageHelper public static class AppxPackageHelper
{ {
private static readonly IAppxFactory AppxFactory = (IAppxFactory)new AppxFactory(); internal static unsafe List<IntPtr> GetAppsFromManifest(IStream* stream)
// This function returns a list of attributes of applications
internal static IEnumerable<IAppxManifestApplication> GetAppsFromManifest(IStream stream)
{ {
var reader = AppxFactory.CreateManifestReader(stream); PInvoke.CoCreateInstance(typeof(AppxFactory).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out IAppxFactory* appxFactory).ThrowOnFailure();
var manifestApps = reader.GetApplications(); using var handle = new SafeComHandle((IntPtr)appxFactory);
while (manifestApps.GetHasCurrent()) IAppxManifestReader* reader = null;
IAppxManifestApplicationsEnumerator* manifestApps = null;
var result = new List<IntPtr>();
appxFactory->CreateManifestReader(stream, &reader);
using var readerHandle = new SafeComHandle((IntPtr)reader);
reader->GetApplications(&manifestApps);
using var manifestAppsHandle = new SafeComHandle((IntPtr)manifestApps);
while (true)
{ {
var manifestApp = manifestApps.GetCurrent(); manifestApps->GetHasCurrent(out var hasCurrent);
var hr = manifestApp.GetStringValue("AppListEntry", out var appListEntry); if (hasCurrent == false)
_ = CheckHRAndReturnOrThrow(hr, appListEntry);
if (appListEntry != "none")
{ {
yield return manifestApp; break;
} }
manifestApps.MoveNext(); IAppxManifestApplication* manifestApp = null;
}
}
internal static T CheckHRAndReturnOrThrow<T>(HRESULT hr, T result) try
{ {
if (hr != HRESULT.S_OK) manifestApps->GetCurrent(&manifestApp).ThrowOnFailure();
{
Marshal.ThrowExceptionForHR((int)hr); var hr = manifestApp->GetStringValue("AppListEntry", out var appListEntryPtr);
var appListEntry = ComFreeHelper.GetStringAndFree(hr, appListEntryPtr);
if (appListEntry != "none")
{
result.Add((IntPtr)manifestApp);
}
else if (manifestApp != null)
{
manifestApp->Release();
}
}
catch (Exception ex)
{
if (manifestApp != null)
{
manifestApp->Release();
}
Logger.LogError($"Failed to get current application from manifest: {ex.Message}");
}
manifestApps->MoveNext(out var hasNext);
if (hasNext == false)
{
break;
}
} }
return result; return result;

View File

@@ -1,47 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
// Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs
[Flags]
public enum ActivateOptions
{
None = 0x00000000,
DesignMode = 0x00000001,
NoErrorUI = 0x00000002,
NoSplashScreen = 0x00000004,
}
// ApplicationActivationManager
[ComImport]
[Guid("2e941141-7f97-4756-ba1d-9decde894a3d")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IApplicationActivationManager
{
IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}
// Application Activation Manager Class
[ComImport]
[Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
public class ApplicationActivationManager : IApplicationActivationManager
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/]
public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using Windows.Win32.System.Com;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxFactory
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
void _VtblGap0_2(); // skip 2 methods
internal IAppxManifestReader CreateManifestReader(IStream inputStream);
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplication
{
[PreserveSig]
HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
[PreserveSig]
HRESULT GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value);
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplicationsEnumerator
{
IAppxManifestApplication GetCurrent();
bool GetHasCurrent();
bool MoveNext();
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestProperties
{
[PreserveSig]
HRESULT GetBoolValue([MarshalAs(UnmanagedType.LPWStr)] string name, out bool value);
[PreserveSig]
HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Guid("4E1BD148-55A0-4480-A3D1-15544710637C")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestReader
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
void _VtblGap0_1(); // skip 1 method
IAppxManifestProperties GetProperties();
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
void _VtblGap1_5(); // skip 5 methods
IAppxManifestApplicationsEnumerator GetApplications();
}

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -16,7 +17,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs;
/// <summary> /// <summary>
/// Provides access to NTFS reparse points in .Net. /// Provides access to NTFS reparse points in .Net.
/// </summary> /// </summary>
public static class ReparsePoint public static partial class ReparsePoint
{ {
#pragma warning disable SA1310 // Field names should not contain underscore #pragma warning disable SA1310 // Field names should not contain underscore
@@ -36,7 +37,7 @@ public static class ReparsePoint
#pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1310 // Field names should not contain underscore
[Flags] [Flags]
private enum FileAccessType : uint internal enum FileAccessType : uint
{ {
DELETE = 0x00010000, DELETE = 0x00010000,
READ_CONTROL = 0x00020000, READ_CONTROL = 0x00020000,
@@ -100,7 +101,7 @@ public static class ReparsePoint
} }
[Flags] [Flags]
private enum FileShareType : uint internal enum FileShareType : uint
{ {
None = 0x00000000, None = 0x00000000,
Read = 0x00000001, Read = 0x00000001,
@@ -108,7 +109,7 @@ public static class ReparsePoint
Delete = 0x00000004, Delete = 0x00000004,
} }
private enum CreationDisposition : uint internal enum CreationDisposition : uint
{ {
New = 1, New = 1,
CreateAlways = 2, CreateAlways = 2,
@@ -118,7 +119,7 @@ public static class ReparsePoint
} }
[Flags] [Flags]
private enum FileAttributes : uint internal enum FileAttributes : uint
{ {
Readonly = 0x00000001, Readonly = 0x00000001,
Hidden = 0x00000002, Hidden = 0x00000002,
@@ -195,8 +196,9 @@ public static class ReparsePoint
public AppExecutionAliasReparseTagBufferLayoutVersion Version; public AppExecutionAliasReparseTagBufferLayoutVersion Version;
} }
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
private static extern bool DeviceIoControl( [return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DeviceIoControl(
IntPtr hDevice, IntPtr hDevice,
uint dwIoControlCode, uint dwIoControlCode,
IntPtr inBuffer, IntPtr inBuffer,
@@ -206,8 +208,8 @@ public static class ReparsePoint
out int pBytesReturned, out int pBytesReturned,
IntPtr lpOverlapped); IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
private static extern IntPtr CreateFile( internal static partial int CreateFile(
string lpFileName, string lpFileName,
FileAccessType dwDesiredAccess, FileAccessType dwDesiredAccess,
FileShareType dwShareMode, FileShareType dwShareMode,
@@ -284,15 +286,18 @@ public static class ReparsePoint
ThrowLastWin32Error("Unable to get information about reparse point."); ThrowLastWin32Error("Unable to get information about reparse point.");
} }
AppExecutionAliasReparseTagHeader aliasReparseHeader = Marshal.PtrToStructure<AppExecutionAliasReparseTagHeader>(outBuffer); unsafe
if (aliasReparseHeader.ReparseTag == IO_REPARSE_TAG_APPEXECLINK)
{ {
var metadata = AppExecutionAliasMetadata.FromPersistedRepresentationIntPtr( var aliasReparseHeader = Unsafe.Read<AppExecutionAliasReparseTagHeader>((void*)outBuffer);
outBuffer,
aliasReparseHeader.Version);
return metadata.ExePath; if (aliasReparseHeader.ReparseTag == IO_REPARSE_TAG_APPEXECLINK)
{
var metadata = AppExecutionAliasMetadata.FromPersistedRepresentationIntPtr(
outBuffer,
aliasReparseHeader.Version);
return metadata.ExePath;
}
} }
return null; return null;
@@ -319,61 +324,65 @@ public static class ReparsePoint
public static AppExecutionAliasMetadata FromPersistedRepresentationIntPtr(IntPtr reparseDataBufferPtr, AppExecutionAliasReparseTagBufferLayoutVersion version) public static AppExecutionAliasMetadata FromPersistedRepresentationIntPtr(IntPtr reparseDataBufferPtr, AppExecutionAliasReparseTagBufferLayoutVersion version)
{ {
var dataOffset = Marshal.SizeOf<AppExecutionAliasReparseTagHeader>(); unsafe
var dataBufferPtr = reparseDataBufferPtr + dataOffset;
string? packageFullName = null;
string? packageFamilyName = null;
string? aumid = null;
string? exePath = null;
VerifyVersion(version);
switch (version)
{ {
case AppExecutionAliasReparseTagBufferLayoutVersion.Initial: var dataOffset = Unsafe.SizeOf<AppExecutionAliasReparseTagHeader>();
packageFullName = Marshal.PtrToStringUni(dataBufferPtr);
if (packageFullName is not null)
{
dataBufferPtr += Encoding.Unicode.GetByteCount(packageFullName) + Encoding.Unicode.GetByteCount("\0");
aumid = Marshal.PtrToStringUni(dataBufferPtr);
if (aumid is not null) var dataBufferPtr = reparseDataBufferPtr + dataOffset;
string? packageFullName = null;
string? packageFamilyName = null;
string? aumid = null;
string? exePath = null;
VerifyVersion(version);
switch (version)
{
case AppExecutionAliasReparseTagBufferLayoutVersion.Initial:
packageFullName = Marshal.PtrToStringUni(dataBufferPtr);
if (packageFullName is not null)
{ {
dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0"); dataBufferPtr += Encoding.Unicode.GetByteCount(packageFullName) + Encoding.Unicode.GetByteCount("\0");
exePath = Marshal.PtrToStringUni(dataBufferPtr); aumid = Marshal.PtrToStringUni(dataBufferPtr);
if (aumid is not null)
{
dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0");
exePath = Marshal.PtrToStringUni(dataBufferPtr);
}
} }
}
break; break;
case AppExecutionAliasReparseTagBufferLayoutVersion.PackageFamilyName: case AppExecutionAliasReparseTagBufferLayoutVersion.PackageFamilyName:
case AppExecutionAliasReparseTagBufferLayoutVersion.MultiAppTypeSupport: case AppExecutionAliasReparseTagBufferLayoutVersion.MultiAppTypeSupport:
packageFamilyName = Marshal.PtrToStringUni(dataBufferPtr); packageFamilyName = Marshal.PtrToStringUni(dataBufferPtr);
if (packageFamilyName is not null) if (packageFamilyName is not null)
{
dataBufferPtr += Encoding.Unicode.GetByteCount(packageFamilyName) + Encoding.Unicode.GetByteCount("\0");
aumid = Marshal.PtrToStringUni(dataBufferPtr);
if (aumid is not null)
{ {
dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0"); dataBufferPtr += Encoding.Unicode.GetByteCount(packageFamilyName) + Encoding.Unicode.GetByteCount("\0");
aumid = Marshal.PtrToStringUni(dataBufferPtr);
exePath = Marshal.PtrToStringUni(dataBufferPtr); if (aumid is not null)
{
dataBufferPtr += Encoding.Unicode.GetByteCount(aumid) + Encoding.Unicode.GetByteCount("\0");
exePath = Marshal.PtrToStringUni(dataBufferPtr);
}
} }
}
break; break;
}
return new AppExecutionAliasMetadata
{
PackageFullName = packageFullName ?? string.Empty,
PackageFamilyName = packageFamilyName ?? string.Empty,
Aumid = aumid ?? string.Empty,
ExePath = exePath ?? string.Empty,
};
} }
return new AppExecutionAliasMetadata
{
PackageFullName = packageFullName ?? string.Empty,
PackageFamilyName = packageFamilyName ?? string.Empty,
Aumid = aumid ?? string.Empty,
ExePath = exePath ?? string.Empty,
};
} }
private static void VerifyVersion(AppExecutionAliasReparseTagBufferLayoutVersion version) private static void VerifyVersion(AppExecutionAliasReparseTagBufferLayoutVersion version)

View File

@@ -3,17 +3,19 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Utils; using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.Storage.Packaging.Appx;
using Windows.Win32.System.Com; using Windows.Win32.System.Com;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
namespace Microsoft.CmdPal.Ext.Apps.Programs; namespace Microsoft.CmdPal.Ext.Apps.Programs;
@@ -55,7 +57,7 @@ public partial class UWP
FamilyName = package.FamilyName; FamilyName = package.FamilyName;
} }
public void InitializeAppInfo(string installedLocation) public unsafe void InitializeAppInfo(string installedLocation)
{ {
Location = installedLocation; Location = installedLocation;
LocationLocalized = ShellLocalization.Instance.GetLocalizedPath(installedLocation); LocationLocalized = ShellLocalization.Instance.GetLocalizedPath(installedLocation);
@@ -65,26 +67,31 @@ public partial class UWP
InitPackageVersion(namespaces); InitPackageVersion(namespaces);
const uint noAttribute = 0x80; const uint noAttribute = 0x80;
const uint STGMREAD = 0x00000000;
var access = (uint)STGM.READ; try
var hResult = PInvoke.SHCreateStreamOnFileEx(path, access, noAttribute, false, null, out IStream stream);
// S_OK
if (hResult == 0)
{ {
Apps = AppxPackageHelper.GetAppsFromManifest(stream).Select(appInManifest => new UWPApplication(appInManifest, this)).Where(a => IStream* stream = null;
PInvoke.SHCreateStreamOnFileEx(path, STGMREAD, noAttribute, false, null, &stream).ThrowOnFailure();
using var streamHandle = new SafeComHandle((IntPtr)stream);
Apps = AppxPackageHelper.GetAppsFromManifest(stream).Select(appInManifest =>
{
using var appHandle = new SafeComHandle(appInManifest);
return new UWPApplication((IAppxManifestApplication*)appInManifest, this);
}).Where(a =>
{ {
var valid = var valid =
!string.IsNullOrEmpty(a.UserModelId) && !string.IsNullOrEmpty(a.UserModelId) &&
!string.IsNullOrEmpty(a.DisplayName) && !string.IsNullOrEmpty(a.DisplayName) &&
a.AppListEntry != "none"; a.AppListEntry != "none";
return valid; return valid;
}).ToList(); }).ToList();
} }
else catch (Exception ex)
{ {
Apps = Array.Empty<UWPApplication>(); Apps = Array.Empty<UWPApplication>();
Logger.LogError($"Failed to initialize UWP app info for {Name} ({FullName}): {ex.Message}");
return;
} }
} }
@@ -123,35 +130,36 @@ public partial class UWP
{ {
var windows10 = new Version(10, 0); var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major; var support = Environment.OSVersion.Version.Major >= windows10.Major;
if (support)
{
var applications = CurrentUserPackages().AsParallel().SelectMany(p =>
{
UWP u;
try
{
u = new UWP(p);
u.InitializeAppInfo(p.InstalledLocation);
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
return Array.Empty<UWPApplication>();
}
return u.Apps; if (!support)
});
var updatedListWithoutDisabledApps = applications
.Where(t1 => AllAppsSettings.Instance.DisabledProgramSources.All(x => x.UniqueIdentifier != t1.UniqueIdentifier))
.Select(x => x);
return updatedListWithoutDisabledApps.ToArray();
}
else
{ {
return Array.Empty<UWPApplication>(); return Array.Empty<UWPApplication>();
} }
var appsBag = new ConcurrentBag<UWPApplication>();
Parallel.ForEach(CurrentUserPackages(), p =>
{
try
{
var u = new UWP(p);
u.InitializeAppInfo(p.InstalledLocation);
foreach (var app in u.Apps)
{
if (AllAppsSettings.Instance.DisabledProgramSources.All(x => x.UniqueIdentifier != app.UniqueIdentifier))
{
appsBag.Add(app);
}
}
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
}
});
return appsBag.ToArray();
} }
private static IEnumerable<IPackage> CurrentUserPackages() private static IEnumerable<IPackage> CurrentUserPackages()

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Xml; using System.Xml;
using ManagedCommon; using ManagedCommon;
@@ -13,7 +14,9 @@ using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils; using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native; using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Storage.Packaging.Appx;
using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion; using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion;
using Theme = Microsoft.CmdPal.Ext.Apps.Utils.Theme; using Theme = Microsoft.CmdPal.Ext.Apps.Utils.Theme;
@@ -97,27 +100,27 @@ public class UWPApplication : IProgram
return commands; return commands;
} }
public UWPApplication(IAppxManifestApplication manifestApp, UWP package) internal unsafe UWPApplication(IAppxManifestApplication* manifestApp, UWP package)
{ {
ArgumentNullException.ThrowIfNull(manifestApp); ArgumentNullException.ThrowIfNull(manifestApp);
var hr = manifestApp.GetAppUserModelId(out var tmpUserModelId); var hr = manifestApp->GetAppUserModelId(out var tmpUserModelIdPtr);
UserModelId = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUserModelId); UserModelId = ComFreeHelper.GetStringAndFree(hr, tmpUserModelIdPtr);
hr = manifestApp.GetAppUserModelId(out var tmpUniqueIdentifier); manifestApp->GetAppUserModelId(out var tmpUniqueIdentifierPtr);
UniqueIdentifier = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUniqueIdentifier); UniqueIdentifier = ComFreeHelper.GetStringAndFree(hr, tmpUniqueIdentifierPtr);
hr = manifestApp.GetStringValue("DisplayName", out var tmpDisplayName); manifestApp->GetStringValue("DisplayName", out var tmpDisplayNamePtr);
DisplayName = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDisplayName); DisplayName = ComFreeHelper.GetStringAndFree(hr, tmpDisplayNamePtr);
hr = manifestApp.GetStringValue("Description", out var tmpDescription); manifestApp->GetStringValue("Description", out var tmpDescriptionPtr);
Description = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDescription); Description = ComFreeHelper.GetStringAndFree(hr, tmpDescriptionPtr);
hr = manifestApp.GetStringValue("BackgroundColor", out var tmpBackgroundColor); manifestApp->GetStringValue("BackgroundColor", out var tmpBackgroundColorPtr);
BackgroundColor = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpBackgroundColor); BackgroundColor = ComFreeHelper.GetStringAndFree(hr, tmpBackgroundColorPtr);
hr = manifestApp.GetStringValue("EntryPoint", out var tmpEntryPoint); manifestApp->GetStringValue("EntryPoint", out var tmpEntryPointPtr);
EntryPoint = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpEntryPoint); EntryPoint = ComFreeHelper.GetStringAndFree(hr, tmpEntryPointPtr);
Package = package ?? throw new ArgumentNullException(nameof(package)); Package = package ?? throw new ArgumentNullException(nameof(package));
@@ -166,7 +169,7 @@ public class UWPApplication : IProgram
return false; return false;
} }
internal string ResourceFromPri(string packageFullName, string resourceReference) internal unsafe string ResourceFromPri(string packageFullName, string resourceReference)
{ {
const string prefix = "ms-resource:"; const string prefix = "ms-resource:";
@@ -200,30 +203,8 @@ public class UWPApplication : IProgram
parsedFallback = prefix + "///" + key; parsedFallback = prefix + "///" + key;
} }
var outBuffer = new StringBuilder(128); if (string.IsNullOrEmpty(parsedFallback))
var source = $"@{{{packageFullName}? {parsed}}}";
var capacity = (uint)outBuffer.Capacity;
var hResult = SHLoadIndirectString(source, outBuffer, capacity, IntPtr.Zero);
if (hResult != HRESULT.S_OK)
{ {
if (!string.IsNullOrEmpty(parsedFallback))
{
var sourceFallback = $"@{{{packageFullName}? {parsedFallback}}}";
hResult = SHLoadIndirectString(sourceFallback, outBuffer, capacity, IntPtr.Zero);
if (hResult == HRESULT.S_OK)
{
var loaded = outBuffer.ToString();
if (!string.IsNullOrEmpty(loaded))
{
return loaded;
}
else
{
return string.Empty;
}
}
}
// https://github.com/Wox-launcher/Wox/issues/964 // https://github.com/Wox-launcher/Wox/issues/964
// known hresult 2147942522: // known hresult 2147942522:
// 'Microsoft Corporation' violates pattern constraint of '\bms-resource:.{1,256}'. // 'Microsoft Corporation' violates pattern constraint of '\bms-resource:.{1,256}'.
@@ -232,17 +213,40 @@ public class UWPApplication : IProgram
// Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription // Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription
return string.Empty; return string.Empty;
} }
else
var capacity = 1024U;
PWSTR outBuffer = new PWSTR((char*)(void*)Marshal.AllocHGlobal((int)capacity * sizeof(char)));
var source = $"@{{{packageFullName}? {parsed}}}";
void* reserved = null;
try
{ {
PInvoke.SHLoadIndirectString(source, outBuffer, capacity, ref reserved).ThrowOnFailure();
var loaded = outBuffer.ToString(); var loaded = outBuffer.ToString();
if (!string.IsNullOrEmpty(loaded)) return string.IsNullOrEmpty(loaded) ? string.Empty : loaded;
}
catch (Exception)
{
try
{ {
return loaded; var sourceFallback = $"@{{{packageFullName}?{parsedFallback}}}";
PInvoke.SHLoadIndirectString(sourceFallback, outBuffer, capacity, ref reserved).ThrowOnFailure();
var loaded = outBuffer.ToString();
return string.IsNullOrEmpty(loaded) ? string.Empty : loaded;
} }
else catch (Exception)
{ {
// ProgramLogger.Exception($"Unable to load resource {resourceReference} from {packageFullName}", new InvalidOperationException(), GetType(), packageFullName);
return string.Empty; return string.Empty;
} }
finally
{
}
}
finally
{
Marshal.FreeHGlobal((IntPtr)outBuffer.Value);
} }
} }
else else
@@ -258,13 +262,12 @@ public class UWPApplication : IProgram
{ PackageVersion.Windows8, "SmallLogo" }, { PackageVersion.Windows8, "SmallLogo" },
}; };
internal string LogoUriFromManifest(IAppxManifestApplication app) internal unsafe string LogoUriFromManifest(IAppxManifestApplication* app)
{ {
if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key)) if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key))
{ {
var hr = app.GetStringValue(key, out var logoUriFromApp); var hr = app->GetStringValue(key, out var logoUriFromAppPtr);
_ = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, logoUriFromApp); return ComFreeHelper.GetStringAndFree(hr, logoUriFromAppPtr);
return logoUriFromApp;
} }
else else
{ {
@@ -349,7 +352,7 @@ public class UWPApplication : IProgram
var prefix = path.Substring(0, end); var prefix = path.Substring(0, end);
var paths = new List<string> { }; var paths = new List<string> { };
const int appIconSize = 36; const int appIconSize = 36;
var targetSizes = new List<int> { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 }.AsParallel(); var targetSizes = new List<int> { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 };
var pathFactorPairs = new Dictionary<string, int>(); var pathFactorPairs = new Dictionary<string, int>();
foreach (var factor in targetSizes) foreach (var factor in targetSizes)

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.Design; using System.ComponentModel.Design;
using System.Diagnostics; using System.Diagnostics;
@@ -841,18 +842,69 @@ public class Win32Program : IProgram
var disabledProgramsList = settings.DisabledProgramSources; var disabledProgramsList = settings.DisabledProgramSources;
// Get all paths but exclude all normal .Executables // Get all paths but exclude all normal .Executables
paths.UnionWith(sources var pathBag = new ConcurrentBag<string>();
.AsParallel()
.SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
.Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath))
.Where(path => !ExecutableApplicationExtensions.Contains(Extension(path))));
runCommandPaths.UnionWith(runCommandSources
.AsParallel()
.SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
.Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath)));
var programs = paths.AsParallel().Select(source => GetProgramFromPath(source)); Parallel.ForEach(sources, source =>
var runCommandPrograms = runCommandPaths.AsParallel().Select(source => GetRunCommandProgramFromPath(source)); {
if (!source.IsEnabled)
{
return;
}
foreach (var path in source.GetPaths())
{
if (disabledProgramsList.All(x => x.UniqueIdentifier != path) &&
!ExecutableApplicationExtensions.Contains(Extension(path)))
{
pathBag.Add(path);
}
}
});
paths.UnionWith(pathBag);
var runCommandPathBag = new ConcurrentBag<string>();
Parallel.ForEach(runCommandSources, source =>
{
if (!source.IsEnabled)
{
return;
}
foreach (var path in source.GetPaths())
{
if (disabledProgramsList.All(x => x.UniqueIdentifier != path))
{
runCommandPathBag.Add(path);
}
}
});
runCommandPaths.UnionWith(runCommandPathBag);
var programsList = new ConcurrentBag<Win32Program>();
Parallel.ForEach(paths, source =>
{
var program = GetProgramFromPath(source);
if (program != null)
{
programsList.Add(program);
}
});
var runCommandProgramsList = new ConcurrentBag<Win32Program>();
Parallel.ForEach(runCommandPaths, source =>
{
var program = GetRunCommandProgramFromPath(source);
if (program != null)
{
runCommandProgramsList.Add(program);
}
});
var programs = programsList.ToList();
var runCommandPrograms = runCommandProgramsList.ToList();
return DeduplicatePrograms(programs.Concat(runCommandPrograms).Where(program => program?.Valid == true)); return DeduplicatePrograms(programs.Concat(runCommandPrograms).Where(program => program?.Valid == true));
} }

View File

@@ -69,11 +69,6 @@ public class ListRepository<T> : IRepository<T>, IEnumerable<T>
} }
} }
public ParallelQuery<T> AsParallel()
{
return _items.Values.AsParallel();
}
public bool Contains(T item) public bool Contains(T item)
{ {
if (item is not null) if (item is not null)

View File

@@ -6,8 +6,8 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.IO.Abstractions;
using System.Threading.Tasks; using System.Threading.Tasks;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Programs;
@@ -15,11 +15,9 @@ using Win32Program = Microsoft.CmdPal.Ext.Apps.Programs.Win32Program;
namespace Microsoft.CmdPal.Ext.Apps.Storage; namespace Microsoft.CmdPal.Ext.Apps.Storage;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
internal sealed partial class Win32ProgramRepository : ListRepository<Programs.Win32Program>, IProgramRepository internal sealed partial class Win32ProgramRepository : ListRepository<Programs.Win32Program>, IProgramRepository
{ {
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;
private const string LnkExtension = ".lnk"; private const string LnkExtension = ".lnk";
private const string UrlExtension = ".url"; private const string UrlExtension = ".url";

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
public static class ComFreeHelper
{
internal static unsafe string GetStringAndFree(HRESULT hr, PWSTR ptr)
{
hr.ThrowOnFailure();
try
{
return ptr.ToString();
}
finally
{
PInvoke.CoTaskMemFree(ptr);
}
}
public static unsafe void ComObjectRelease<T>(T* comPtr)
where T : unmanaged
{
if (comPtr != null)
{
((IUnknown*)comPtr)->Release();
}
}
}

View File

@@ -1,157 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
[SuppressMessage("Interoperability", "CA1401:P/Invokes should not be visible", Justification = "We want plugins to share this NativeMethods class, instead of each one creating its own.")]
public sealed class Native
{
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
public static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, nint ppvReserved);
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string path, nint pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
public static extern HRESULT SHCreateStreamOnFileEx(string fileName, STGM grfMode, uint attributes, bool create, System.Runtime.InteropServices.ComTypes.IStream reserved, out System.Runtime.InteropServices.ComTypes.IStream stream);
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
public static extern HRESULT SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf, nint ppvReserved);
public enum HRESULT : uint
{
/// <summary>
/// Operation successful.
/// </summary>
S_OK = 0x00000000,
/// <summary>
/// Operation successful. (negative condition/no operation)
/// </summary>
S_FALSE = 0x00000001,
/// <summary>
/// Not implemented.
/// </summary>
E_NOTIMPL = 0x80004001,
/// <summary>
/// No such interface supported.
/// </summary>
E_NOINTERFACE = 0x80004002,
/// <summary>
/// Pointer that is not valid.
/// </summary>
E_POINTER = 0x80004003,
/// <summary>
/// Operation aborted.
/// </summary>
E_ABORT = 0x80004004,
/// <summary>
/// Unspecified failure.
/// </summary>
E_FAIL = 0x80004005,
/// <summary>
/// Unexpected failure.
/// </summary>
E_UNEXPECTED = 0x8000FFFF,
/// <summary>
/// General access denied error.
/// </summary>
E_ACCESSDENIED = 0x80070005,
/// <summary>
/// Handle that is not valid.
/// </summary>
E_HANDLE = 0x80070006,
/// <summary>
/// Failed to allocate necessary memory.
/// </summary>
E_OUTOFMEMORY = 0x8007000E,
/// <summary>
/// One or more arguments are not valid.
/// </summary>
E_INVALIDARG = 0x80070057,
/// <summary>
/// The operation was canceled by the user. (Error source 7 means Win32.)
/// </summary>
/// <SeeAlso href="https://learn.microsoft.com/windows/win32/debug/system-error-codes--1000-1299-"/>
/// <SeeAlso href="https://en.wikipedia.org/wiki/HRESULT"/>
E_CANCELLED = 0x800704C7,
}
public static class ShellItemTypeConstants
{
/// <summary>
/// Guid for type IShellItem.
/// </summary>
public static readonly Guid ShellItemGuid = new("43826d1e-e718-42ee-bc55-a1e261c37bfe");
/// <summary>
/// Guid for type IShellItem2.
/// </summary>
public static readonly Guid ShellItem2Guid = new("7E9FB0D3-919F-4307-AB2E-9B1860310C93");
}
/// <summary>
/// The following are ShellItem DisplayName types.
/// </summary>
[Flags]
public enum SIGDN : uint
{
NORMALDISPLAY = 0,
PARENTRELATIVEPARSING = 0x80018001,
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
DESKTOPABSOLUTEPARSING = 0x80028000,
PARENTRELATIVEEDITING = 0x80031001,
DESKTOPABSOLUTEEDITING = 0x8004c000,
FILESYSPATH = 0x80058000,
URL = 0x80068000,
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
public interface IShellItem
{
void BindToHandler(
nint pbc,
[MarshalAs(UnmanagedType.LPStruct)] Guid bhid,
[MarshalAs(UnmanagedType.LPStruct)] Guid riid,
out nint ppv);
void GetParent(out IShellItem ppsi);
void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
}
/// <summary>
/// <see href="https://learn.microsoft.com/windows/win32/stg/stgm-constants">see all STGM values</see>
/// </summary>
[Flags]
public enum STGM : long
{
READ = 0x00000000L,
WRITE = 0x00000001L,
READWRITE = 0x00000002L,
CREATE = 0x00001000L,
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
public partial class SafeComHandle : SafeHandle
{
public SafeComHandle()
: base(IntPtr.Zero, ownsHandle: true)
{
}
public SafeComHandle(IntPtr handle)
: base(IntPtr.Zero, ownsHandle: true)
{
SetHandle(handle);
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
var count = Marshal.Release(handle);
return true;
}
}

View File

@@ -3,124 +3,17 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text; using System.Text;
using ManagedCommon; using ManagedCommon;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
namespace Microsoft.CmdPal.Ext.Apps.Utils; namespace Microsoft.CmdPal.Ext.Apps.Utils;
public class ShellLinkHelper : IShellLinkHelper public class ShellLinkHelper : IShellLinkHelper
{ {
[Flags]
private enum SLGP_FLAGS
{
SLGP_SHORTPATH = 0x1,
SLGP_UNCPRIORITY = 0x2,
SLGP_RAWPATH = 0x4,
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching COM")]
private struct WIN32_FIND_DATAW
{
public uint dwFileAttributes;
public long ftCreationTime;
public long ftLastAccessTime;
public long ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
[Flags]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
public enum SLR_FLAGS
{
SLR_NO_UI = 0x1,
SLR_ANY_MATCH = 0x2,
SLR_UPDATE = 0x4,
SLR_NOUPDATE = 0x8,
SLR_NOSEARCH = 0x10,
SLR_NOTRACK = 0x20,
SLR_NOLINKINFO = 0x40,
SLR_INVOKE_MSI = 0x80,
}
// Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW
// The IShellLink interface allows Shell links to be created, modified, and resolved
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
/// <summary>Retrieves the path and file name of a Shell link object</summary>
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
/// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
void GetIDList(out nint ppidl);
/// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
void SetIDList(nint pidl);
/// <summary>Retrieves the description string for a Shell link object</summary>
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
/// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
/// <summary>Retrieves the name of the working directory for a Shell link object</summary>
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
/// <summary>Sets the name of the working directory for a Shell link object</summary>
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
/// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
/// <summary>Sets the command-line arguments for a Shell link object</summary>
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
/// <summary>Retrieves the hot key for a Shell link object</summary>
void GetHotkey(out short pwHotkey);
/// <summary>Sets a hot key for a Shell link object</summary>
void SetHotkey(short wHotkey);
/// <summary>Retrieves the show command for a Shell link object</summary>
void GetShowCmd(out int piShowCmd);
/// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
void SetShowCmd(int iShowCmd);
/// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
/// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
/// <summary>Sets the relative path to the Shell link object</summary>
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
/// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
void Resolve(ref nint hwnd, SLR_FLAGS fFlags);
/// <summary>Sets the path and file name of a Shell link object</summary>
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
private class ShellLink
{
}
// Contains the description of the app // Contains the description of the app
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
@@ -130,60 +23,63 @@ public class ShellLinkHelper : IShellLinkHelper
public bool HasArguments { get; set; } public bool HasArguments { get; set; }
// Retrieve the target path using Shell Link // Retrieve the target path using Shell Link
public string RetrieveTargetPath(string path) public unsafe string RetrieveTargetPath(string path)
{ {
var link = new ShellLink(); var target = string.Empty;
const int STGM_READ = 0;
try
{
((IPersistFile)link).Load(path, STGM_READ);
}
catch (System.IO.FileNotFoundException ex)
{
Logger.LogError(ex.Message);
return string.Empty;
}
var hwnd = default(nint);
((IShellLinkW)link).Resolve(ref hwnd, 0);
const int MAX_PATH = 260; const int MAX_PATH = 260;
var buffer = new StringBuilder(MAX_PATH); IShellLinkW* link = null;
var data = default(WIN32_FIND_DATAW); PInvoke.CoCreateInstance(typeof(ShellLink).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out link).ThrowOnFailure();
((IShellLinkW)link).GetPath(buffer, buffer.Capacity, ref data, SLGP_FLAGS.SLGP_SHORTPATH); using var linkHandle = new SafeComHandle((IntPtr)link);
var target = buffer.ToString();
const int STGMREAD = 0;
IPersistFile* persistFile = null;
Guid iid = typeof(IPersistFile).GUID;
((IUnknown*)link)->QueryInterface(&iid, (void**)&persistFile);
if (persistFile != null)
{
using var persistFileHandle = new SafeComHandle((IntPtr)persistFile);
try
{
persistFile->Load(path, STGMREAD);
}
catch (System.IO.FileNotFoundException)
{
// Log.Exception($"Failed to load {path}, {e.Message}", e, GetType());
return string.Empty;
}
}
var hwnd = HWND.Null;
const uint SLR_NO_UI = 0x1;
link->Resolve(hwnd, SLR_NO_UI);
var buffer = stackalloc char[MAX_PATH];
var hr = link->GetPath((PWSTR)buffer, MAX_PATH, null, 0x1);
target = hr.Succeeded ? new string(buffer) : string.Empty;
// To set the app description // To set the app description
if (!string.IsNullOrEmpty(target)) if (!string.IsNullOrEmpty(target))
{ {
buffer = new StringBuilder(MAX_PATH); var descBuffer = stackalloc char[MAX_PATH];
try var desHr = link->GetDescription(descBuffer, MAX_PATH);
{ Description = desHr.Succeeded ? new string(descBuffer) : string.Empty;
((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
Description = buffer.ToString();
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
Description = string.Empty;
}
var argumentBuffer = new StringBuilder(MAX_PATH); var argsBuffer = stackalloc char[MAX_PATH];
((IShellLinkW)link).GetArguments(argumentBuffer, argumentBuffer.Capacity); var argHr = link->GetArguments(argsBuffer, MAX_PATH);
Arguments = argumentBuffer.ToString();
Arguments = argHr.Succeeded ? new string(argsBuffer) : string.Empty;
// Set variable to true if the program takes in any arguments // Set variable to true if the program takes in any arguments
if (argumentBuffer.Length != 0) if (Arguments.Length != 0)
{ {
HasArguments = true; HasArguments = true;
} }
} }
// To release unmanaged memory
Marshal.ReleaseComObject(link);
return target; return target;
} }
} }

View File

@@ -4,7 +4,8 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO; using System.IO;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native; using Windows.Win32;
using Windows.Win32.UI.Shell;
namespace Microsoft.CmdPal.Ext.Apps.Utils; namespace Microsoft.CmdPal.Ext.Apps.Utils;
@@ -23,7 +24,7 @@ public class ShellLocalization
/// </summary> /// </summary>
/// <param name="path">Path to the shell item (e. g. shortcut 'File Explorer.lnk').</param> /// <param name="path">Path to the shell item (e. g. shortcut 'File Explorer.lnk').</param>
/// <returns>The localized name as string or <see cref="string.Empty"/>.</returns> /// <returns>The localized name as string or <see cref="string.Empty"/>.</returns>
public string GetLocalizedName(string path) public unsafe string GetLocalizedName(string path)
{ {
var lowerInvariantPath = path.ToLowerInvariant(); var lowerInvariantPath = path.ToLowerInvariant();
@@ -33,18 +34,29 @@ public class ShellLocalization
return value; return value;
} }
var shellItemType = ShellItemTypeConstants.ShellItemGuid; void* shellItemPtrVoid = null;
var retCode = SHCreateItemFromParsingName(path, nint.Zero, ref shellItemType, out var shellItem); try
if (retCode != 0) {
var retCode = PInvoke.SHCreateItemFromParsingName(path, null, typeof(IShellItem).GUID, out shellItemPtrVoid).ThrowOnFailure();
using var shellItemHandle = new SafeComHandle((IntPtr)shellItemPtrVoid);
IShellItem* shellItemPtr = (IShellItem*)shellItemPtrVoid;
var hr = shellItemPtr->GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY, out var filenamePtr);
var filename = ComFreeHelper.GetStringAndFree(hr, filenamePtr);
if (filename == null)
{
return string.Empty;
}
_ = _localizationCache.TryAdd(lowerInvariantPath, filename);
return filename;
}
catch (Exception)
{ {
return string.Empty; return string.Empty;
} }
shellItem.GetDisplayName(SIGDN.NORMALDISPLAY, out var filename);
_ = _localizationCache.TryAdd(lowerInvariantPath, filename);
return filename;
} }
/// <summary> /// <summary>