Files
PowerToys/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs
Davide Giacometti 227c5d8147 [Peek] Terminate Preview Handlers Processes (#40116)
<!-- 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 is an attempt to release Preview Handlers instantiated by Peek and
close the related processe.

⚠️ Note that even if the PR improve the current behavior, the solution
doesn't work 100% of times.
I noticed that sometimes the process gets leaked also when Preview
Handler is used in Explorer 🤔

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] **Closes:** #40117
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

- Ported the same fix applied to CmdPal to ensure the process is
terminated gracefully instead of being killed:
https://github.com/microsoft/PowerToys/pull/39589
- Attempt to cleanup Preview Handlers and close the relative process

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

Tested manually:
- Preview through some Excel and Word files
- Close Peek window
- Excel and Word processes are closed
2025-07-14 08:50:45 +08:00

253 lines
8.5 KiB
C#

// 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.Collections.Concurrent;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Microsoft.Win32;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers.Helpers;
using Peek.FilePreviewer.Previewers.Interfaces;
using Windows.Win32;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.Shell.PropertiesSystem;
using IShellItem = Windows.Win32.UI.Shell.IShellItem;
namespace Peek.FilePreviewer.Previewers
{
public partial class ShellPreviewHandlerPreviewer : ObservableObject, IShellPreviewHandlerPreviewer, IDisposable
{
private static readonly ConcurrentDictionary<Guid, IClassFactory> HandlerFactories = new();
[ObservableProperty]
private IPreviewHandler? preview;
[ObservableProperty]
private PreviewState state;
private Stream? fileStream;
public ShellPreviewHandlerPreviewer(IFileSystemItem file)
{
FileItem = file;
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
private IFileSystemItem FileItem { get; }
private DispatcherQueue Dispatcher { get; }
public void Dispose()
{
Clear();
GC.SuppressFinalize(this);
}
public async Task CopyAsync()
{
await Dispatcher.RunOnUiThread(async () =>
{
var storageItem = await FileItem.GetStorageItemAsync();
ClipboardHelper.SaveToClipboard(storageItem);
});
}
public Task<PreviewSize> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new PreviewSize { MonitorSize = null });
}
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
Clear();
State = PreviewState.Loading;
cancellationToken.ThrowIfCancellationRequested();
// Create the preview handler
var previewHandler = await Task.Run(() =>
{
var previewHandlerGuid = GetPreviewHandlerGuid(FileItem.Extension);
if (!string.IsNullOrEmpty(previewHandlerGuid))
{
var clsid = Guid.Parse(previewHandlerGuid);
bool retry = false;
do
{
unsafe
{
// This runs the preview handler in a separate process (prevhost.exe)
if (!HandlerFactories.TryGetValue(clsid, out var factory))
{
var hr = PInvoke_FilePreviewer.CoGetClassObject(clsid, CLSCTX.CLSCTX_LOCAL_SERVER, null, typeof(IClassFactory).GUID, out object pFactory);
Marshal.ThrowExceptionForHR(hr);
// Storing the factory in memory helps makes the handlers load faster
factory = (IClassFactory)pFactory;
factory.LockServer(true);
HandlerFactories.AddOrUpdate(clsid, factory, (_, _) => factory);
}
try
{
var iid = typeof(IPreviewHandler).GUID;
factory.CreateInstance(null, &iid, out var instance);
return instance as IPreviewHandler;
}
catch
{
if (!retry)
{
// Process is probably dead, attempt to get the factory again (once)
HandlerFactories.TryRemove(new(clsid, factory));
retry = true;
}
else
{
break;
}
}
}
}
while (retry);
}
return null;
});
if (previewHandler == null)
{
State = PreviewState.Error;
return;
}
cancellationToken.ThrowIfCancellationRequested();
// Initialize the preview handler with the selected file
bool success = await Task.Run(() =>
{
const uint STGM_READ = 0x00000000;
if (previewHandler is IInitializeWithStream initWithStream)
{
fileStream = File.OpenRead(FileItem.Path);
initWithStream.Initialize(new IStreamWrapper(fileStream), STGM_READ);
}
else if (previewHandler is IInitializeWithItem initWithItem)
{
var hr = PInvoke_FilePreviewer.SHCreateItemFromParsingName(FileItem.Path, null, typeof(IShellItem).GUID, out var item);
Marshal.ThrowExceptionForHR(hr);
initWithItem.Initialize((IShellItem)item, STGM_READ);
}
else if (previewHandler is IInitializeWithFile initWithFile)
{
unsafe
{
fixed (char* pPath = FileItem.Path)
{
initWithFile.Initialize(pPath, STGM_READ);
}
}
}
else
{
// Handler is missing the required interfaces
return false;
}
return true;
});
if (!success)
{
State = PreviewState.Error;
return;
}
cancellationToken.ThrowIfCancellationRequested();
// Preview.SetWindow() needs to be set in the control
Preview = previewHandler;
}
public void Clear()
{
if (Preview != null)
{
try
{
Preview.Unload();
Marshal.FinalReleaseComObject(Preview);
}
catch
{
}
Preview = null;
}
if (fileStream != null)
{
fileStream.Dispose();
fileStream = null;
}
}
public static bool IsItemSupported(IFileSystemItem item)
{
return !string.IsNullOrEmpty(GetPreviewHandlerGuid(item.Extension));
}
public static void ReleaseHandlerFactories()
{
foreach (var factory in HandlerFactories.Values)
{
try
{
Marshal.FinalReleaseComObject(factory);
}
catch
{
}
}
}
private static string? GetPreviewHandlerGuid(string fileExt)
{
const string PreviewHandlerKeyPath = "shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}";
// Search by file extension
using var classExtensionKey = Registry.ClassesRoot.OpenSubKey(fileExt);
using var classExtensionPreviewHandlerKey = classExtensionKey?.OpenSubKey(PreviewHandlerKeyPath);
if (classExtensionKey != null && classExtensionPreviewHandlerKey == null)
{
// Search by file class
var className = classExtensionKey.GetValue(null) as string;
if (!string.IsNullOrEmpty(className))
{
using var classKey = Registry.ClassesRoot.OpenSubKey(className);
using var classPreviewHandlerKey = classKey?.OpenSubKey(PreviewHandlerKeyPath);
return classPreviewHandlerKey?.GetValue(null) as string;
}
}
return classExtensionPreviewHandlerKey?.GetValue(null) as string;
}
}
}