feat(cua-windows): allow selecting unlisted executables by path
This commit is contained in:
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");
|
||||
|
||||
Reference in new issue
Block a user