@crewhaus/tool-desktop 0.7.0 → 0.8.1
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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +121 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +139 -19
- package/dist/lib/clipboard.d.ts +1 -0
- package/dist/lib/clipboard.js +6 -0
- package/dist/lib/fsseam.d.ts +12 -0
- package/dist/lib/fsseam.js +86 -11
- package/dist/lib/open.d.ts +7 -1
- package/dist/lib/open.js +224 -4
- package/dist/lib/power.d.ts +38 -7
- package/dist/lib/power.js +64 -7
- package/dist/lib/presence.js +8 -1
- package/dist/lib/print.d.ts +8 -0
- package/dist/lib/print.js +63 -3
- package/dist/lib/windows.js +5 -2
- package/dist/paths.d.ts +21 -2
- package/dist/paths.js +72 -13
- package/dist/run.d.ts +7 -0
- package/dist/run.js +55 -75
- package/package.json +12 -7
- package/dist/fixtures.d.ts +0 -84
- package/dist/fixtures.js +0 -130
package/dist/lib/open.js
CHANGED
|
@@ -265,7 +265,24 @@ export function classifyTarget(target, allowed) {
|
|
|
265
265
|
* - `.desktop`, `.lnk`, `.url` and `.webloc` are INDIRECTION. They contain a
|
|
266
266
|
* target, and opening one reaches a program or a URL that never passed
|
|
267
267
|
* the scheme allow-list at the top of this file. A `.url` file holding
|
|
268
|
-
* `URL=file:///etc/passwd` walks straight around the gate.
|
|
268
|
+
* `URL=file:///etc/passwd` walks straight around the gate. The macOS
|
|
269
|
+
* location files are the same family: `.fileloc` is `.webloc`'s file:
|
|
270
|
+
* twin, and Launch Services resolves it straight to its target — an app
|
|
271
|
+
* outside the workspace, or a workspace `.command` this list refuses by
|
|
272
|
+
* name, laundered through one level of indirection (flag-truth-5#6,
|
|
273
|
+
* security-10#4). `.afploc`/`.ftploc` are schemes refused above.
|
|
274
|
+
*
|
|
275
|
+
* The list also names what INSTALLS or RUNS on open without an execute bit:
|
|
276
|
+
* configuration profiles, Java Web Start, ClickOnce and MSIX installers,
|
|
277
|
+
* scripts an interpreter's launcher is registered for, Automator and
|
|
278
|
+
* Shortcuts documents, macOS plug-in bundles, every disk image type macOS's
|
|
279
|
+
* DiskImageMounter registers, System Settings extensions and network
|
|
280
|
+
* configurations, certificates and keychains Keychain Access imports, and
|
|
281
|
+
* Microsoft's Level-1 attachment list in full. A deny-list always trails the
|
|
282
|
+
* handler table on a real machine; this one is the union of what the 0.7.0
|
|
283
|
+
* reviews found and those handlers register (read from their Info.plist
|
|
284
|
+
* files on macOS 26), and an allow-list of document types for path opens is
|
|
285
|
+
* the structural fix still to be decided.
|
|
269
286
|
*/
|
|
270
287
|
const DANGEROUS_EXTENSIONS = new Set([
|
|
271
288
|
// runs directly
|
|
@@ -293,6 +310,9 @@ const DANGEROUS_EXTENSIONS = new Set([
|
|
|
293
310
|
"bash",
|
|
294
311
|
"zsh",
|
|
295
312
|
"fish",
|
|
313
|
+
"ksh",
|
|
314
|
+
"csh",
|
|
315
|
+
"tcsh",
|
|
296
316
|
"command",
|
|
297
317
|
"tool",
|
|
298
318
|
"run",
|
|
@@ -307,17 +327,217 @@ const DANGEROUS_EXTENSIONS = new Set([
|
|
|
307
327
|
"scptd",
|
|
308
328
|
"applescript",
|
|
309
329
|
"terminal",
|
|
330
|
+
// a Terminal.app session file: Terminal is its registered handler and
|
|
331
|
+
// runs the command its ExecutionString names
|
|
332
|
+
"term",
|
|
333
|
+
// more macOS bundles, installers and automation
|
|
334
|
+
"mpkg",
|
|
335
|
+
"prefpane",
|
|
336
|
+
"saver",
|
|
337
|
+
"service",
|
|
338
|
+
"osax",
|
|
339
|
+
"qlgenerator",
|
|
340
|
+
"kext",
|
|
341
|
+
"plugin",
|
|
342
|
+
"bundle",
|
|
343
|
+
"appex",
|
|
344
|
+
"action",
|
|
345
|
+
"caction",
|
|
346
|
+
"wflow",
|
|
347
|
+
"shortcut",
|
|
348
|
+
"mobileconfig",
|
|
349
|
+
"configprofile",
|
|
350
|
+
"provisionprofile",
|
|
351
|
+
// interpreters whose launcher is registered to RUN the file on open
|
|
352
|
+
"py",
|
|
353
|
+
"pyw",
|
|
354
|
+
"pyz",
|
|
355
|
+
"pyzw",
|
|
356
|
+
"pyc",
|
|
357
|
+
"pyo",
|
|
358
|
+
"pl",
|
|
359
|
+
"rb",
|
|
360
|
+
"jnlp",
|
|
361
|
+
// Windows: installers, shell and management documents that act on open
|
|
362
|
+
"appref-ms",
|
|
363
|
+
"application",
|
|
364
|
+
"appx",
|
|
365
|
+
"appxbundle",
|
|
366
|
+
"msix",
|
|
367
|
+
"msixbundle",
|
|
368
|
+
"appinstaller",
|
|
369
|
+
"msc",
|
|
370
|
+
"chm",
|
|
371
|
+
"gadget",
|
|
372
|
+
"ws",
|
|
373
|
+
"wsc",
|
|
374
|
+
"sct",
|
|
375
|
+
"diagcab",
|
|
376
|
+
"xll",
|
|
377
|
+
"rdp",
|
|
378
|
+
"theme",
|
|
379
|
+
"themepack",
|
|
380
|
+
"deskthemepack",
|
|
381
|
+
// the rest of Microsoft's Level-1 (always blocked) attachment list, in
|
|
382
|
+
// full: each runs, installs, configures or redirects on open
|
|
383
|
+
"appcontent-ms",
|
|
384
|
+
"printerexport",
|
|
385
|
+
"osd",
|
|
386
|
+
"prf",
|
|
387
|
+
"xnk",
|
|
388
|
+
"udl",
|
|
389
|
+
"vsmacros",
|
|
390
|
+
"vsw",
|
|
391
|
+
"webpnp",
|
|
392
|
+
"asp",
|
|
393
|
+
"aspx",
|
|
394
|
+
"asx",
|
|
395
|
+
"bgi",
|
|
396
|
+
"cnt",
|
|
397
|
+
"fxp",
|
|
398
|
+
"hpj",
|
|
399
|
+
"htc",
|
|
400
|
+
"its",
|
|
401
|
+
"ops",
|
|
402
|
+
"pcd",
|
|
403
|
+
"plg",
|
|
404
|
+
"prg",
|
|
405
|
+
"pst",
|
|
406
|
+
"tmp",
|
|
407
|
+
"msu",
|
|
408
|
+
"mst",
|
|
409
|
+
"cab",
|
|
410
|
+
"ins",
|
|
411
|
+
"isp",
|
|
412
|
+
"hlp",
|
|
413
|
+
"grp",
|
|
414
|
+
"mcf",
|
|
415
|
+
"xbap",
|
|
416
|
+
"vb",
|
|
417
|
+
"vbp",
|
|
418
|
+
"bas",
|
|
419
|
+
"shb",
|
|
420
|
+
"shs",
|
|
421
|
+
"wsb",
|
|
422
|
+
"msh",
|
|
423
|
+
"msh1",
|
|
424
|
+
"msh2",
|
|
425
|
+
"mshxml",
|
|
426
|
+
"msh1xml",
|
|
427
|
+
"msh2xml",
|
|
428
|
+
"ps1xml",
|
|
429
|
+
"ps2",
|
|
430
|
+
"ps2xml",
|
|
431
|
+
"psc1",
|
|
432
|
+
"psc2",
|
|
433
|
+
"psd1",
|
|
434
|
+
"psdm1",
|
|
435
|
+
// Access databases and their shortcuts run an AutoExec macro on open
|
|
436
|
+
"ade",
|
|
437
|
+
"adp",
|
|
438
|
+
"accda",
|
|
439
|
+
"accdb",
|
|
440
|
+
"accde",
|
|
441
|
+
"accdr",
|
|
442
|
+
"accdt",
|
|
443
|
+
"mad",
|
|
444
|
+
"maf",
|
|
445
|
+
"mag",
|
|
446
|
+
"mam",
|
|
447
|
+
"maq",
|
|
448
|
+
"mar",
|
|
449
|
+
"mas",
|
|
450
|
+
"mat",
|
|
451
|
+
"mau",
|
|
452
|
+
"mav",
|
|
453
|
+
"maw",
|
|
454
|
+
"mda",
|
|
455
|
+
"mdb",
|
|
456
|
+
"mde",
|
|
457
|
+
"mdt",
|
|
458
|
+
"mdw",
|
|
459
|
+
"mdz",
|
|
460
|
+
// Linux packages and self-running images
|
|
461
|
+
"deb",
|
|
462
|
+
"rpm",
|
|
463
|
+
"flatpakref",
|
|
464
|
+
"appimage",
|
|
465
|
+
"snap",
|
|
466
|
+
// disk images that mount on open (and skip mark-of-the-web on Windows):
|
|
467
|
+
// on macOS, every type DiskImageMounter registers
|
|
468
|
+
"iso",
|
|
469
|
+
"img",
|
|
470
|
+
"vhd",
|
|
471
|
+
"vhdx",
|
|
472
|
+
"udif",
|
|
473
|
+
"toast",
|
|
474
|
+
"dvdr",
|
|
475
|
+
"cdr",
|
|
476
|
+
"dmgpart",
|
|
477
|
+
"sparseimage",
|
|
478
|
+
"asif",
|
|
479
|
+
"sparsebundle",
|
|
480
|
+
"backupbundle",
|
|
481
|
+
// macOS System Settings extensions and network configurations, which it
|
|
482
|
+
// installs on open
|
|
483
|
+
"sysprefex",
|
|
484
|
+
"internetconnect",
|
|
485
|
+
"networkconnect",
|
|
486
|
+
// certificates and keychains: Keychain Access imports them on open (and
|
|
487
|
+
// Windows' Level-1 list names cer, crt and der for the same reason)
|
|
488
|
+
"cer",
|
|
489
|
+
"cert",
|
|
490
|
+
"crt",
|
|
491
|
+
"der",
|
|
492
|
+
"pem",
|
|
493
|
+
"p7",
|
|
494
|
+
"p7b",
|
|
495
|
+
"p7c",
|
|
496
|
+
"p7m",
|
|
497
|
+
"p7r",
|
|
498
|
+
"p7s",
|
|
499
|
+
"p10",
|
|
500
|
+
"p12",
|
|
501
|
+
"pfx",
|
|
502
|
+
"pkcs12",
|
|
503
|
+
"keychain",
|
|
504
|
+
"keychain-db",
|
|
310
505
|
// indirection — the target never passes the scheme gate
|
|
311
506
|
"desktop",
|
|
312
507
|
"lnk",
|
|
313
508
|
"url",
|
|
314
509
|
"webloc",
|
|
315
510
|
"inetloc",
|
|
511
|
+
"fileloc",
|
|
512
|
+
"vncloc",
|
|
513
|
+
"afploc",
|
|
514
|
+
"ftploc",
|
|
515
|
+
"mailloc",
|
|
516
|
+
"newsloc",
|
|
517
|
+
"atloc",
|
|
518
|
+
"telnetloc",
|
|
519
|
+
"website",
|
|
520
|
+
"scf",
|
|
521
|
+
"searchconnector-ms",
|
|
522
|
+
"library-ms",
|
|
523
|
+
"search-ms",
|
|
524
|
+
"settingcontent-ms",
|
|
316
525
|
"inf",
|
|
317
526
|
]);
|
|
318
|
-
/**
|
|
527
|
+
/**
|
|
528
|
+
* The lowercase extension, or `""`.
|
|
529
|
+
*
|
|
530
|
+
* Trailing dots and spaces are dropped first: Windows strips them when it
|
|
531
|
+
* opens a path, so `x.bat.` and `x.bat ` run `x.bat`, and reading them as
|
|
532
|
+
* "no extension" let a refused type through by spelling.
|
|
533
|
+
*/
|
|
319
534
|
export function extensionOf(path) {
|
|
320
|
-
const
|
|
535
|
+
const full = path.split(/[\\/]/).pop() ?? path;
|
|
536
|
+
// A loop, not /[. ]+$/, which rescans the run from every start (quadratic).
|
|
537
|
+
let end = full.length;
|
|
538
|
+
while (end > 0 && (full[end - 1] === "." || full[end - 1] === " "))
|
|
539
|
+
end--;
|
|
540
|
+
const base = full.slice(0, end);
|
|
321
541
|
const dot = base.lastIndexOf(".");
|
|
322
542
|
return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
|
|
323
543
|
}
|
|
@@ -333,7 +553,7 @@ export function refusePath(absolutePath, facts) {
|
|
|
333
553
|
}
|
|
334
554
|
const extension = extensionOf(absolutePath);
|
|
335
555
|
if (DANGEROUS_EXTENSIONS.has(extension)) {
|
|
336
|
-
return `the path ends in ".${extension}", which a desktop
|
|
556
|
+
return `the path ends in ".${extension}", which a desktop RUNS, installs, mounts or imports on open, or follows to a target this tool never got to check — "open this" and "run this" are the same gesture to an operating system, so it is refused`;
|
|
337
557
|
}
|
|
338
558
|
if (facts.isFile && isExecutable(facts)) {
|
|
339
559
|
return "the path is an executable file, and asking a desktop to OPEN one is how it gets RUN — this tool refuses, whatever the registered handler would have done";
|
package/dist/lib/power.d.ts
CHANGED
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
* caller that cannot see when its own inhibitor lapses will hold another.
|
|
18
18
|
* 2. **A PID IS NOT AN IDENTITY.** Pids are reused, and `release` reading a
|
|
19
19
|
* stale state file would otherwise send SIGTERM to whatever now owns that
|
|
20
|
-
* number. So
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* number. So `release` and `status` VERIFY the live process's command line
|
|
21
|
+
* against the holder THIS package starts on this platform
|
|
22
|
+
* ({@link isHolderCommand}) before signalling anything — never against a
|
|
23
|
+
* value the record itself carries, since the record also supplies the pid.
|
|
24
|
+
* A mismatch is reported as `stale` and the file is cleared — nothing is
|
|
25
|
+
* killed. The record lives in a per-user private directory outside the
|
|
26
|
+
* workspace, where the agent's file tools cannot plant one.
|
|
24
27
|
* 3. **WINDOWS CANNOT DO THIS IN A ONE-LINER.** `SetThreadExecutionState` is
|
|
25
28
|
* THREAD-scoped and its effect dies with the thread, so a PowerShell
|
|
26
29
|
* one-liner that sets the flag and exits does precisely nothing — the
|
|
@@ -39,8 +42,34 @@ export type AssertionScope = "system" | "display";
|
|
|
39
42
|
/** The ceiling, and the reason there is one. Eight hours is a working day. */
|
|
40
43
|
export declare const MAX_HOLD_MINUTES = 480;
|
|
41
44
|
export declare const DEFAULT_HOLD_MINUTES = 30;
|
|
42
|
-
/**
|
|
43
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Where 0.7.0 remembered the held assertion, relative to the workspace root.
|
|
47
|
+
*
|
|
48
|
+
* NOT READ ANY MORE, only reported. A file in the workspace can be written by
|
|
49
|
+
* the agent the tool serves (Write, CopyPath, an extracted archive), and the
|
|
50
|
+
* record names the pid `release` signals: a planted record made `release`
|
|
51
|
+
* SIGTERM any process of the operator's (security-10#1). The record now lives
|
|
52
|
+
* in a private per-user directory — see `powerStateFile` in ../index.ts.
|
|
53
|
+
*/
|
|
54
|
+
export declare const LEGACY_STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
|
|
55
|
+
/**
|
|
56
|
+
* The program this package's holder runs on `platform` — the ONE source for
|
|
57
|
+
* both `planHold` and the identity check `release` makes before signalling.
|
|
58
|
+
* The check used to compare against the `marker` field of the state record,
|
|
59
|
+
* which is the same record that supplies the pid, so whoever wrote the record
|
|
60
|
+
* chose what the live process was checked against (security-10#1).
|
|
61
|
+
*/
|
|
62
|
+
export declare function holderMarker(platform: HostPlatform): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Is `commandLine` a holder THIS package starts on `platform`?
|
|
65
|
+
*
|
|
66
|
+
* The program must be the platform's holder, and the arguments must carry
|
|
67
|
+
* what `planHold` always gives it: `-t <seconds>` for caffeinate,
|
|
68
|
+
* `--who=crewhaus` for systemd-inhibit, and the generated type name inside a
|
|
69
|
+
* powershell host. An operator's own `caffeinate -d` is therefore never
|
|
70
|
+
* taken for ours.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isHolderCommand(platform: HostPlatform, commandLine: string): boolean;
|
|
44
73
|
export declare const WINDOWS_INHIBIT: import("./escape").PowerShellScript;
|
|
45
74
|
export declare const WINDOWS_PROCESS_PROBE: import("./escape").PowerShellScript;
|
|
46
75
|
/**
|
|
@@ -134,7 +163,9 @@ export type Liveness = "alive" | "gone" | "reused" | "unknown";
|
|
|
134
163
|
* test is kept for that case alone, narrowed to a powershell host.
|
|
135
164
|
*/
|
|
136
165
|
export declare function commandMatchesMarker(commandLine: string, marker: string): boolean;
|
|
137
|
-
export declare function classifyLiveness(
|
|
166
|
+
export declare function classifyLiveness(
|
|
167
|
+
/** The holder's program name, or a predicate over the live command line. */
|
|
168
|
+
holder: string | ((commandLine: string) => boolean), result: {
|
|
138
169
|
readonly code: number;
|
|
139
170
|
readonly stdout: string;
|
|
140
171
|
readonly stderr: string;
|
package/dist/lib/power.js
CHANGED
|
@@ -3,8 +3,62 @@ import { unsupportedPlatform } from "./outcome";
|
|
|
3
3
|
/** The ceiling, and the reason there is one. Eight hours is a working day. */
|
|
4
4
|
export const MAX_HOLD_MINUTES = 480;
|
|
5
5
|
export const DEFAULT_HOLD_MINUTES = 30;
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Where 0.7.0 remembered the held assertion, relative to the workspace root.
|
|
8
|
+
*
|
|
9
|
+
* NOT READ ANY MORE, only reported. A file in the workspace can be written by
|
|
10
|
+
* the agent the tool serves (Write, CopyPath, an extracted archive), and the
|
|
11
|
+
* record names the pid `release` signals: a planted record made `release`
|
|
12
|
+
* SIGTERM any process of the operator's (security-10#1). The record now lives
|
|
13
|
+
* in a private per-user directory — see `powerStateFile` in ../index.ts.
|
|
14
|
+
*/
|
|
15
|
+
export const LEGACY_STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
|
|
16
|
+
/**
|
|
17
|
+
* The program this package's holder runs on `platform` — the ONE source for
|
|
18
|
+
* both `planHold` and the identity check `release` makes before signalling.
|
|
19
|
+
* The check used to compare against the `marker` field of the state record,
|
|
20
|
+
* which is the same record that supplies the pid, so whoever wrote the record
|
|
21
|
+
* chose what the live process was checked against (security-10#1).
|
|
22
|
+
*/
|
|
23
|
+
export function holderMarker(platform) {
|
|
24
|
+
switch (platform) {
|
|
25
|
+
case "darwin":
|
|
26
|
+
return DARWIN_HOLDER;
|
|
27
|
+
case "linux":
|
|
28
|
+
return LINUX_HOLDER;
|
|
29
|
+
case "win32":
|
|
30
|
+
return WINDOWS_HOLDER_TYPE;
|
|
31
|
+
default:
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const DARWIN_HOLDER = "caffeinate";
|
|
36
|
+
const LINUX_HOLDER = "systemd-inhibit";
|
|
37
|
+
/** The type name the Windows holder script defines; see WINDOWS_INHIBIT. */
|
|
38
|
+
const WINDOWS_HOLDER_TYPE = "CHPwr";
|
|
39
|
+
/**
|
|
40
|
+
* Is `commandLine` a holder THIS package starts on `platform`?
|
|
41
|
+
*
|
|
42
|
+
* The program must be the platform's holder, and the arguments must carry
|
|
43
|
+
* what `planHold` always gives it: `-t <seconds>` for caffeinate,
|
|
44
|
+
* `--who=crewhaus` for systemd-inhibit, and the generated type name inside a
|
|
45
|
+
* powershell host. An operator's own `caffeinate -d` is therefore never
|
|
46
|
+
* taken for ours.
|
|
47
|
+
*/
|
|
48
|
+
export function isHolderCommand(platform, commandLine) {
|
|
49
|
+
const marker = holderMarker(platform);
|
|
50
|
+
if (marker === undefined || !commandMatchesMarker(commandLine, marker))
|
|
51
|
+
return false;
|
|
52
|
+
const tokens = commandLine.trim().split(/\s+/);
|
|
53
|
+
switch (platform) {
|
|
54
|
+
case "darwin":
|
|
55
|
+
return tokens.includes("-t");
|
|
56
|
+
case "linux":
|
|
57
|
+
return tokens.includes("--who=crewhaus");
|
|
58
|
+
default:
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
8
62
|
export const WINDOWS_INHIBIT = registerPowerShellScript({
|
|
9
63
|
name: "power-inhibit",
|
|
10
64
|
// DOCUMENTED, NOT RECORDED. The flags are ES_CONTINUOUS (0x80000000) |
|
|
@@ -83,7 +137,7 @@ export function planHold(platform, scope, seconds, reason) {
|
|
|
83
137
|
if (scope === "display")
|
|
84
138
|
argv.push("-d");
|
|
85
139
|
argv.push("-t", String(seconds));
|
|
86
|
-
return { ok: true, backend: "caffeinate", marker:
|
|
140
|
+
return { ok: true, backend: "caffeinate", marker: DARWIN_HOLDER, request: { argv } };
|
|
87
141
|
}
|
|
88
142
|
case "linux": {
|
|
89
143
|
const what = scope === "display" ? "idle:sleep:handle-lid-switch" : "idle:sleep";
|
|
@@ -101,7 +155,7 @@ export function planHold(platform, scope, seconds, reason) {
|
|
|
101
155
|
return {
|
|
102
156
|
ok: true,
|
|
103
157
|
backend: "systemd-inhibit",
|
|
104
|
-
marker:
|
|
158
|
+
marker: LINUX_HOLDER,
|
|
105
159
|
request: { argv },
|
|
106
160
|
};
|
|
107
161
|
}
|
|
@@ -114,7 +168,7 @@ export function planHold(platform, scope, seconds, reason) {
|
|
|
114
168
|
return {
|
|
115
169
|
ok: true,
|
|
116
170
|
backend: "powershell SetThreadExecutionState",
|
|
117
|
-
marker:
|
|
171
|
+
marker: WINDOWS_HOLDER_TYPE,
|
|
118
172
|
request: { argv: built.argv, env: built.env },
|
|
119
173
|
};
|
|
120
174
|
}
|
|
@@ -184,7 +238,9 @@ export function commandMatchesMarker(commandLine, marker) {
|
|
|
184
238
|
return true;
|
|
185
239
|
return /^powershell$/i.test(base) && commandLine.includes(marker);
|
|
186
240
|
}
|
|
187
|
-
export function classifyLiveness(
|
|
241
|
+
export function classifyLiveness(
|
|
242
|
+
/** The holder's program name, or a predicate over the live command line. */
|
|
243
|
+
holder, result) {
|
|
188
244
|
if (result.refused === true || result.missing || result.timedOut)
|
|
189
245
|
return "unknown";
|
|
190
246
|
// `ps -p <pid>` exits 1 with no output when nothing has that pid. That is a
|
|
@@ -208,7 +264,8 @@ export function classifyLiveness(marker, result) {
|
|
|
208
264
|
const line = result.stdout.trim();
|
|
209
265
|
if (line === "")
|
|
210
266
|
return "gone";
|
|
211
|
-
|
|
267
|
+
const ours = typeof holder === "string" ? commandMatchesMarker(line, holder) : holder(line);
|
|
268
|
+
return ours ? "alive" : "reused";
|
|
212
269
|
}
|
|
213
270
|
/** Minutes to a bounded second count, or a refusal naming the ceiling. */
|
|
214
271
|
export function boundedSeconds(minutes) {
|
package/dist/lib/presence.js
CHANGED
|
@@ -76,7 +76,12 @@ export const WINDOWS_PRESENCE = registerPowerShellScript({
|
|
|
76
76
|
script: [
|
|
77
77
|
`$src = '${WINDOWS_IDLE_SOURCE}'`,
|
|
78
78
|
"try { Add-Type -TypeDefinition $src -ErrorAction Stop; [Console]::Out.WriteLine('idleMs=' + [CHIdle]::Ms()) } catch { [Console]::Out.WriteLine('idleMs=?') }",
|
|
79
|
-
|
|
79
|
+
// THIS session's LogonUI only: Get-Process lists every session's, so on
|
|
80
|
+
// a host with another signed-in (or RDP) user, their lock screen read as
|
|
81
|
+
// this user being locked out — the direction that sends a workflow off
|
|
82
|
+
// to act unattended.
|
|
83
|
+
"$sid = [System.Diagnostics.Process]::GetCurrentProcess().SessionId",
|
|
84
|
+
"$lock = @(Get-Process -Name LogonUI -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $sid }).Count",
|
|
80
85
|
"[Console]::Out.WriteLine('locked=' + $(if ($lock -gt 0) { 'yes' } else { 'no' }))",
|
|
81
86
|
].join("; "),
|
|
82
87
|
reads: [],
|
|
@@ -218,6 +223,8 @@ export function parseLoginctl(stdout) {
|
|
|
218
223
|
}
|
|
219
224
|
/** Parse the two lines the Windows script prints. */
|
|
220
225
|
export function parseWindowsPresence(stdout) {
|
|
226
|
+
// `[Console]::Out.WriteLine` ends each line with CRLF on Windows; `$`
|
|
227
|
+
// under `m` matches before `\r` as well as `\n`, so both parse.
|
|
221
228
|
const idle = /^idleMs=(\d+)$/m.exec(stdout);
|
|
222
229
|
const locked = /^locked=(yes|no)$/m.exec(stdout);
|
|
223
230
|
return {
|
package/dist/lib/print.d.ts
CHANGED
|
@@ -71,6 +71,14 @@ export type QueueFacts = {
|
|
|
71
71
|
* parser that breaks on the next one.
|
|
72
72
|
*/
|
|
73
73
|
export declare function parseQueues(stdout: string, stderr: string): QueueFacts;
|
|
74
|
+
/**
|
|
75
|
+
* Parse the Windows printer probe's JSON: `{"printers":[{"name","status"}],
|
|
76
|
+
* "default":<name>|null}`. Anything else — no stdout, a PowerShell error, a
|
|
77
|
+
* shape this parser does not know — is UNREADABLE, never "no printers".
|
|
78
|
+
* A name is taken whole, spaces, commas and a `\\server\queue` share
|
|
79
|
+
* included.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseWindowsQueues(stdout: string, stderr: string): QueueFacts;
|
|
74
82
|
/** The probe could not answer. Distinct from "it answered, there are none". */
|
|
75
83
|
export declare function unreadableQueues(reason: string): QueueFacts;
|
|
76
84
|
export type PrintOptions = {
|
package/dist/lib/print.js
CHANGED
|
@@ -20,9 +20,17 @@ export function planPrinterProbe(platform) {
|
|
|
20
20
|
}
|
|
21
21
|
export const WINDOWS_PRINTER_LIST = registerPowerShellScript({
|
|
22
22
|
name: "printer-list",
|
|
23
|
-
// DOCUMENTED, NOT RECORDED
|
|
24
|
-
//
|
|
25
|
-
|
|
23
|
+
// DOCUMENTED, NOT RECORDED on a desktop — `Get-Printer` is in the
|
|
24
|
+
// PrintManagement module, present on client Windows since 8.1; the
|
|
25
|
+
// windows-tools CI job logs what a Windows runner prints (see
|
|
26
|
+
// windows-recording.test.ts). JSON, not prose: the 0.7.0 script printed
|
|
27
|
+
// `printer <Name> is <Status>` into the lpstat parser, whose `\S+` cut every
|
|
28
|
+
// Windows name with a space at the first one — every Windows box ships
|
|
29
|
+
// "Microsoft Print to PDF" (reliability#7). Names and statuses are cast to
|
|
30
|
+
// strings (an enum would serialise as a number), the list is forced to an
|
|
31
|
+
// array (ConvertTo-Json unwraps a one-element pipeline), and a failure goes
|
|
32
|
+
// to stderr with nothing on stdout, so it can never read as "no printers".
|
|
33
|
+
script: "try { $rows = @(Get-Printer -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ name = [string]$_.Name; status = [string]$_.PrinterStatus } }); $def = $null; try { $def = [string](Get-CimInstance -ClassName Win32_Printer -Filter 'Default=TRUE' -ErrorAction Stop | Select-Object -First 1 -ExpandProperty Name) } catch { $def = $null }; [Console]::Out.Write((ConvertTo-Json -InputObject ([pscustomobject]@{ printers = $rows; default = $def }) -Compress -Depth 3)) } catch { [Console]::Error.WriteLine($_.Exception.Message) }",
|
|
26
34
|
reads: [],
|
|
27
35
|
});
|
|
28
36
|
export const WINDOWS_PRINT_TEXT = registerPowerShellScript({
|
|
@@ -75,6 +83,58 @@ export function parseQueues(stdout, stderr) {
|
|
|
75
83
|
}
|
|
76
84
|
return { printers, defaultPrinter, schedulerDown: false, unreadable: null };
|
|
77
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse the Windows printer probe's JSON: `{"printers":[{"name","status"}],
|
|
88
|
+
* "default":<name>|null}`. Anything else — no stdout, a PowerShell error, a
|
|
89
|
+
* shape this parser does not know — is UNREADABLE, never "no printers".
|
|
90
|
+
* A name is taken whole, spaces, commas and a `\\server\queue` share
|
|
91
|
+
* included.
|
|
92
|
+
*/
|
|
93
|
+
export function parseWindowsQueues(stdout, stderr) {
|
|
94
|
+
const text = stdout.trim();
|
|
95
|
+
const why = stderr.trim();
|
|
96
|
+
const unreadable = (detail) => unreadableQueues(`the Windows printer probe ${detail}${why === "" ? "" : `: ${why.slice(0, 300)}`}, so whether this host has printers is unknown — it is NOT that it has none`);
|
|
97
|
+
if (text === "")
|
|
98
|
+
return unreadable("printed nothing");
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(text);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return unreadable("did not print the JSON it was written to print");
|
|
105
|
+
}
|
|
106
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
107
|
+
return unreadable("printed an unknown shape");
|
|
108
|
+
const record = parsed;
|
|
109
|
+
const rawList = record["printers"];
|
|
110
|
+
// ConvertTo-Json unwraps a one-element array under some hosts; accept both.
|
|
111
|
+
const list = Array.isArray(rawList)
|
|
112
|
+
? rawList
|
|
113
|
+
: typeof rawList === "object" && rawList !== null
|
|
114
|
+
? [rawList]
|
|
115
|
+
: rawList === null || rawList === undefined
|
|
116
|
+
? []
|
|
117
|
+
: undefined;
|
|
118
|
+
if (list === undefined)
|
|
119
|
+
return unreadable("printed an unknown shape");
|
|
120
|
+
const printers = [];
|
|
121
|
+
for (const row of list) {
|
|
122
|
+
if (typeof row !== "object" || row === null)
|
|
123
|
+
return unreadable("printed an unknown row");
|
|
124
|
+
const name = row["name"];
|
|
125
|
+
const status = row["status"];
|
|
126
|
+
if (typeof name !== "string" || name === "")
|
|
127
|
+
return unreadable("printed a row with no name");
|
|
128
|
+
printers.push({ name, state: typeof status === "string" ? status : "" });
|
|
129
|
+
}
|
|
130
|
+
const dflt = record["default"];
|
|
131
|
+
return {
|
|
132
|
+
printers,
|
|
133
|
+
defaultPrinter: typeof dflt === "string" && dflt !== "" ? dflt : null,
|
|
134
|
+
schedulerDown: false,
|
|
135
|
+
unreadable: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
78
138
|
/** The probe could not answer. Distinct from "it answered, there are none". */
|
|
79
139
|
export function unreadableQueues(reason) {
|
|
80
140
|
return { printers: [], defaultPrinter: null, schedulerDown: false, unreadable: reason };
|
package/dist/lib/windows.js
CHANGED
|
@@ -95,8 +95,11 @@ export const WINDOWS_WINDOW_LIST = registerPowerShellScript({
|
|
|
95
95
|
// window per process and no geometry; real bounds need EnumWindows +
|
|
96
96
|
// GetWindowRect through a P/Invoke shim, which is a compile step this
|
|
97
97
|
// package will not take (house rule 8). So the Windows rows report bounds
|
|
98
|
-
// as UNKNOWN rather than as zeros, and say why.
|
|
99
|
-
|
|
98
|
+
// as UNKNOWN rather than as zeros, and say why. Focus too: the 0.7.0 script
|
|
99
|
+
// printed a literal `false` for every window, a definite "nothing has
|
|
100
|
+
// focus" from a probe that never asked (GetForegroundWindow is the same
|
|
101
|
+
// P/Invoke step), so focused is `?` → null like the bounds.
|
|
102
|
+
script: "$fs = [char]31; $rs = [char]30; Get-Process | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object { [Console]::Out.Write($_.ProcessName + $fs + $_.MainWindowTitle + $fs + '?' + $fs + '?' + $fs + '?' + $fs + '?' + $fs + '?' + $fs + '?' + $rs) }",
|
|
100
103
|
reads: [],
|
|
101
104
|
});
|
|
102
105
|
export function planWindowList(platform, env) {
|
package/dist/paths.d.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { CrewhausError } from "@crewhaus/errors";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The longest path this resolves, in UTF-16 code units; a longer one is
|
|
4
|
+
* refused before it is walked. Nothing that long can be opened or created:
|
|
5
|
+
* PATH_MAX is 1,024 bytes on macOS and 4,096 on Linux, Windows stops at
|
|
6
|
+
* 32,767 UTF-16 units, and a path's UTF-8 form is never shorter than its
|
|
7
|
+
* UTF-16 one. The limit is twice that because the part of a path that exists
|
|
8
|
+
* is itself under PATH_MAX, so the part that does not is over PATH_MAX on its
|
|
9
|
+
* own: no spelling of the place reaches it, not even one through a symlink
|
|
10
|
+
* that shortens the part that exists.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_PATH_CHARS: number;
|
|
13
|
+
/**
|
|
14
|
+
* Raised when a caller-supplied path resolves outside the workspace root, or
|
|
15
|
+
* is longer than any path can be.
|
|
16
|
+
*/
|
|
3
17
|
export declare class ToolPermissionError extends CrewhausError {
|
|
4
18
|
readonly name = "ToolPermissionError";
|
|
5
19
|
readonly toolName: string;
|
|
6
20
|
readonly path: string;
|
|
7
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Why, when it is not that the location escapes the workspace root: a path
|
|
23
|
+
* longer than any path can be. A caller that rewords the refusal says this.
|
|
24
|
+
*/
|
|
25
|
+
readonly reason: string | undefined;
|
|
26
|
+
constructor(toolName: string, attemptedPath: string, reason?: string);
|
|
8
27
|
}
|
|
9
28
|
/**
|
|
10
29
|
* A validated path, in both forms the tools need.
|