feat(cua-windows): allow selecting unlisted executables by path

This commit is contained in:
Huo Yaoyuan authored and GitHub committed 2026-11-10 23:13:40 +08:00
1 parent 1374429891
commit f5ceca4b1d
4 files changed
+55 -3

No files matched your search

@@ -94,6 +94,39 @@ internal static class Applications
? @"\\" + resolved[8..] : resolved.StartsWith(@"\\?\", StringComparison.Ordinal) ? resolved[4..] : resolved;
}
internal static string ResolveExecutable(string path)
{
try
{
if (!Path.IsPathFullyQualified(path) || path.StartsWith(@"\\.\", StringComparison.Ordinal)
|| path.StartsWith(@"\\?\", StringComparison.Ordinal)
|| !string.Equals(Path.GetExtension(path), ".exe", StringComparison.OrdinalIgnoreCase))
throw new NativeFailure("INVALID_ARGUMENT", "Use an absolute .exe file path, not a command line or device path");
if ((File.GetAttributes(path) & FileAttributes.Directory) != 0)
throw new NativeFailure("INVALID_ARGUMENT", "The selected executable path is a directory");
var resolved = CanonicalPath(path);
if (!string.Equals(Path.GetExtension(resolved), ".exe", StringComparison.OrdinalIgnoreCase))
throw new NativeFailure("INVALID_ARGUMENT", "The selected path must resolve to an .exe file");
return resolved;
}
catch (Exception error) when (error is FileNotFoundException or DirectoryNotFoundException)
{
throw new NativeFailure("APP_NOT_FOUND", "The selected executable does not exist; provide an existing absolute .exe path");
}
catch (UnauthorizedAccessException)
{
throw new NativeFailure("PERMISSION_DENIED", "Windows refused access to the selected executable");
}
catch (Exception error) when (error is ArgumentException or NotSupportedException)
{
throw new NativeFailure("INVALID_ARGUMENT", "The executable path is invalid; provide an absolute .exe file path");
}
catch (IOException)
{
throw new NativeFailure("APP_IDENTITY_UNAVAILABLE", "The selected executable could not be read; check the file before selecting it again");
}
}
internal static string Digest(string path)
{
using var file = File.OpenRead(path);
@@ -101,7 +101,8 @@ internal sealed class DesktopExecutor(MtaDispatcher mta, StaDispatcher sta) : IA
{
var before = Windows.Refresh(app, context)
.Select(window => (window.Handle, window.ProcessId, window.ProcessStarted)).ToHashSet();
await sta.Invoke(() => ShellBroker.Launch(@"shell:AppsFolder\" + app.AppId), cancellation);
await sta.Invoke(() => ShellBroker.Launch(Path.IsPathFullyQualified(app.AppId)
? app.AppId : @"shell:AppsFolder\" + app.AppId), cancellation);
var elapsed = Stopwatch.StartNew();
WindowState[] windows;
while (true)
@@ -42,6 +42,16 @@ internal static class DesktopWindows
internal static async Task<AppState> Select(string selector, RequestContext context)
{
context.Check();
if (Path.IsPathFullyQualified(selector))
{
var path = Applications.ResolveExecutable(selector);
var windows = Observe(context).Windows;
context.Check();
// An unlisted executable discloses its full path in the consent question.
return new AppState(Guid.NewGuid().ToString("N"), path, Path.GetFileName(path),
ApplicationAuthorization.Create(path, path, path), windows.Any(window => string.Equals(window.AppId, path, StringComparison.Ordinal)));
}
var applications = await Discover(context);
var application = SelectApplication(applications, selector);
context.Check();
@@ -107,10 +117,10 @@ internal static class DesktopWindows
private static CatalogApplication SelectApplication(IReadOnlyList<CatalogApplication> applications, string selector)
{
if (string.IsNullOrEmpty(selector))
throw new NativeFailure("INVALID_ARGUMENT", "An exact application id from listApps is required");
throw new NativeFailure("INVALID_ARGUMENT", "An exact application id from listApps or an absolute .exe file path is required");
foreach (var application in applications)
if (string.Equals(application.AppId, selector, StringComparison.Ordinal)) return application;
throw new NativeFailure("APP_NOT_FOUND", "Use an exact application id from listApps");
throw new NativeFailure("APP_NOT_FOUND", "Use an exact application id from listApps or an existing absolute .exe file path");
}
private static CatalogApplication[] Merge(ShellApplication[] shellApplications, ResolvedWindow[] windows)
@@ -21,6 +21,14 @@ Refuses(() => NativeRequest.Parse(WireJson.Parse("""{"id":"one","method":"getSta
Refuses(() => NativeRequest.Parse(WireJson.Parse("""{"id":"one","method":"typeText","params":{"appHandle":"app","text":"hello","unowned":1}}""")), "INVALID_ARGUMENT");
Refuses(() => WireJson.Parse("""{"id":"one","id":"two"}"""), "INVALID_REQUEST");
Refuses(() => WireJson.Parse("""{"params":{"x":1,"x":2}}"""), "INVALID_REQUEST");
foreach (var path in new[] { @"C:\Tools\Editor.exe", @"D:\Portable Apps\编辑器.exe", @"\\server\tools\Editor.exe" })
{
var selection = NativeRequest.Parse(WireJson.Parse(System.Text.Json.JsonSerializer.Serialize(new
{
id = "select", method = "getApp", @params = new { selector = path },
})));
Check(selection.Params.GetProperty("selector").GetString() == path, "Executable selection preserves the exact path on the wire");
}
var launch = NativeRequest.Parse(WireJson.Parse("""{"id":"launch","method":"launchApp","params":{"appHandle":"pending"}}"""));
Check(launch.Method == "launchApp" && launch.Params.GetProperty("appHandle").GetString() == "pending",
"Host launch continuation retains the selected application handle");