diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..de782d5a4807 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# Repository Instructions + +This repository contains the Files Windows desktop app, a WinUI-based file manager for Windows. The codebase includes the main app, reusable controls, storage layers, Win32/CsWin32 interop, packaging support, background/server components, and UI/interaction tests. + +## Codebase Overview + +```text +/src +├── Files.App // Main WinUI desktop app: startup, DI, views, view models, actions, services, dialogs, styles, assets, strings, and app helpers. +├── Files.App.CsWin32 // CsWin32 source-generated Win32 interop. Add APIs to NativeMethods.txt here. +├── Files.App.Controls // Reusable WinUI controls shared by the app. +├── Files.App.Storage // App-facing storage abstractions and storage implementation pieces. +├── Files.App.BackgroundTasks // Background task project. +├── Files.App.Server // App service/server behavior. +├── Files.App.Launcher // Launch-related entry points. +├── Files.App.OpenDialog // File open dialog-specific app project/folder. +├── Files.App.SaveDialog // File save dialog-specific app project/folder. +├── Files.App (Package) // Packaging-related app project assets. +├── Files.Core.Storage // Lower-level storage primitives that should not depend on the main WinUI app. +├── Files.Core.SourceGenerator // Roslyn source generators used by the solution. +└── Files.Shared // Shared models, helpers, and code used by multiple projects. +``` + +```text +/tests +├── Files.App.UITests // UI test assets and views. +├── Files.InteractionTests // Interaction tests used by CI automation. +└── Files.App.UnitTests // Placeholder/stale in this checkout; verify project files before assuming unit tests exist here. +``` + +## When Dealing With Interop Code + +When the user asks to convert marshaled interop code into unmarshaled interop, or asks to remove trim-unsafe manual P/Invoke definitions, see [docs/interop-unmarshaled-conversion.md](docs/interop-unmarshaled-conversion.md). + +Prefer adding APIs and related generated types to `src/Files.App.CsWin32/NativeMethods.txt`, then update the callees to use CsWin32-generated `Windows.Win32.PInvoke` APIs directly. Do not leave manual `DllImport` definitions in place or replace them with local `LibraryImport` declarations when CsWin32 can generate the API. + +## When Building the App + +Use `.github/workflows/ci.yml` as the source of truth for building. +For normal local verification, build with MSBuild restore and explicit configuration/platform; packaging is not required. + +```powershell +msbuild -restore src\Files.App\Files.App.csproj /p:Configuration=Debug /p:Platform=x64 +``` + +## When Packaging the App + +Use `.github/workflows/ci.yml` as the source of truth for packaging. Adjust `Configuration`, `Platform`, and `AppxBundlePlatforms` as needed. diff --git a/docs/interop-unmarshaled-conversion.md b/docs/interop-unmarshaled-conversion.md new file mode 100644 index 000000000000..9c262b83aee9 --- /dev/null +++ b/docs/interop-unmarshaled-conversion.md @@ -0,0 +1,128 @@ +# Converting Marshaled Interop to CsWin32 Unmarshaled Interop + +This repository is moving trim-unsafe manual interop toward source-generated CsWin32 interop. +When a user asks to convert marshaled interop code into unmarshaled interop, prefer updating `NativeMethods.txt` and the call sites instead of preserving manual `DllImport`/`LibraryImport` declarations or wrapping them with more local P/Invoke code. + +Removing Vanara interop usage is also part of this direction since it internally has trim-unsafe interop code. + +## Goal + +Remove interop code that relies on runtime marshalling, especially declarations using `DllImport`, `ComImport` and the Vanara package. + +The target shape is: + +- Add the native API, COM interface, enum, or struct name to `src/Files.App.CsWin32/NativeMethods.txt`. +- Use `Windows.Win32.PInvoke` and generated CsWin32 types at the call site. +- Delete the manual declarations and Vanara references. +- Keep `Win32PInvoke` only for definitions that are not yet converted or cannot be generated by CsWin32. + +## Workflow + +1. Locate manual interop: + + ```powershell + git grep -n "DllImport\|MarshalAs\|StringBuilder" -- src + git grep -n "Win32PInvoke\." -- src/Files.App + git grep -n "using Vanara\|Vanara\.PInvoke\|Kernel32\.\|Shell32\.\|User32\." -- src/Files.App + ``` + +2. Add the API and related generated types to `src/Files.App.CsWin32/NativeMethods.txt`. + + Include both the function and any dependent structs, enums, or COM interfaces that the call site needs. For example: + + ```text + RmStartSession + RmRegisterResources + RmGetList + RmEndSession + RM_PROCESS_INFO + SHBrowseForFolder + BROWSEINFOW + SHGetPathFromIDList + SHCreateItemFromParsingName + SHCreateStreamOnFileEx + ``` + +3. Build the CsWin32 project or the app project to refresh generated signatures: + + ```powershell + dotnet build src/Files.App.CsWin32/Files.App.CsWin32.csproj -c Debug -p:Platform=x64 + ``` + +4. Update callers to use generated APIs directly. + + Prefer generated safe overloads when they exist, such as `Span`, `SafeHandle`, `ComPtr`, generated enums, and generated structs. Use unsafe raw overloads only when the generated API naturally exposes pointers or when COM pointer identity is required. + +5. Remove the manual definition only after all callers have moved. + + Use targeted checks: + + ```powershell + git grep -n "Win32PInvoke\.RmStartSession\|Win32PInvoke\.SHBrowseForFolder" -- src/Files.App + git grep -n "RM_PROCESS_INFO\|BROWSEINFO" -- src/Files.App + ``` + +6. Build and confirm there are no C# errors. + + In this repo, WinUI XAML compiler failures may appear independently of interop changes. Separate `error CS*` failures from `MSB3073` XAML compiler failures when reporting verification. + +## Manual Definition Conversion Notes + +- `StringBuilder` output buffers should usually become `Span` or fixed `char*` buffers. +- `IntPtr` handles should become `SafeFileHandle`, `SafeHandle`, `HANDLE`, or generated handle structs where practical. +- Some APIs such as `CoCreateInstance` and `SHCreateItemFromParsingName` often require unsafe pointer overloads: + + ```csharp + void* raw; + Guid iid = SomeInterfaceIid; + HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, &iid, &raw); + IntPtr instance = (IntPtr)raw; + ``` + +- Shell APIs that return PIDLs or allocated strings still require explicit lifetime management, but the API declaration should come from CsWin32: + + ```csharp + var pidl = PInvoke.SHBrowseForFolder(ref browseInfo); + Marshal.FreeCoTaskMem((nint)pidl); + ``` + +- Restart Manager APIs can use generated `RM_PROCESS_INFO` and `Span` session keys instead of local struct definitions. +- Do not keep a local `LibraryImport` copy when the API can be represented in `NativeMethods.txt`. The requested direction is to update callees to CsWin32-generated interop. + +## Vanara Conversion Notes + +Treat Vanara removal the same way as manual P/Invoke removal when Vanara is only wrapping a native API. Add the API to `NativeMethods.txt`, inspect the generated CsWin32 signature, then update the call site to generated types. + +Example conversion: + +```csharp +// Before +var lib = Kernel32.LoadLibrary(file); +StringBuilder result = new(2048); +_ = User32.LoadString(lib, number, result, result.Capacity); +Kernel32.FreeLibrary(lib); +return result.ToString(); + +// After +using var lib = PInvoke.LoadLibrary(file); +Span result = stackalloc char[2048]; +int length = PInvoke.LoadString(lib, (uint)number, result, result.Length); +return result[..length].ToString(); +``` + +Useful heuristics: + +- Start with simple `Kernel32.*`, `User32.*`, or `Shell32.*` calls that map directly to one Win32 function. +- Remove `using Vanara.PInvoke` only when no remaining types in the file depend on it. +- Prefer generated `Span` overloads over `StringBuilder` buffers when available. +- Prefer generated handle types and `SafeHandle` overloads where CsWin32 provides them. +- Watch for CsWin32 convenience overloads. Some APIs generate safer shapes than the underlying Win32 signature; for example, `LoadLibrary` returns a disposable `FreeLibrarySafeHandle`, so the call site can use `using var` instead of a separate `FreeLibrary` call. + +## Verification Checklist + +- `NativeMethods.txt` contains every newly used API/type. +- No caller remains for the deleted manual method or struct. +- The generated CsWin32 types are used at call sites. +- `dotnet build src/Files.App.CsWin32/Files.App.CsWin32.csproj -c Debug -p:Platform=x64` succeeds. +- `dotnet build src/Files.App/Files.App.csproj -c Debug -p:Platform=x64` has no new `error CS*` errors. +- Any remaining build failure is called out explicitly, especially XAML compiler failures unrelated to interop. diff --git a/src/Files.App.CsWin32/Extras.cs b/src/Files.App.CsWin32/Extras.cs index 99fd57262dba..67b62cf0e28d 100644 --- a/src/Files.App.CsWin32/Extras.cs +++ b/src/Files.App.CsWin32/Extras.cs @@ -22,11 +22,11 @@ namespace UI.WindowsAndMessaging public static partial class PInvoke { - [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] - static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + [LibraryImport("User32", EntryPoint = "SetWindowLongW")] + private static partial int _SetWindowLong(nint hWnd, int nIndex, int dwNewLong); - [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] - static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + [LibraryImport("User32", EntryPoint = "SetWindowLongPtrW")] + private static partial nint _SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong); // NOTE: // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index e49c2be59db0..228d31d080d3 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -50,7 +50,37 @@ WNetAddConnection3 CREDENTIALW CredWrite WNetConnectionDialog1 +WNetGetConnection CONNECTDLGSTRUCTW +RmRegisterResources +RmStartSession +RmEndSession +RmGetList +RM_UNIQUE_PROCESS +RM_PROCESS_INFO +CreateEvent +SetEvent +CancelIoEx +WaitForSingleObjectEx +ReadDirectoryChangesW +CreateFileFromAppW +GetFileAttributesExFromApp +SetFileAttributesFromApp +ReadFile +WriteFile +WriteFileEx +GetFileTime +SetFileTime +GetFileInformationByHandleEx +FILE_ID_BOTH_DIR_INFO +FindFirstStreamW +FindNextStreamW +WIN32_FIND_STREAM_DATA +RegisterApplicationRestart +BROWSEINFOW +SHBrowseForFolder +SHGetPathFromIDList +SHCreateStreamOnFileEx DwmSetWindowAttribute WIN32_ERROR CoCreateInstance @@ -116,9 +146,13 @@ WindowsDeleteString IPreviewHandler AssocQueryString GetModuleHandle +LoadLibrary +FreeLibrary +LoadString SHEmptyRecycleBin SHFileOperation SHGetFolderPath +SHGetKnownFolderPath SHGFP_TYPE SHGetKnownFolderItem SHQUERYRBINFO diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index 1aa976e2c503..9875d3f9e04e 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -7,6 +7,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.Windows.AppLifecycle; +using Windows.Win32; using Windows.ApplicationModel; using Windows.ApplicationModel.DataTransfer; using Windows.Storage; @@ -227,9 +228,8 @@ private async void Window_Closed(object sender, WindowEventArgs args) var results = items.Select(x => x.ItemPath).ToList(); System.IO.File.WriteAllLines(OutputPath, results); - IntPtr eventHandle = Win32PInvoke.CreateEvent(IntPtr.Zero, false, false, "FILEDIALOG"); - Win32PInvoke.SetEvent(eventHandle); - Win32PInvoke.CloseHandle(eventHandle); + using var eventHandle = PInvoke.CreateEvent(null, false, false, "FILEDIALOG"); + PInvoke.SetEvent(eventHandle); } // Continue running the app on the background diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Process.cs b/src/Files.App/Helpers/Win32/Win32Helper.Process.cs index 78f0e8d5a65e..ddffe517f545 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Process.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Process.cs @@ -1,6 +1,10 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.RestartManager; + namespace Files.App.Helpers { /// @@ -68,36 +72,37 @@ public static async Task InvokeWin32ComponentsAsync(IEnumerable ap /// public static List WhoIsLocking(string[] resources) { - string key = Guid.NewGuid().ToString(); + Span key = stackalloc char[64]; + Guid.NewGuid().TryFormat(key, out var charsWritten); + key = key[..charsWritten]; List processes = []; - int res = Win32PInvoke.RmStartSession(out uint handle, 0, key); - if (res != 0) throw new Exception("Could not begin restart session. Unable to determine file locker."); + WIN32_ERROR res = PInvoke.RmStartSession(out uint handle, key); + if (res != WIN32_ERROR.NO_ERROR) throw new Exception("Could not begin restart session. Unable to determine file locker."); try { - const int ERROR_MORE_DATA = 234; uint pnProcInfo = 0; - uint lpdwRebootReasons = Win32PInvoke.RmRebootReasonNone; + uint lpdwRebootReasons; - res = Win32PInvoke.RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null); + res = PInvoke.RmRegisterResources(handle, resources, [], []); - if (res != 0) throw new Exception("Could not register resource."); + if (res != WIN32_ERROR.NO_ERROR) throw new Exception("Could not register resource."); // Note: // There's a race condition here -- the first call to RmGetList() returns the total number of process. // However, when we call RmGetList() again to get the actual processes this number may have increased. - res = Win32PInvoke.RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons); + res = PInvoke.RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, [], out lpdwRebootReasons); - if (res == ERROR_MORE_DATA) + if (res == WIN32_ERROR.ERROR_MORE_DATA) { // Create an array to store the process results - var processInfo = new Win32PInvoke.RM_PROCESS_INFO[pnProcInfoNeeded]; + var processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; pnProcInfo = pnProcInfoNeeded; // Get the list - res = Win32PInvoke.RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); - if (res == 0) + res = PInvoke.RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, out lpdwRebootReasons); + if (res == WIN32_ERROR.NO_ERROR) { processes = new List((int)pnProcInfo); @@ -107,7 +112,7 @@ public static List WhoIsLocking(string[] resources) { try { - processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId)); + processes.Add(Process.GetProcessById((int)processInfo[i].Process.dwProcessId)); } // catch the error -- in case the process is no longer running catch (ArgumentException) { } @@ -115,11 +120,11 @@ public static List WhoIsLocking(string[] resources) } else throw new Exception("Could not list processes locking resource."); } - else if (res != 0) throw new Exception("Could not list processes locking resource. Failed to get size of result."); + else if (res != WIN32_ERROR.NO_ERROR) throw new Exception("Could not list processes locking resource. Failed to get size of result."); } finally { - _ = Win32PInvoke.RmEndSession(handle); + _ = PInvoke.RmEndSession(handle); } return processes; diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs index 0368aba3a2a0..ef0562e846f9 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs @@ -5,6 +5,9 @@ using System.Runtime.InteropServices; using Vanara.PInvoke; using Vanara.Windows.Shell; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; namespace Files.App.Helpers { @@ -78,12 +81,12 @@ public static partial class Win32Helper }, App.Logger); } - public static string GetFolderFromKnownFolderGUID(Guid guid) + public static unsafe string GetFolderFromKnownFolderGUID(Guid guid) { - nint pszPath; - Win32PInvoke.SHGetKnownFolderPath(guid, 0, nint.Zero, out pszPath); - string path = Marshal.PtrToStringUni(pszPath); - Marshal.FreeCoTaskMem(pszPath); + PWSTR pszPath; + PInvoke.SHGetKnownFolderPath(ref guid, (KNOWN_FOLDER_FLAG)0, null, out pszPath); + string path = pszPath.ToString(); + Marshal.FreeCoTaskMem((nint)pszPath.Value); return path; } diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs index d5b3f7672ac6..65f047c1b150 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs @@ -16,6 +16,8 @@ using Windows.System; using Windows.Win32; using Windows.Win32.Storage.FileSystem; +using Windows.Win32.System.LibraryLoader; +using Windows.Win32.UI.WindowsAndMessaging; using COMPRESSION_FORMAT = Windows.Win32.Storage.FileSystem.COMPRESSION_FORMAT; using HRESULT = Vanara.PInvoke.HRESULT; using HWND = Vanara.PInvoke.HWND; @@ -40,13 +42,12 @@ public static partial class Win32Helper public static string ExtractStringFromDLL(string file, int number) { - var lib = Kernel32.LoadLibrary(file); - StringBuilder result = new StringBuilder(2048); + using var lib = PInvoke.LoadLibrary(file); + Span result = stackalloc char[2048]; - _ = User32.LoadString(lib, number, result, result.Capacity); - Kernel32.FreeLibrary(lib); + int length = PInvoke.LoadString(lib, (uint)number, result, result.Length); - return result.ToString(); + return result[..length].ToString(); } public static string?[] CommandLineToArgs(string commandLine) diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Consts.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Consts.cs index d36433d37833..39508efa6046 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Consts.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Consts.cs @@ -8,10 +8,6 @@ public static partial class Win32PInvoke public static readonly Guid DataTransferManagerInteropIID = new(0xa5caee9b, 0x8708, 0x49d1, 0x8d, 0x36, 0x67, 0xd2, 0x5a, 0x8d, 0xa0, 0x0c); - public const int RmRebootReasonNone = 0; - public const int CCH_RM_MAX_APP_NAME = 255; - public const int CCH_RM_MAX_SVC_NAME = 63; - public const int FILE_NOTIFY_CHANGE_FILE_NAME = 1; public const int FILE_NOTIFY_CHANGE_DIR_NAME = 2; public const int FILE_NOTIFY_CHANGE_ATTRIBUTES = 4; diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Enums.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Enums.cs index c9d3acb14e80..b545d9be4cc8 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Enums.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Enums.cs @@ -5,17 +5,6 @@ namespace Files.App.Helpers { public static partial class Win32PInvoke { - public enum RM_APP_TYPE - { - RmUnknownApp = 0, - RmMainWindow = 1, - RmOtherWindow = 2, - RmService = 3, - RmExplorer = 4, - RmConsole = 5, - RmCritical = 1000 - } - public enum File_Attributes : uint { Readonly = 0x00000001, diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs index d429c25c6d7d..814a9c30eee4 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs @@ -23,62 +23,6 @@ public delegate void LPOVERLAPPED_COMPLETION_ROUTINE( ref NativeOverlapped lpOverlapped ); - [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] - public static extern int RmRegisterResources( - uint pSessionHandle, - uint nFiles, - string[] rgsFilenames, - uint nApplications, - [In] RM_UNIQUE_PROCESS[] rgApplications, - uint nServices, - string[] rgsServiceNames - ); - - [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] - public static extern int RmStartSession( - out uint pSessionHandle, - int dwSessionFlags, - string strSessionKey - ); - - [DllImport("rstrtmgr.dll")] - public static extern int RmEndSession( - uint pSessionHandle - ); - - [DllImport("rstrtmgr.dll")] - public static extern int RmGetList( - uint dwSessionHandle, - out uint pnProcInfoNeeded, - ref uint pnProcInfo, - [In, Out] RM_PROCESS_INFO[] rgAffectedApps, - ref uint lpdwRebootReasons - ); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr CreateEvent( - IntPtr lpEventAttributes, - bool bManualReset, - bool bInitialState, - string lpName - ); - - [DllImport("kernel32.dll")] - public static extern bool SetEvent( - IntPtr hEvent - ); - - [DllImport("shell32.dll")] - public static extern IntPtr SHBrowseForFolder( - ref BROWSEINFO lpbi - ); - - [DllImport("shell32.dll", CharSet = CharSet.Unicode)] - public static extern bool SHGetPathFromIDList( - IntPtr pidl, - [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszPath - ); - [DllImport("api-ms-win-core-handle-l1-1-0.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseHandle( @@ -286,47 +230,6 @@ int dwAdditionalFlags public static partial bool IsElevationRequired( [MarshalAs(UnmanagedType.LPWStr)] string pszPath); - [DllImport("shlwapi.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = true, CharSet = CharSet.Unicode)] - public static extern HRESULT SHCreateStreamOnFileEx( - string pszFile, - STGM grfMode, - uint dwAttributes, - uint fCreate, - IntPtr pstmTemplate, - out IntPtr ppstm - ); - - [DllImport("shell32.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = true, CharSet = CharSet.Unicode)] - public static extern HRESULT SHCreateItemFromParsingName( - string pszPath, - IntPtr pbc, - ref Guid riid, - out IntPtr ppv - ); - - [DllImport("ole32.dll", CallingConvention = CallingConvention.StdCall)] - public static extern HRESULT CoCreateInstance( - ref Guid rclsid, - IntPtr pUnkOuter, - ClassContext dwClsContext, - ref Guid riid, - out IntPtr ppv - ); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - public static extern uint RegisterApplicationRestart( - string pwzCommandLine, - int dwFlags - ); - - [DllImport("shell32.dll")] - public static extern int SHGetKnownFolderPath( - [MarshalAs(UnmanagedType.LPStruct)] Guid rfid, - uint dwFlags, - IntPtr hToken, - out IntPtr pszPath - ); - // cryptui.dll [DllImport("cryptui.dll", SetLastError = true, CharSet = CharSet.Auto)] public unsafe static extern bool CryptUIDlgViewSignerInfo(CRYPTUI_VIEWSIGNERINFO_STRUCT* pViewInfo); diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs index d1453d5cd285..fec7a25f37e4 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs @@ -10,44 +10,6 @@ namespace Files.App.Helpers { public static partial class Win32PInvoke { - [StructLayout(LayoutKind.Sequential)] - public struct RM_UNIQUE_PROCESS - { - public int dwProcessId; - public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct RM_PROCESS_INFO - { - public RM_UNIQUE_PROCESS Process; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] - public string strAppName; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] - public string strServiceShortName; - - public RM_APP_TYPE ApplicationType; - public uint AppStatus; - public uint TSSessionId; - [MarshalAs(UnmanagedType.Bool)] - public bool bRestartable; - } - - [StructLayout(LayoutKind.Sequential)] - public struct BROWSEINFO - { - public IntPtr hwndOwner; - public IntPtr pidlRoot; - public string pszDisplayName; - public string lpszTitle; - public uint ulFlags; - public IntPtr lpfn; - public int lParam; - public IntPtr iImage; - } - public unsafe struct OVERLAPPED { public IntPtr Internal; diff --git a/src/Files.App/Services/App/AppUpdateSideloadService.cs b/src/Files.App/Services/App/AppUpdateSideloadService.cs index b79cb44016f4..7ba8c80f0d3b 100644 --- a/src/Files.App/Services/App/AppUpdateSideloadService.cs +++ b/src/Files.App/Services/App/AppUpdateSideloadService.cs @@ -9,6 +9,8 @@ using Windows.ApplicationModel; using Windows.Management.Deployment; using Windows.Storage; +using Windows.Win32; +using Windows.Win32.System.Recovery; namespace Files.App.Services { @@ -227,7 +229,7 @@ private async Task ApplyPackageUpdateAsync() { PackageManager packageManager = new PackageManager(); - var restartStatus = Win32PInvoke.RegisterApplicationRestart(null, 0); + var restartStatus = PInvoke.RegisterApplicationRestart(null, (REGISTER_APPLICATION_RESTART_FLAGS)0); App.AppModel.ForceProcessTermination = true; Logger?.LogInformation($"Register for restart: {restartStatus}"); diff --git a/src/Files.App/Services/Storage/StorageNetworkService.cs b/src/Files.App/Services/Storage/StorageNetworkService.cs index bc45e2cb2b9f..e0dd6b2fae72 100644 --- a/src/Files.App/Services/Storage/StorageNetworkService.cs +++ b/src/Files.App/Services/Storage/StorageNetworkService.cs @@ -1,7 +1,6 @@ // Copyright (c) Files Community // Licensed under the MIT License. -using System.Runtime.InteropServices; using System.Text; using Vanara.PInvoke; using Vanara.Windows.Shell; @@ -221,17 +220,14 @@ public async Task AuthenticateNetworkShare(string path) { // Special handling for network drives // This part will change path from "y:\Download" to "\\192.168.0.1\nfs\Download" - [DllImport("mpr.dll", CharSet = CharSet.Auto)] - static extern int WNetGetConnection(string lpLocalName, StringBuilder lpRemoteName, ref int lpnLength); - - StringBuilder remoteName = new StringBuilder(300); - int length = remoteName.Capacity; + Span remoteName = stackalloc char[300]; + uint length = (uint)remoteName.Length; string lpLocalName = path.Substring(0, 2); - int ret = WNetGetConnection(lpLocalName, remoteName, ref length); + WIN32_ERROR ret = PInvoke.WNetGetConnection(lpLocalName, remoteName, ref length); - if (ret == 0) - path = path.Replace(lpLocalName, remoteName.ToString()); + if (ret == WIN32_ERROR.NO_ERROR) + path = path.Replace(lpLocalName, remoteName[..(int)length].TrimEnd('\0').ToString()); } diff --git a/src/Files.App/Utils/Shell/ItemStreamHelper.cs b/src/Files.App/Utils/Shell/ItemStreamHelper.cs index 114c071cc655..61b3c816b579 100644 --- a/src/Files.App/Utils/Shell/ItemStreamHelper.cs +++ b/src/Files.App/Utils/Shell/ItemStreamHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Runtime.InteropServices; +using Windows.Win32; using Windows.Win32.System.Com; namespace Files.App.Utils.Shell @@ -10,28 +11,31 @@ public static class ItemStreamHelper { static readonly Guid IShellItemIid = Guid.ParseExact("43826d1e-e718-42ee-bc55-a1e261c37bfe", "d"); - public static IntPtr IShellItemFromPath(string path) + public static unsafe IntPtr IShellItemFromPath(string path) { - IntPtr psi; + void* psi; Guid iid = IShellItemIid; - var hr = Win32PInvoke.SHCreateItemFromParsingName(path, IntPtr.Zero, ref iid, out psi); + var hr = PInvoke.SHCreateItemFromParsingName(path, null, ref iid, out psi); if ((int)hr < 0) return IntPtr.Zero; - return psi; + return (IntPtr)psi; } - public static IntPtr IStreamFromPath(string path) + public static unsafe IntPtr IStreamFromPath(string path) { - var hr = Win32PInvoke.SHCreateStreamOnFileEx( + IStream* pstm; + var hr = PInvoke.SHCreateStreamOnFileEx( path, - STGM.STGM_READ | STGM.STGM_FAILIFTHERE | STGM.STGM_SHARE_DENY_NONE, - 0, 0, - IntPtr.Zero, out var pstm); + (uint)(STGM.STGM_READ | STGM.STGM_FAILIFTHERE | STGM.STGM_SHARE_DENY_NONE), + 0, + false, + null, + &pstm); if ((int)hr < 0) return IntPtr.Zero; - return pstm; + return (IntPtr)pstm; } public static void ReleaseObject(IntPtr obj) diff --git a/src/Files.App/Utils/Shell/PreviewHandler.cs b/src/Files.App/Utils/Shell/PreviewHandler.cs index abb49b7d518a..f153dd76ed49 100644 --- a/src/Files.App/Utils/Shell/PreviewHandler.cs +++ b/src/Files.App/Utils/Shell/PreviewHandler.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using Windows.UI; +using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Com; @@ -158,9 +159,9 @@ public PreviewHandler(Guid clsid, nint frame) static readonly Guid IPreviewHandlerIid = Guid.ParseExact("8895b1c6-b41f-4c1c-a562-0d564250836f", "d"); - void SetupHandler(Guid clsid) + unsafe void SetupHandler(Guid clsid) { - IntPtr pph; + void* pphRaw; var iid = IPreviewHandlerIid; var cannotCreate = "Cannot create class " + clsid.ToString() + " as IPreviewHandler."; var cannotCast = "Cannot cast class " + clsid.ToString() + " as IObjectWithSite."; @@ -169,16 +170,20 @@ void SetupHandler(Guid clsid) // If we use Activator.CreateInstance(Type.GetTypeFromCLSID(...)), // CLR will allow in-process server, which defeats isolation and // creates strange bugs. - int hr = Win32PInvoke.CoCreateInstance(ref clsid, IntPtr.Zero, Win32PInvoke.ClassContext.LocalServer, ref iid, out pph); + HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, &iid, &pphRaw); + IntPtr pph = (IntPtr)pphRaw; // See https://blogs.msdn.microsoft.com/adioltean/2005/06/24/when-cocreateinstance-returns-0x80080005-co_e_server_exec_failure/ // CO_E_SERVER_EXEC_FAILURE also tends to happen when debugging in Visual Studio. // Moreover, to create the instance in a server at low integrity level, we need // to use another thread with low mandatory label. We keep it simple by creating // a same-integrity object. - if (hr == E_SERVER_EXEC_FAILURE) - hr = Win32PInvoke.CoCreateInstance(ref clsid, IntPtr.Zero, Win32PInvoke.ClassContext.LocalServer, ref iid, out pph); - if (hr < 0) - throw new COMException(cannotCreate, hr); + if (hr.Value == E_SERVER_EXEC_FAILURE) + { + hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, &iid, &pphRaw); + pph = (IntPtr)pphRaw; + } + if (hr.Failed) + throw new COMException(cannotCreate, hr.Value); var previewHandlerObject = comWrappers.GetOrCreateObjectForComInstance(pph, CreateObjectFlags.UniqueInstance); previewHandler = previewHandlerObject as IPreviewHandler; @@ -189,9 +194,9 @@ void SetupHandler(Guid clsid) var objectWithSite = previewHandlerObject as IObjectWithSite; if (objectWithSite == null) throw new COMException(cannotCast); - hr = objectWithSite.SetSite(comWrappers.GetOrCreateComInterfaceForObject(comSite, CreateComInterfaceFlags.None)); - if (hr < 0) - throw new COMException(cannotSetSite, hr); + int setSiteHr = objectWithSite.SetSite(comWrappers.GetOrCreateComInterfaceForObject(comSite, CreateComInterfaceFlags.None)); + if (setSiteHr < 0) + throw new COMException(cannotSetSite, setSiteHr); visuals = previewHandlerObject as IPreviewHandlerVisuals; } @@ -574,4 +579,4 @@ public void Dispose() #endregion } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs index 591a2c26529c..472378bf74dc 100644 --- a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs @@ -4,8 +4,9 @@ using Files.Shared.Helpers; using System.IO; using System.Runtime.InteropServices; -using System.Text; using System.Windows.Input; +using Windows.Win32; +using Windows.Win32.UI.Shell; namespace Files.App.ViewModels.Dialogs { @@ -228,20 +229,34 @@ private bool IsValidAbsolutePath(string path) return Path.Exists(path) && Path.IsPathFullyQualified(path) && path != Path.GetPathRoot(path); } - private Task SelectDestination() + private unsafe Task SelectDestination() { - Win32PInvoke.BROWSEINFO bi = new Win32PInvoke.BROWSEINFO(); - bi.ulFlags = 0x00004000; - bi.lpszTitle = "Select a folder"; - nint pidl = Win32PInvoke.SHBrowseForFolder(ref bi); - if (pidl != nint.Zero) + BROWSEINFOW bi = new() { - StringBuilder path = new StringBuilder(260); - if (Win32PInvoke.SHGetPathFromIDList(pidl, path)) + ulFlags = 0x00004000 + }; + + Span displayName = stackalloc char[260]; + fixed (char* displayNameBuffer = displayName) + fixed (char* title = "Select a folder") + { + bi.pszDisplayName = displayNameBuffer; + bi.lpszTitle = title; + var pidl = PInvoke.SHBrowseForFolder(ref bi); + if (pidl is not null) { - ShortcutTarget = path.ToString(); + Span path = stackalloc char[260]; + fixed (char* pathBuffer = path) + { + if (PInvoke.SHGetPathFromIDList(pidl, pathBuffer)) + { + var length = path.IndexOf('\0'); + ShortcutTarget = path[..(length < 0 ? path.Length : length)].ToString(); + } + } + + Marshal.FreeCoTaskMem((nint)pidl); } - Marshal.FreeCoTaskMem(pidl); } return Task.CompletedTask; @@ -294,4 +309,4 @@ private void AutoFillName() } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index f016c95f0cf0..f7f70a4fbf93 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -18,6 +18,7 @@ using Windows.Storage; using Windows.Storage.FileProperties; using Windows.Storage.Search; +using Windows.Win32; using Windows.Win32.System.SystemServices; using static Files.App.Helpers.Win32PInvoke; using ByteSize = ByteSizeLib.ByteSize; @@ -2233,7 +2234,8 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat notifyFilters |= FILE_NOTIFY_CHANGE_ATTRIBUTES; var overlapped = new OVERLAPPED(); - overlapped.hEvent = CreateEvent(IntPtr.Zero, false, false, null); + using var eventHandle = PInvoke.CreateEvent(null, false, false, null); + overlapped.hEvent = eventHandle.DangerousGetHandle(); const uint INFINITE = 0xFFFFFFFF; while (x.Status != AsyncStatus.Canceled) @@ -2297,7 +2299,6 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat } } - CloseHandle(overlapped.hEvent); operationQueue.Clear(); Debug.WriteLine("aWatcherAction done: {0}", rand); @@ -2344,7 +2345,8 @@ private void WatchForGitChanges() var notifyFilters = FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_CREATION; var overlapped = new OVERLAPPED(); - overlapped.hEvent = CreateEvent(IntPtr.Zero, false, false, null); + using var eventHandle = PInvoke.CreateEvent(null, false, false, null); + overlapped.hEvent = eventHandle.DangerousGetHandle(); const uint INFINITE = 0xFFFFFFFF; while (x.Status != AsyncStatus.Canceled) @@ -2389,7 +2391,6 @@ private void WatchForGitChanges() } } - CloseHandle(overlapped.hEvent); gitChangesQueue.Clear(); });