@neuvybe/jarvis 0.74.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.
@@ -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 the app as its own
4
- # child, with Explorer's environment, outside every Jarvis container. It writes nothing; its
5
- # exit code is all it tells the core. Operations: 0 open, 1 probe (is a desktop shell there?),
6
- # 2 check (the second look alone, no COM). Fixed codes: 0 opened (a probe: a shell is there; a
7
- # check: accepted), 0x6A560011 restricted language mode, 0x6A560012 the compile failed, 0x6A560013 the
8
- # spec was rejected, 0x6A560014 no desktop shell, 0x6A560015 Explorer refused, 0x6A560016 an
9
- # internal failure. With no desktop shell window it activates nothing, so it never starts
10
- # Explorer itself.
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
- if (op != 0 || !Acceptable(file, args)) return Spec;
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(file, args, "", "open", 1);
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 target first; this is the second, narrower look: a page, a drive
86
- // path, or a vetted program with one quoted path as its argument. Nothing here is echoed.
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 IsPage(file) || IsDrivePath(file);
90
- if (!IsDrivePath(file) || !file.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) return false;
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 IsPage(string file) {
97
- bool scheme = file.StartsWith("http://", StringComparison.Ordinal)
98
- || file.StartsWith("https://", StringComparison.Ordinal);
99
- return scheme && file.IndexOf('"') < 0 && file.IndexOf(' ') < 0 && file.IndexOf('\\') < 0;
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 bool IsDrivePath(string path) {
103
- if (path.Length < 3 || !char.IsLetter(path[0]) || path[1] != ':' || path[2] != '\\') return false;
104
- if (path.IndexOf(':', 2) >= 0 || path.IndexOf('%') >= 0 || path.IndexOf('"') >= 0) return false;
105
- if (path.IndexOf("shell:", StringComparison.OrdinalIgnoreCase) >= 0) return false;
106
- return path.IndexOfAny(new char[] { '<', '>', '|', '?', '*' }) < 0;
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