@amalgm/shell 0.1.163 → 0.1.164-main.34182745507
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/PURPOSE.md +68 -6
- package/README.md +58 -11
- package/config/releases/trust/runtime/main.root.json +84 -0
- package/config/releases/trust/runtime/preview.root.json +84 -0
- package/dist/boot.js +28 -20
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/content-cache-index.js +2 -2
- package/dist/content-cache-index.js.map +1 -1
- package/dist/detection/runtime.js +2 -2
- package/dist/detection/runtime.js.map +1 -1
- package/dist/entity-apply-host.js +2 -2
- package/dist/entity-apply-host.js.map +1 -1
- package/dist/entity-apply-store.d.ts +2 -1
- package/dist/entity-apply-store.js +52 -37
- package/dist/entity-apply-store.js.map +1 -1
- package/dist/installation/lease.js +2 -2
- package/dist/installation/lease.js.map +1 -1
- package/dist/installation/ownership.js +2 -2
- package/dist/installation/ownership.js.map +1 -1
- package/dist/machine-host.d.ts +1 -0
- package/dist/machine-host.js +56 -31
- package/dist/machine-host.js.map +1 -1
- package/dist/process-lifecycle.d.ts +1 -3
- package/dist/process-lifecycle.js +10 -0
- package/dist/process-lifecycle.js.map +1 -1
- package/dist/pty-host.d.ts +1 -1
- package/dist/pty-host.js +70 -21
- package/dist/pty-host.js.map +1 -1
- package/dist/service/cli.js +50 -9
- package/dist/service/cli.js.map +1 -1
- package/dist/service/control.d.ts +10 -1
- package/dist/service/control.js +1 -1
- package/dist/service/control.js.map +1 -1
- package/dist/service/keepalive.d.ts +10 -3
- package/dist/service/keepalive.js +267 -29
- package/dist/service/keepalive.js.map +1 -1
- package/dist/service/recovery.d.ts +27 -0
- package/dist/service/recovery.js +58 -0
- package/dist/service/recovery.js.map +1 -0
- package/dist/service/systemd-unit.d.ts +7 -0
- package/dist/service/systemd-unit.js +43 -0
- package/dist/service/systemd-unit.js.map +1 -0
- package/dist/service/systemd.d.ts +1 -5
- package/dist/service/systemd.js +9 -31
- package/dist/service/systemd.js.map +1 -1
- package/dist/service/windows-action.d.ts +9 -0
- package/dist/service/windows-action.js +55 -0
- package/dist/service/windows-action.js.map +1 -0
- package/dist/service/windows-task.d.ts +1 -8
- package/dist/service/windows-task.js +4 -40
- package/dist/service/windows-task.js.map +1 -1
- package/dist/service/windows.js +3 -3
- package/dist/service/windows.js.map +1 -1
- package/dist/sqlite.d.ts +4 -0
- package/dist/sqlite.js +16 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/updates/artifacts.d.ts +40 -0
- package/dist/updates/artifacts.js +299 -0
- package/dist/updates/artifacts.js.map +1 -0
- package/dist/updates/bootstrap.d.ts +48 -0
- package/dist/updates/bootstrap.js +384 -0
- package/dist/updates/bootstrap.js.map +1 -0
- package/dist/updates/controller.d.ts +66 -0
- package/dist/updates/controller.js +322 -0
- package/dist/updates/controller.js.map +1 -0
- package/dist/updates/isolated-command-lifetime.d.ts +1 -0
- package/dist/updates/isolated-command-lifetime.js +63 -0
- package/dist/updates/isolated-command-lifetime.js.map +1 -0
- package/dist/updates/store.d.ts +41 -0
- package/dist/updates/store.js +80 -0
- package/dist/updates/store.js.map +1 -0
- package/dist/updates/trust.d.ts +20 -0
- package/dist/updates/trust.js +138 -0
- package/dist/updates/trust.js.map +1 -0
- package/dist/user-ground-host.d.ts +1 -1
- package/dist/user-ground-host.js +3 -3
- package/dist/user-ground-host.js.map +1 -1
- package/dist/watching/host.js +4 -2
- package/dist/watching/host.js.map +1 -1
- package/dist/workspace-add-staging.js +2 -2
- package/dist/workspace-add-staging.js.map +1 -1
- package/examinations/fixtures/machine-control-plane.js +57 -0
- package/examinations/fixtures/private-wire-authority.js +1065 -0
- package/examinations/native-service.mjs +92 -0
- package/examinations/network-view.mjs +202 -0
- package/examinations/runtime-upgrade.native-test.mjs +198 -0
- package/npm-shrinkwrap.json +2104 -191
- package/package.json +94 -13
package/dist/service/systemd.js
CHANGED
|
@@ -6,34 +6,11 @@ import { stopKeepalive } from "./control.js";
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, rmSync } from "node:fs";
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
|
-
import {
|
|
9
|
+
import { systemdUnavailableOutput, systemdUnitPath } from "@amalgm/core/supervision";
|
|
10
10
|
import { atomicWrite, ensureUserDir } from "../filesystem.js";
|
|
11
11
|
import { keepaliveCommand, osCommand, serviceEnvironment } from "./adapter.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const TIMEOUT_STOP_SEC = 30;
|
|
15
|
-
export function renderSystemdUnit(input) {
|
|
16
|
-
const env = Object.entries(input.environment)
|
|
17
|
-
.map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`)
|
|
18
|
-
.join("\n");
|
|
19
|
-
return `[Unit]
|
|
20
|
-
Description=Amalgm always-on runtime
|
|
21
|
-
StartLimitIntervalSec=0
|
|
22
|
-
|
|
23
|
-
[Service]
|
|
24
|
-
Type=exec
|
|
25
|
-
WorkingDirectory=${systemdQuote(input.workingDirectory)}
|
|
26
|
-
${env}
|
|
27
|
-
ExecStart=${input.command.map(systemdQuote).join(" ")}
|
|
28
|
-
Restart=on-failure
|
|
29
|
-
RestartSec=${RESTART_SEC}
|
|
30
|
-
KillMode=mixed
|
|
31
|
-
TimeoutStopSec=${TIMEOUT_STOP_SEC}
|
|
32
|
-
|
|
33
|
-
[Install]
|
|
34
|
-
WantedBy=default.target
|
|
35
|
-
`;
|
|
36
|
-
}
|
|
12
|
+
import { renderSystemdUnit } from './systemd-unit.js';
|
|
13
|
+
export { renderSystemdUnit } from './systemd-unit.js';
|
|
37
14
|
/** Does this login have a user manager to talk to? Containers and bare SSH often do not. */
|
|
38
15
|
export function userSystemdAvailable() {
|
|
39
16
|
const probe = osCommand("systemctl", ["--user", "show-environment"], { tolerate: true });
|
|
@@ -49,14 +26,15 @@ export const systemdOwner = {
|
|
|
49
26
|
backend: "systemd",
|
|
50
27
|
files: (identity) => ({ unit: unit(identity) }),
|
|
51
28
|
async install(identity, launcher, keepaliveArgs) {
|
|
52
|
-
|
|
53
|
-
const file = unit(identity);
|
|
54
|
-
ensureUserDir(dirname(file));
|
|
55
|
-
atomicWrite(file, renderSystemdUnit({
|
|
29
|
+
const description = renderSystemdUnit({
|
|
56
30
|
command: keepaliveCommand(launcher, keepaliveArgs),
|
|
57
31
|
environment: serviceEnvironment(identity),
|
|
58
32
|
workingDirectory: identity.home,
|
|
59
|
-
})
|
|
33
|
+
});
|
|
34
|
+
await stopKeepalive(identity);
|
|
35
|
+
const file = unit(identity);
|
|
36
|
+
ensureUserDir(dirname(file));
|
|
37
|
+
atomicWrite(file, description, 0o644);
|
|
60
38
|
systemctl(["daemon-reload"]);
|
|
61
39
|
// A selected data home need not be in the OS account's unit search path.
|
|
62
40
|
// Enabling the absolute file also links it into that account's user manager.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../src/service/systemd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../src/service/systemd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ChildRestartPolicy } from '@amalgm/core/supervision';
|
|
2
|
+
export declare const encodePowerShell: (source: string) => string;
|
|
3
|
+
export declare function jsonExpression(value: unknown): string;
|
|
4
|
+
export declare function powershellExecutable(env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
/** Microsoft argv quoting goes directly to ProcessStartInfo, without a shell. */
|
|
6
|
+
export declare function quoteWindowsArgument(value: string): string;
|
|
7
|
+
/** Scheduler owns this action's logged-in lifetime. Its launch-retry setting
|
|
8
|
+
* cannot observe controller exits, so this native port applies Core's policy. */
|
|
9
|
+
export declare function windowsControllerAction(command: readonly string[], workingDirectory: string, restartPolicy?: ChildRestartPolicy): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { win32 } from 'node:path';
|
|
2
|
+
import { CHILD_RESTART_POLICY } from '@amalgm/core/supervision';
|
|
3
|
+
export const encodePowerShell = (source) => Buffer.from(source, 'utf16le').toString('base64');
|
|
4
|
+
export function jsonExpression(value) {
|
|
5
|
+
return `ConvertFrom-Json ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${Buffer.from(JSON.stringify(value), 'utf8').toString('base64')}')))`;
|
|
6
|
+
}
|
|
7
|
+
export function powershellExecutable(env = process.env) {
|
|
8
|
+
return win32.join(env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
9
|
+
}
|
|
10
|
+
/** Microsoft argv quoting goes directly to ProcessStartInfo, without a shell. */
|
|
11
|
+
export function quoteWindowsArgument(value) {
|
|
12
|
+
if (value.includes('\0'))
|
|
13
|
+
throw new Error('A process argument cannot contain NUL.');
|
|
14
|
+
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
15
|
+
}
|
|
16
|
+
/** Scheduler owns this action's logged-in lifetime. Its launch-retry setting
|
|
17
|
+
* cannot observe controller exits, so this native port applies Core's policy. */
|
|
18
|
+
export function windowsControllerAction(command, workingDirectory, restartPolicy = CHILD_RESTART_POLICY) {
|
|
19
|
+
if (!command.length || !command[0])
|
|
20
|
+
throw new Error('The controller needs an executable.');
|
|
21
|
+
return encodePowerShell(`
|
|
22
|
+
$ErrorActionPreference = 'Stop'
|
|
23
|
+
$launch = ${jsonExpression({ executable: command[0], arguments: command.slice(1).map(quoteWindowsArgument).join(' '), workingDirectory })}
|
|
24
|
+
$policy = ${jsonExpression(restartPolicy)}
|
|
25
|
+
$start = New-Object System.Diagnostics.ProcessStartInfo
|
|
26
|
+
$start.FileName = $launch.executable
|
|
27
|
+
$start.Arguments = $launch.arguments
|
|
28
|
+
$start.WorkingDirectory = $launch.workingDirectory
|
|
29
|
+
$start.UseShellExecute = $false
|
|
30
|
+
$start.CreateNoWindow = $true
|
|
31
|
+
$clock = [Diagnostics.Stopwatch]::StartNew()
|
|
32
|
+
$history = @()
|
|
33
|
+
$delay = $policy.initialDelayMs
|
|
34
|
+
while ($true) {
|
|
35
|
+
$started = $clock.ElapsedMilliseconds
|
|
36
|
+
$runtimeProcess = $null
|
|
37
|
+
$result = 1
|
|
38
|
+
try {
|
|
39
|
+
$runtimeProcess = [System.Diagnostics.Process]::Start($start)
|
|
40
|
+
$runtimeProcess.WaitForExit()
|
|
41
|
+
$result = $runtimeProcess.ExitCode
|
|
42
|
+
} catch { [Console]::Error.WriteLine($_.Exception.Message) }
|
|
43
|
+
finally { if ($runtimeProcess) { $runtimeProcess.Dispose() } }
|
|
44
|
+
if ($result -eq 0) { exit 0 }
|
|
45
|
+
$now = $clock.ElapsedMilliseconds
|
|
46
|
+
if ($now - $started -ge $policy.stableAfterMs) { $delay = $policy.initialDelayMs; $history = @() }
|
|
47
|
+
$history = @($history | Where-Object { $now - $_ -le $policy.windowMs })
|
|
48
|
+
if ($history.Count -ge $policy.maxRestarts) { exit $result }
|
|
49
|
+
$history += $now
|
|
50
|
+
Start-Sleep -Milliseconds $delay
|
|
51
|
+
$delay = [Math]::Min($delay * 2, $policy.maxDelayMs)
|
|
52
|
+
}
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=windows-action.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows-action.js","sourceRoot":"","sources":["../../src/service/windows-action.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAA2B,MAAM,0BAA0B,CAAC;AAEzF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC9G,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,kFAAkF,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC/J,CAAC;AACD,MAAM,UAAU,oBAAoB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACvE,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,aAAa,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAChH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACpF,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC;AAC9E,CAAC;AAED;iFACiF;AACjF,MAAM,UAAU,uBAAuB,CAAC,OAA0B,EAAE,gBAAwB,EAC1F,gBAAoC,oBAAoB;IACxD,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAC3F,OAAO,gBAAgB,CAAC;;YAEd,cAAc,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,gBAAgB,EAAE,CAAC;YAC7H,cAAc,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BxC,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
/** Microsoft command-line quoting, applied directly to ProcessStartInfo.
|
|
3
|
-
* No cmd.exe, environment expansion, or shell interpretation is involved. */
|
|
4
|
-
export declare function quoteWindowsArgument(value: string): string;
|
|
5
|
-
export declare function powershellExecutable(env?: NodeJS.ProcessEnv): string;
|
|
6
|
-
/** The scheduler must observe the controller's real lifetime and exit code.
|
|
7
|
-
* A hidden, waiting host wrapper does not make any recovery decisions. */
|
|
8
|
-
export declare function windowsControllerAction(command: readonly string[], workingDirectory: string): string;
|
|
1
|
+
export { encodePowerShell, powershellExecutable, quoteWindowsArgument, windowsControllerAction } from './windows-action.js';
|
|
9
2
|
export type WindowsTaskOperation = 'install' | 'start' | 'disable' | 'stop' | 'status' | 'uninstall';
|
|
10
3
|
/** All interpolated input is encoded JSON data. Task Scheduler runs under the
|
|
11
4
|
* current account's interactive token and never stores an account password. */
|
|
@@ -1,49 +1,13 @@
|
|
|
1
|
-
import { win32 } from 'node:path';
|
|
2
1
|
import { CHILD_RESTART_POLICY } from '@amalgm/core/supervision';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
/** Microsoft command-line quoting, applied directly to ProcessStartInfo.
|
|
7
|
-
* No cmd.exe, environment expansion, or shell interpretation is involved. */
|
|
8
|
-
export function quoteWindowsArgument(value) {
|
|
9
|
-
if (value.includes('\0'))
|
|
10
|
-
throw new Error('A process argument cannot contain NUL.');
|
|
11
|
-
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
12
|
-
}
|
|
13
|
-
export function powershellExecutable(env = process.env) {
|
|
14
|
-
return win32.join(env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
15
|
-
}
|
|
16
|
-
function jsonExpression(value) {
|
|
17
|
-
const encoded = Buffer.from(JSON.stringify(value), 'utf8').toString('base64');
|
|
18
|
-
return `ConvertFrom-Json ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')))`;
|
|
19
|
-
}
|
|
20
|
-
/** The scheduler must observe the controller's real lifetime and exit code.
|
|
21
|
-
* A hidden, waiting host wrapper does not make any recovery decisions. */
|
|
22
|
-
export function windowsControllerAction(command, workingDirectory) {
|
|
23
|
-
if (!command.length || !command[0])
|
|
24
|
-
throw new Error('The controller needs an executable.');
|
|
25
|
-
return encodePowerShell(`
|
|
26
|
-
$ErrorActionPreference = 'Stop'
|
|
27
|
-
$launch = ${jsonExpression({ executable: command[0], arguments: command.slice(1).map(quoteWindowsArgument).join(' '), workingDirectory })}
|
|
28
|
-
$start = New-Object System.Diagnostics.ProcessStartInfo
|
|
29
|
-
$start.FileName = $launch.executable
|
|
30
|
-
$start.Arguments = $launch.arguments
|
|
31
|
-
$start.WorkingDirectory = $launch.workingDirectory
|
|
32
|
-
$start.UseShellExecute = $false
|
|
33
|
-
$start.CreateNoWindow = $true
|
|
34
|
-
$runtimeProcess = [System.Diagnostics.Process]::Start($start)
|
|
35
|
-
try {
|
|
36
|
-
$runtimeProcess.WaitForExit()
|
|
37
|
-
$result = $runtimeProcess.ExitCode
|
|
38
|
-
} finally { $runtimeProcess.Dispose() }
|
|
39
|
-
exit $result
|
|
40
|
-
`);
|
|
41
|
-
}
|
|
2
|
+
import { encodePowerShell, jsonExpression, powershellExecutable } from './windows-action.js';
|
|
3
|
+
export { encodePowerShell, powershellExecutable, quoteWindowsArgument, windowsControllerAction } from './windows-action.js';
|
|
42
4
|
/** All interpolated input is encoded JSON data. Task Scheduler runs under the
|
|
43
5
|
* current account's interactive token and never stores an account password. */
|
|
44
6
|
export function windowsTaskOperation(input) {
|
|
45
7
|
const request = { ...input, executable: powershellExecutable(),
|
|
46
8
|
restartCount: CHILD_RESTART_POLICY.maxRestarts,
|
|
9
|
+
// MS-TSCH 2.5.4.2: these retries cover failure to start an action, not its exit.
|
|
10
|
+
// https://learn.microsoft.com/openspecs/windows_protocols/ms-tsch/2ff4aa5a-7bc4-449f-bbb1-27475645867f
|
|
47
11
|
// Task Scheduler's supported minimum is one minute.
|
|
48
12
|
// https://learn.microsoft.com/windows/win32/taskschd/taskschedulerschema-restarttype-complextype
|
|
49
13
|
restartIntervalMs: Math.max(60_000, CHILD_RESTART_POLICY.initialDelayMs) };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windows-task.js","sourceRoot":"","sources":["../../src/service/windows-task.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"windows-task.js","sourceRoot":"","sources":["../../src/service/windows-task.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAI5H;+EAC+E;AAC/E,MAAM,UAAU,oBAAoB,CAAC,KAIpC;IACC,MAAM,OAAO,GAAG,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE;QAC5D,YAAY,EAAE,oBAAoB,CAAC,WAAW;QAC9C,iFAAiF;QACjF,uGAAuG;QACvG,oDAAoD;QACpD,iGAAiG;QACjG,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,cAAc,CAAC,EAAE,CAAC;IAC7E,OAAO,gBAAgB,CAAC;;aAEb,cAAc,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BnC,CAAC,CAAC;AACH,CAAC"}
|
package/dist/service/windows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Windows Task Scheduler owns a hidden
|
|
2
|
-
* logged-in lifetime
|
|
1
|
+
/** Windows Task Scheduler owns a hidden action in the current account's
|
|
2
|
+
* logged-in lifetime; that action applies Core's controller recovery policy. */
|
|
3
3
|
import { existsSync, rmSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { windowsStartupScriptPath } from '@amalgm/core/supervision';
|
|
@@ -49,7 +49,7 @@ export const windowsOwner = {
|
|
|
49
49
|
return {
|
|
50
50
|
registered: native.registered && existsSync(definition(identity)), loaded: native.registered,
|
|
51
51
|
state: controller?.alive ? 'running' : native.state?.toLowerCase() || 'not registered',
|
|
52
|
-
pid: controller?.alive ? controller.pid : null,
|
|
52
|
+
pid: controller?.alive ? controller.bootstrapPid ?? controller.pid : null,
|
|
53
53
|
...(native.lastResult ? { detail: `Task Scheduler result: ${native.lastResult}` } : {}),
|
|
54
54
|
};
|
|
55
55
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windows.js","sourceRoot":"","sources":["../../src/service/windows.ts"],"names":[],"mappings":"AAAA;
|
|
1
|
+
{"version":3,"file":"windows.js","sourceRoot":"","sources":["../../src/service/windows.ts"],"names":[],"mappings":"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"}
|
package/dist/sqlite.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
/** Every persistent SQLite handle names the same OS file as Node's filesystem.
|
|
3
|
+
* The Windows namespace prefix bypasses legacy Win32 MAX_PATH interpretation. */
|
|
4
|
+
export declare function openFileDatabase(file: string, options?: Database.Options): Database.Database;
|
package/dist/sqlite.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { resolve, toNamespacedPath } from 'node:path';
|
|
3
|
+
/** Every persistent SQLite handle names the same OS file as Node's filesystem.
|
|
4
|
+
* The Windows namespace prefix bypasses legacy Win32 MAX_PATH interpretation. */
|
|
5
|
+
export function openFileDatabase(file, options) {
|
|
6
|
+
const filename = toNamespacedPath(resolve(file));
|
|
7
|
+
try {
|
|
8
|
+
return new Database(filename, options);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error instanceof Error)
|
|
12
|
+
error.message = `SQLite ${JSON.stringify(file)}: ${error.message}`;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAEtD;iFACiF;AACjF,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,OAA0B;IACvE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACjD,IAAI,CAAC;QAAC,OAAO,IAAI,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAAC,CAAC;IAC/C,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,KAAK;YAAE,KAAK,CAAC,OAAO,GAAG,UAAU,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;QAC/F,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AcceptedRuntimeRelease } from '@amalgm/core/supervision';
|
|
2
|
+
import type { ServiceIdentity } from '../service/registration.js';
|
|
3
|
+
export declare class RuntimeDownloadError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export declare class RuntimeCommandError extends Error {
|
|
6
|
+
readonly stdout: string;
|
|
7
|
+
readonly exitCode: number | null;
|
|
8
|
+
constructor(message: string, stdout: string, exitCode: number | null);
|
|
9
|
+
}
|
|
10
|
+
/** Only npm's structured top-level error classifies dependency transport.
|
|
11
|
+
* Script diagnostics, integrity failures and preflight output never do. */
|
|
12
|
+
export declare function classifyNpmFailure(error: unknown): unknown;
|
|
13
|
+
export declare const RUNTIME_CONTRACT: Readonly<{
|
|
14
|
+
schema: 1;
|
|
15
|
+
controllerProtocol: 1;
|
|
16
|
+
nodeMajor: 24;
|
|
17
|
+
platforms: string[];
|
|
18
|
+
protocol: 1;
|
|
19
|
+
stores: Readonly<{
|
|
20
|
+
id: "installation";
|
|
21
|
+
reads: {
|
|
22
|
+
min: number;
|
|
23
|
+
max: number;
|
|
24
|
+
};
|
|
25
|
+
writes: 1;
|
|
26
|
+
}>[];
|
|
27
|
+
}>;
|
|
28
|
+
export declare function managedPackage(root?: string): boolean;
|
|
29
|
+
export declare function artifactDirectory(identity: ServiceIdentity, release: AcceptedRuntimeRelease): string;
|
|
30
|
+
export declare function hashFile(file: string): Promise<string>;
|
|
31
|
+
export declare function examineArchive(file: string, directory: string): Promise<readonly string[]>;
|
|
32
|
+
export declare function runIsolated(node: string, args: readonly string[], cwd: string, signal: AbortSignal, timeout?: number): Promise<string>;
|
|
33
|
+
/** New generations never overwrite packages that an existing process can read.
|
|
34
|
+
* The receipt is written only after strict installation and isolated preflight. */
|
|
35
|
+
export declare function stageRuntime(identity: ServiceIdentity, release: AcceptedRuntimeRelease, node: string, signal: AbortSignal, installedBaseline?: string): Promise<string>;
|
|
36
|
+
export declare function collectRuntimeArtifacts(identity: ServiceIdentity, retained: readonly string[]): Promise<void>;
|
|
37
|
+
/** Called only after the new controller owns the exclusive controller lease,
|
|
38
|
+
* before discovery or staging starts. An isolated command's own lease remains
|
|
39
|
+
* authoritative even if the controller that spawned it has already exited. */
|
|
40
|
+
export declare function collectAbandonedUpdateWork(identity: ServiceIdentity): Promise<void>;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { createReadStream, existsSync } from 'node:fs';
|
|
6
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { extract, list } from 'tar';
|
|
9
|
+
import { BaseFetcher } from 'tuf-js';
|
|
10
|
+
import { acquireProcessLease } from '../installation/lease.js';
|
|
11
|
+
import { installedPackage, packageRoot, RUNTIME_FORMAT, CONTROLLER_PROTOCOL } from './store.js';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
export class RuntimeDownloadError extends Error {
|
|
14
|
+
}
|
|
15
|
+
export class RuntimeCommandError extends Error {
|
|
16
|
+
stdout;
|
|
17
|
+
exitCode;
|
|
18
|
+
constructor(message, stdout, exitCode) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.stdout = stdout;
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const npmTransportCodes = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EAI_AGAIN',
|
|
25
|
+
'ENOTFOUND', 'ENETUNREACH', 'EHOSTUNREACH', 'E408', 'E429', 'E500', 'E502', 'E503', 'E504']);
|
|
26
|
+
/** Only npm's structured top-level error classifies dependency transport.
|
|
27
|
+
* Script diagnostics, integrity failures and preflight output never do. */
|
|
28
|
+
export function classifyNpmFailure(error) {
|
|
29
|
+
if (!(error instanceof RuntimeCommandError) || error.exitCode === null)
|
|
30
|
+
return error;
|
|
31
|
+
let code;
|
|
32
|
+
try {
|
|
33
|
+
code = JSON.parse(error.stdout).error?.code;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
return typeof code === 'string' && npmTransportCodes.has(code)
|
|
39
|
+
? new RuntimeDownloadError(`Runtime dependency transport failed (${code})`, { cause: error }) : error;
|
|
40
|
+
}
|
|
41
|
+
export const RUNTIME_CONTRACT = Object.freeze({ schema: 1, controllerProtocol: CONTROLLER_PROTOCOL, nodeMajor: 24,
|
|
42
|
+
platforms: ['darwin-arm64', 'darwin-x64', 'linux-x64', 'win32-x64'],
|
|
43
|
+
protocol: 1, stores: [RUNTIME_FORMAT] });
|
|
44
|
+
export function managedPackage(root = packageRoot) {
|
|
45
|
+
const pkg = installedPackage(root);
|
|
46
|
+
return pkg.name === 'amalgm' && ['main', 'preview'].includes(pkg.amalgmSource?.branch ?? '');
|
|
47
|
+
}
|
|
48
|
+
export function artifactDirectory(identity, release) {
|
|
49
|
+
return join(identity.serviceDir, 'generations', release.digest, 'package');
|
|
50
|
+
}
|
|
51
|
+
export async function hashFile(file) {
|
|
52
|
+
const hash = createHash('sha512');
|
|
53
|
+
for await (const bytes of createReadStream(file))
|
|
54
|
+
hash.update(bytes);
|
|
55
|
+
return hash.digest('hex');
|
|
56
|
+
}
|
|
57
|
+
class RegistryFetcher extends BaseFetcher {
|
|
58
|
+
signal;
|
|
59
|
+
constructor(signal) {
|
|
60
|
+
super();
|
|
61
|
+
this.signal = signal;
|
|
62
|
+
}
|
|
63
|
+
async fetch(url) {
|
|
64
|
+
const target = new URL(url);
|
|
65
|
+
if (target.origin !== 'https://registry.npmjs.org')
|
|
66
|
+
throw new Error('Runtime archive escaped the npm registry');
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(target, { signal: this.signal, redirect: 'error' });
|
|
69
|
+
if (!response.ok || !response.body) {
|
|
70
|
+
const message = `Runtime archive returned HTTP ${response.status}`;
|
|
71
|
+
if (response.status >= 500 || response.status === 429 || response.status === 404)
|
|
72
|
+
throw new RuntimeDownloadError(message);
|
|
73
|
+
throw new Error(message);
|
|
74
|
+
}
|
|
75
|
+
// BaseFetcher consumes this body after fetch() returns. Classify failures
|
|
76
|
+
// at the transport reader, before archive integrity or disk effects run.
|
|
77
|
+
const reader = response.body.getReader();
|
|
78
|
+
return new ReadableStream({
|
|
79
|
+
async pull(controller) {
|
|
80
|
+
try {
|
|
81
|
+
const next = await reader.read();
|
|
82
|
+
if (next.done) {
|
|
83
|
+
reader.releaseLock();
|
|
84
|
+
controller.close();
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
controller.enqueue(next.value);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
reader.releaseLock();
|
|
91
|
+
controller.error(new RuntimeDownloadError('Runtime archive transfer was interrupted', { cause: error }));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async cancel(reason) { try {
|
|
95
|
+
await reader.cancel(reason);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
reader.releaseLock();
|
|
99
|
+
} },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof TypeError || error.name === 'TimeoutError')
|
|
104
|
+
throw new RuntimeDownloadError(String(error));
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function examineArchive(file, directory) {
|
|
110
|
+
const paths = new Set();
|
|
111
|
+
let size = 0;
|
|
112
|
+
let invalid = null;
|
|
113
|
+
await list({ file, strict: true, onReadEntry(entry) {
|
|
114
|
+
if (invalid)
|
|
115
|
+
return;
|
|
116
|
+
const parts = entry.path.replace(/\/$/, '').split('/');
|
|
117
|
+
if (parts[0] !== 'package' || parts.some(part => !part || part === '.' || part === '..') || entry.path.includes('\\')
|
|
118
|
+
|| !['File', 'Directory'].includes(entry.type) || paths.has(entry.path) || paths.size >= 20_000) {
|
|
119
|
+
invalid = 'Runtime archive has unsafe or ambiguous entries';
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
paths.add(entry.path);
|
|
123
|
+
size += entry.size;
|
|
124
|
+
if (size > 256 * 1024 * 1024)
|
|
125
|
+
invalid = 'Runtime archive exceeds its expanded bound';
|
|
126
|
+
} });
|
|
127
|
+
if (invalid)
|
|
128
|
+
throw new Error(invalid);
|
|
129
|
+
await extract({ file, cwd: directory, strict: true, preservePaths: false, noChmod: true });
|
|
130
|
+
return [...paths].filter(path => !path.endsWith('/'));
|
|
131
|
+
}
|
|
132
|
+
export async function runIsolated(node, args, cwd, signal, timeout = 600_000) {
|
|
133
|
+
const ownerFile = join(cwd, '.isolated-command.sqlite');
|
|
134
|
+
const token = randomBytes(32).toString('hex');
|
|
135
|
+
const streams = new Set();
|
|
136
|
+
const lifetime = createServer(stream => {
|
|
137
|
+
streams.add(stream);
|
|
138
|
+
stream.once('close', () => streams.delete(stream));
|
|
139
|
+
stream.on('error', () => { });
|
|
140
|
+
stream.setTimeout(5_000, () => stream.destroy());
|
|
141
|
+
let admission = '';
|
|
142
|
+
stream.on('data', bytes => {
|
|
143
|
+
admission += bytes.toString('utf8');
|
|
144
|
+
if (admission.length > 65 || !(token + '\n').startsWith(admission))
|
|
145
|
+
stream.destroy();
|
|
146
|
+
else if (admission === token + '\n') {
|
|
147
|
+
stream.setTimeout(0);
|
|
148
|
+
stream.write('start\n');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
lifetime.maxConnections = 1;
|
|
153
|
+
await new Promise((resolveReady, reject) => { lifetime.once('error', reject); lifetime.listen(0, '127.0.0.1', resolveReady); });
|
|
154
|
+
const address = lifetime.address();
|
|
155
|
+
if (!address || typeof address === 'string')
|
|
156
|
+
throw new Error('The isolated command has no parent lifetime address');
|
|
157
|
+
const env = { ...process.env, npm_config_userconfig: join(cwd, '.release-npmrc'), npm_config_audit: 'false', npm_config_fund: 'false',
|
|
158
|
+
AMALGM_ISOLATED_COMMAND_LIFETIME: JSON.stringify({ file: ownerFile, timeout, port: address.port, token }) };
|
|
159
|
+
delete env.NODE_AUTH_TOKEN;
|
|
160
|
+
delete env.NPM_TOKEN;
|
|
161
|
+
delete env.NODE_OPTIONS;
|
|
162
|
+
return await new Promise((resolveResult, reject) => {
|
|
163
|
+
const child = spawn(node, ['--import', new URL('./isolated-command-lifetime.js', import.meta.url).href, ...args], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
164
|
+
let bytes = '';
|
|
165
|
+
let outputBytes = '';
|
|
166
|
+
let complete = false;
|
|
167
|
+
let drain;
|
|
168
|
+
const cancel = () => child.kill('SIGKILL');
|
|
169
|
+
const timer = setTimeout(cancel, timeout);
|
|
170
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
171
|
+
const clean = () => { clearTimeout(timer); clearTimeout(drain); signal.removeEventListener('abort', cancel); };
|
|
172
|
+
const output = (chunk) => { bytes = (bytes + chunk.toString('utf8')).slice(-64 * 1024); };
|
|
173
|
+
child.stdout.on('data', (chunk) => { outputBytes = (outputBytes + chunk.toString('utf8')).slice(-64 * 1024); output(chunk); });
|
|
174
|
+
child.stderr.on('data', output);
|
|
175
|
+
child.once('exit', () => {
|
|
176
|
+
// A script descendant retaining stdout cannot keep the direct command's
|
|
177
|
+
// result pending after that command has actually exited.
|
|
178
|
+
drain = setTimeout(() => { child.stdout?.destroy(); child.stderr?.destroy(); }, 1_000);
|
|
179
|
+
drain.unref();
|
|
180
|
+
});
|
|
181
|
+
child.on('error', error => { if (!child.pid) {
|
|
182
|
+
complete = true;
|
|
183
|
+
clean();
|
|
184
|
+
reject(error);
|
|
185
|
+
} });
|
|
186
|
+
child.on('close', code => { if (complete)
|
|
187
|
+
return; clean(); if (code === 0 && !signal.aborted)
|
|
188
|
+
resolveResult(outputBytes);
|
|
189
|
+
else
|
|
190
|
+
reject(new RuntimeCommandError(`Isolated runtime command failed (${code}): ${bytes.slice(-4_000)}`, outputBytes, code)); });
|
|
191
|
+
if (signal.aborted)
|
|
192
|
+
cancel();
|
|
193
|
+
}).finally(async () => {
|
|
194
|
+
for (const stream of streams)
|
|
195
|
+
stream.destroy();
|
|
196
|
+
await new Promise(resolveClosed => lifetime.close(() => resolveClosed()));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function verifyPackage(root, release) {
|
|
200
|
+
const artifact = release.target.release.units[0].artifact;
|
|
201
|
+
const pkg = installedPackage(root);
|
|
202
|
+
if (pkg.name !== artifact.name || pkg.version !== artifact.version
|
|
203
|
+
|| ['repository', 'branch', 'commit'].some(key => Reflect.get(pkg.amalgmSource ?? {}, key) !== Reflect.get(artifact.source, key))
|
|
204
|
+
|| await hashFile(join(root, 'npm-shrinkwrap.json')) !== artifact.lockSha512)
|
|
205
|
+
throw new Error('Runtime package differs from the signed composition');
|
|
206
|
+
}
|
|
207
|
+
/** New generations never overwrite packages that an existing process can read.
|
|
208
|
+
* The receipt is written only after strict installation and isolated preflight. */
|
|
209
|
+
export async function stageRuntime(identity, release, node, signal, installedBaseline) {
|
|
210
|
+
const destination = artifactDirectory(identity, release);
|
|
211
|
+
if (!installedBaseline && existsSync(join(dirname(destination), 'verified.json'))) {
|
|
212
|
+
await verifyPackage(destination, release);
|
|
213
|
+
return destination;
|
|
214
|
+
}
|
|
215
|
+
const parent = dirname(dirname(destination));
|
|
216
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
217
|
+
const scratch = await mkdtemp(join(parent, '.stage-'));
|
|
218
|
+
try {
|
|
219
|
+
const artifact = release.target.release.units[0].artifact;
|
|
220
|
+
const archive = join(scratch, 'runtime.tgz');
|
|
221
|
+
const fetcher = new RegistryFetcher(AbortSignal.any([signal, AbortSignal.timeout(120_000)]));
|
|
222
|
+
await fetcher.downloadFile(`https://registry.npmjs.org/amalgm/-/amalgm-${artifact.version}.tgz`, 128 * 1024 * 1024, async (file) => {
|
|
223
|
+
if (await hashFile(file) !== artifact.sha512)
|
|
224
|
+
throw new Error('Runtime archive integrity differs from its signed target');
|
|
225
|
+
await writeFile(archive, await readFile(file));
|
|
226
|
+
});
|
|
227
|
+
const files = await examineArchive(archive, scratch);
|
|
228
|
+
const root = join(scratch, 'package');
|
|
229
|
+
await verifyPackage(root, release);
|
|
230
|
+
if (installedBaseline) {
|
|
231
|
+
await verifyPackage(installedBaseline, release);
|
|
232
|
+
for (const path of files) {
|
|
233
|
+
const source = resolve(scratch, path);
|
|
234
|
+
const relative = path.slice('package/'.length);
|
|
235
|
+
const stat = await lstat(source);
|
|
236
|
+
if (stat.isFile() && await hashFile(source) !== await hashFile(join(installedBaseline, relative)))
|
|
237
|
+
throw new Error('Installed baseline contains different package bytes');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await writeFile(join(root, '.release-npmrc'), 'registry=https://registry.npmjs.org/\nstrict-allow-scripts=true\n');
|
|
241
|
+
const npm = join(dirname(require.resolve('npm/package.json')), 'bin/npm-cli.js');
|
|
242
|
+
try {
|
|
243
|
+
await runIsolated(node, [npm, 'ci', '--json', '--omit=dev', '--strict-allow-scripts', '--no-audit', '--no-fund'], root, signal);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
throw classifyNpmFailure(error);
|
|
247
|
+
}
|
|
248
|
+
const result = await runIsolated(node, [join(root, 'dist/main.js'), 'version', '--runtime-contract'], root, signal, 30_000);
|
|
249
|
+
const contract = JSON.parse(result.trim());
|
|
250
|
+
const declared = release.target.release;
|
|
251
|
+
if (contract.controllerProtocol !== declared.controllerProtocol || contract.nodeMajor !== declared.nodeMajor
|
|
252
|
+
|| contract.protocol !== declared.units[0].protocol || JSON.stringify(contract.stores) !== JSON.stringify(declared.units[0].stores))
|
|
253
|
+
throw new Error('Runtime preflight contract differs from its signed recipe');
|
|
254
|
+
await rm(archive);
|
|
255
|
+
await rm(join(root, '.release-npmrc'));
|
|
256
|
+
await rm(join(root, '.isolated-command.sqlite'));
|
|
257
|
+
await writeFile(join(scratch, 'verified.json'), JSON.stringify({ digest: release.digest }));
|
|
258
|
+
if (existsSync(dirname(destination))) {
|
|
259
|
+
await verifyPackage(destination, release);
|
|
260
|
+
return destination;
|
|
261
|
+
}
|
|
262
|
+
await rename(scratch, dirname(destination));
|
|
263
|
+
return destination;
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
await rm(scratch, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export async function collectRuntimeArtifacts(identity, retained) {
|
|
270
|
+
const parent = join(identity.serviceDir, 'generations');
|
|
271
|
+
for (const entry of await readdir(parent).catch(() => [])) {
|
|
272
|
+
if (/^[a-f0-9]{64}$/.test(entry) && !retained.includes(entry))
|
|
273
|
+
await rm(join(parent, entry), { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Called only after the new controller owns the exclusive controller lease,
|
|
277
|
+
* before discovery or staging starts. An isolated command's own lease remains
|
|
278
|
+
* authoritative even if the controller that spawned it has already exited. */
|
|
279
|
+
export async function collectAbandonedUpdateWork(identity) {
|
|
280
|
+
const parent = join(identity.serviceDir, 'generations');
|
|
281
|
+
for (const entry of await readdir(parent, { withFileTypes: true }).catch(() => [])) {
|
|
282
|
+
if (!entry.isDirectory() || !/^\.stage-[A-Za-z0-9]+$/.test(entry.name))
|
|
283
|
+
continue;
|
|
284
|
+
const directory = join(parent, entry.name);
|
|
285
|
+
const owner = join(directory, 'package', '.isolated-command.sqlite');
|
|
286
|
+
if (existsSync(owner)) {
|
|
287
|
+
const lease = acquireProcessLease(owner);
|
|
288
|
+
if (!lease)
|
|
289
|
+
continue;
|
|
290
|
+
lease.close();
|
|
291
|
+
}
|
|
292
|
+
await rm(directory, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
for (const entry of await readdir(identity.serviceDir, { withFileTypes: true })) {
|
|
295
|
+
if (entry.isDirectory() && /^trust-refresh-[A-Za-z0-9]+$/.test(entry.name))
|
|
296
|
+
await rm(join(identity.serviceDir, entry.name), { recursive: true, force: true });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=artifacts.js.map
|