@neuvybe/jarvis 0.75.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/open-helper.ps1 +154 -26
- package/dist/index.js +191 -191
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Windows hand-off for the `open` tool (packages/core/src/tools/open-launch.ts).
|
|
2
2
|
# One request per process: it reads one binary spec from standard input, asks the running
|
|
3
|
-
# Explorer to open the target, and exits with a fixed code. Explorer creates
|
|
4
|
-
# child, with Explorer's environment, outside every Jarvis container. It
|
|
5
|
-
# exit code is all it tells the core. Operations: 0 open, 1 probe
|
|
6
|
-
# 2 check (the second look alone, no COM)
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# Explorer
|
|
3
|
+
# Explorer to open the target or start the app, and exits with a fixed code. Explorer creates
|
|
4
|
+
# the app as its own child, with Explorer's environment, outside every Jarvis container. It
|
|
5
|
+
# writes nothing; its exit code is all it tells the core. Operations: 0 open a target, 1 probe
|
|
6
|
+
# (is a desktop shell there?), 2 check (the second look alone, no COM), 3 launch an app by its
|
|
7
|
+
# name or path, alone or with one quoted target, 4 resolve an app without launching it. Fixed
|
|
8
|
+
# codes: 0 opened (a probe: a shell is there; a check: accepted; a resolve: found), 0x6A560011
|
|
9
|
+
# restricted language mode, 0x6A560012 the compile failed, 0x6A560013 the spec was rejected,
|
|
10
|
+
# 0x6A560014 no desktop shell, 0x6A560015 Explorer refused, 0x6A560016 an internal failure,
|
|
11
|
+
# 0x6A560017 no app of that name or path. With no desktop shell window it activates nothing,
|
|
12
|
+
# so it never starts Explorer itself.
|
|
11
13
|
$ErrorActionPreference = 'Stop'
|
|
12
14
|
$ProgressPreference = 'SilentlyContinue'
|
|
13
15
|
$PSModuleAutoLoadingPreference = 'None'
|
|
@@ -18,6 +20,7 @@ using System;
|
|
|
18
20
|
using System.IO;
|
|
19
21
|
using System.Runtime.InteropServices;
|
|
20
22
|
using System.Text;
|
|
23
|
+
using Microsoft.Win32;
|
|
21
24
|
|
|
22
25
|
public static class JarvisOpen {
|
|
23
26
|
const int Opened = 0;
|
|
@@ -25,8 +28,10 @@ public static class JarvisOpen {
|
|
|
25
28
|
const int NoDesktop = 0x6A560014;
|
|
26
29
|
const int Refused = 0x6A560015;
|
|
27
30
|
const int Internal = 0x6A560016;
|
|
31
|
+
const int NotFound = 0x6A560017;
|
|
28
32
|
const int MaxSpec = 64 * 1024;
|
|
29
33
|
const int MaxField = 32 * 1024;
|
|
34
|
+
const int MaxDepth = 8;
|
|
30
35
|
|
|
31
36
|
[DllImport("user32.dll")]
|
|
32
37
|
static extern IntPtr GetShellWindow();
|
|
@@ -44,12 +49,25 @@ public static class JarvisOpen {
|
|
|
44
49
|
// A check runs only the second look below: no COM, nothing opened (tests, diagnostics).
|
|
45
50
|
if (op == 2) return Acceptable(file, args) ? Opened : Spec;
|
|
46
51
|
if (op == 1) return ShellApplication() == null ? NoDesktop : Opened;
|
|
47
|
-
|
|
52
|
+
// A resolve answers whether the app is found, launching nothing (tests, diagnostics).
|
|
53
|
+
if (op == 4) {
|
|
54
|
+
if (!Acceptable(file, args) || args.Length != 0) return Spec;
|
|
55
|
+
return Resolve(file, false, null) == null ? NotFound : Opened;
|
|
56
|
+
}
|
|
57
|
+
// A target alone travels with no arguments; an app may take one quoted target.
|
|
58
|
+
if ((op != 0 && op != 3) || !Acceptable(file, args)) return Spec;
|
|
59
|
+
if (op == 0 && args.Length != 0) return Spec;
|
|
48
60
|
object shell = ShellApplication();
|
|
49
61
|
if (shell == null) return NoDesktop;
|
|
62
|
+
string launch = file;
|
|
63
|
+
if (op == 3) {
|
|
64
|
+
launch = Resolve(file, args.Length != 0, shell);
|
|
65
|
+
// A Store app is there but takes no target this way: the hand-off is refused, not missing.
|
|
66
|
+
if (launch == null) return args.Length != 0 && StoreApp(file, shell) != null ? Refused : NotFound;
|
|
67
|
+
}
|
|
50
68
|
try {
|
|
51
69
|
dynamic dispatch = shell;
|
|
52
|
-
dispatch.ShellExecute(
|
|
70
|
+
dispatch.ShellExecute(launch, args, "", "open", 1);
|
|
53
71
|
} catch {
|
|
54
72
|
return Refused;
|
|
55
73
|
}
|
|
@@ -82,28 +100,138 @@ public static class JarvisOpen {
|
|
|
82
100
|
return text;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
// The core checks every
|
|
86
|
-
//
|
|
103
|
+
// The core checks every request first; this is the helper's own second look, at the form
|
|
104
|
+
// only - what may be opened is not its question: something to hand on, no control
|
|
105
|
+
// character, no leading dash, and the arguments empty or exactly one quoted value.
|
|
87
106
|
static bool Acceptable(string file, string args) {
|
|
107
|
+
if (file.Length == 0 || file[0] == '-') return false;
|
|
88
108
|
foreach (char c in file + args) if (c < ' ' || c == (char)127) return false;
|
|
89
|
-
if (args.Length == 0) return
|
|
90
|
-
|
|
91
|
-
if (args.Length < 3 || args[0] != '"' || args[args.Length - 1] != '"') return false;
|
|
92
|
-
string inner = args.Substring(1, args.Length - 2);
|
|
93
|
-
return inner.IndexOf('"') < 0 && IsDrivePath(inner.TrimEnd('\\') + "\\");
|
|
109
|
+
if (args.Length == 0) return true;
|
|
110
|
+
return args.Length >= 3 && args[0] == '"' && args.IndexOf('"', 1) == args.Length - 1;
|
|
94
111
|
}
|
|
95
112
|
|
|
96
|
-
static bool
|
|
97
|
-
|
|
98
|
-
||
|
|
99
|
-
|
|
113
|
+
static bool IsAbsolute(string path) {
|
|
114
|
+
return path.StartsWith("\\\\", StringComparison.Ordinal)
|
|
115
|
+
|| (path.Length >= 3 && char.IsLetter(path[0]) && path[1] == ':' && path[2] == '\\');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Finds an app as the user knows it, in this order: its absolute path; a Start-menu shortcut
|
|
119
|
+
// of exactly that name (the user's, then all users'); an App Paths entry for <name>.exe (the
|
|
120
|
+
// user's, then the machine's); and, to start it by itself only, a Store app of that name in
|
|
121
|
+
// the Start menu's app list. With a target to hand it (needProgram), the answer must be a
|
|
122
|
+
// program the target can be passed to. Null when none is found.
|
|
123
|
+
static string Resolve(string app, bool needProgram, object shell) {
|
|
124
|
+
if (IsAbsolute(app)) return File.Exists(app) ? app : null;
|
|
125
|
+
if (app.IndexOf('\\') >= 0 || app.IndexOf('/') >= 0) return null;
|
|
126
|
+
string shortcut = StartMenuShortcut(app);
|
|
127
|
+
if (shortcut != null) {
|
|
128
|
+
if (!needProgram) return shortcut;
|
|
129
|
+
string program = ShortcutProgram(shortcut);
|
|
130
|
+
if (program != null) return program;
|
|
131
|
+
}
|
|
132
|
+
string registered = AppPath(app);
|
|
133
|
+
if (registered != null) return registered;
|
|
134
|
+
return needProgram || shell == null ? null : StoreApp(app, shell);
|
|
100
135
|
}
|
|
101
136
|
|
|
102
|
-
static
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
static string StartMenuShortcut(string name) {
|
|
138
|
+
string fileName = name + ".lnk";
|
|
139
|
+
foreach (Environment.SpecialFolder root in new Environment.SpecialFolder[] {
|
|
140
|
+
Environment.SpecialFolder.Programs, Environment.SpecialFolder.CommonPrograms }) {
|
|
141
|
+
string folder = Environment.GetFolderPath(root);
|
|
142
|
+
if (folder.Length == 0) continue;
|
|
143
|
+
string found = FindShortcut(folder, fileName, 0);
|
|
144
|
+
if (found != null) return found;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Depth first: a folder's own shortcuts before its subfolders', and subfolders in name order,
|
|
150
|
+
// so a top-level match wins over any nested one. A folder that cannot be read is skipped.
|
|
151
|
+
static string FindShortcut(string folder, string fileName, int depth) {
|
|
152
|
+
if (depth > MaxDepth) return null;
|
|
153
|
+
string[] files;
|
|
154
|
+
string[] folders;
|
|
155
|
+
try {
|
|
156
|
+
files = Directory.GetFiles(folder);
|
|
157
|
+
folders = Directory.GetDirectories(folder);
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
Array.Sort(files, StringComparer.OrdinalIgnoreCase);
|
|
162
|
+
foreach (string file in files) {
|
|
163
|
+
if (string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) return file;
|
|
164
|
+
}
|
|
165
|
+
Array.Sort(folders, StringComparer.OrdinalIgnoreCase);
|
|
166
|
+
foreach (string sub in folders) {
|
|
167
|
+
string found = FindShortcut(sub, fileName, depth + 1);
|
|
168
|
+
if (found != null) return found;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The program a shortcut starts, to hand a target to, only when the shortcut is that program
|
|
174
|
+
// alone: a plain .exe on disk and no arguments of its own. A launcher stub (Squirrel's
|
|
175
|
+
// Update.exe --processStart, a browser profile or web-app shortcut) adds arguments, and an
|
|
176
|
+
// installer's advertised shortcut points into the Windows Installer cache; both fall through
|
|
177
|
+
// to App Paths.
|
|
178
|
+
static string ShortcutProgram(string shortcut) {
|
|
179
|
+
try {
|
|
180
|
+
dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell"));
|
|
181
|
+
dynamic link = shell.CreateShortcut(shortcut);
|
|
182
|
+
string target = link.TargetPath;
|
|
183
|
+
string arguments = link.Arguments;
|
|
184
|
+
if (string.IsNullOrEmpty(target) || !string.IsNullOrEmpty(arguments)) return null;
|
|
185
|
+
string installer = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Installer") + "\\";
|
|
186
|
+
if (target.StartsWith(installer, StringComparison.OrdinalIgnoreCase)) return null;
|
|
187
|
+
if (target.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) && File.Exists(target)) return target;
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static string AppPath(string name) {
|
|
194
|
+
string key = name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name : name + ".exe";
|
|
195
|
+
foreach (RegistryKey hive in new RegistryKey[] { Registry.CurrentUser, Registry.LocalMachine }) {
|
|
196
|
+
try {
|
|
197
|
+
using (RegistryKey entry = hive.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + key)) {
|
|
198
|
+
if (entry == null) continue;
|
|
199
|
+
string value = entry.GetValue("") as string;
|
|
200
|
+
if (value == null) continue;
|
|
201
|
+
value = Environment.ExpandEnvironmentVariables(value).Trim().Trim('"');
|
|
202
|
+
if (IsAbsolute(value) && File.Exists(value)) return value;
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A Store app of exactly that name in the Start menu's app list, as a shell:AppsFolder
|
|
211
|
+
// location, which starts it with no arguments. An item that cannot be read is skipped.
|
|
212
|
+
static string StoreApp(string name, object shell) {
|
|
213
|
+
dynamic items;
|
|
214
|
+
int count;
|
|
215
|
+
try {
|
|
216
|
+
dynamic dispatch = shell;
|
|
217
|
+
dynamic folder = dispatch.NameSpace("shell:AppsFolder");
|
|
218
|
+
if (folder == null) return null;
|
|
219
|
+
items = folder.Items();
|
|
220
|
+
count = items.Count;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
for (int i = 0; i < count; i++) {
|
|
225
|
+
try {
|
|
226
|
+
dynamic item = items.Item(i);
|
|
227
|
+
string label = item.Name;
|
|
228
|
+
if (!string.Equals(label, name, StringComparison.OrdinalIgnoreCase)) continue;
|
|
229
|
+
string id = item.Path;
|
|
230
|
+
if (!string.IsNullOrEmpty(id)) return "shell:AppsFolder\\" + id;
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
107
235
|
}
|
|
108
236
|
|
|
109
237
|
// The running Explorer's shell automation object for the desktop, or null with no desktop
|