@vzn/vx 0.0.638 → 0.0.640
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/README.md +1 -1
- package/docs/caching.md +152 -95
- package/docs/cli.md +225 -372
- package/docs/execution.md +22 -47
- package/docs/features.md +14 -16
- package/docs/patterns.md +6 -8
- package/docs/schema.md +268 -215
- package/docs/security.md +1 -1
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/show.json +14 -14
- package/skills/vx/SKILL.md +10 -7
- package/src/cache/cache.ts +44 -11
- package/src/cache/file-hashes.ts +7 -12
- package/src/cache/fs-clock.ts +62 -0
- package/src/cache/git-inputs.ts +8 -2
- package/src/cache/index.ts +5 -2
- package/src/cache/inputs.ts +154 -83
- package/src/cache/key-fold.ts +23 -3
- package/src/cache/layer.ts +33 -18
- package/src/cache/layered-cache.ts +108 -315
- package/src/cache/linked-inputs.ts +87 -0
- package/src/cache/plugin-cache.ts +2 -10
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +24 -8
- package/src/cache/run-store.ts +4 -4
- package/src/cache/schema.ts +6 -4
- package/src/cache/scoped-cache.ts +7 -0
- package/src/cli/completions.ts +2 -2
- package/src/cli/foreign-flags.ts +11 -23
- package/src/cli/help.ts +6 -31
- package/src/cli/info.ts +2 -1
- package/src/cli/init.ts +23 -11
- package/src/cli/last.ts +30 -4
- package/src/cli/plan-format.ts +5 -58
- package/src/cli/plugin-commands.ts +2 -2
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +23 -185
- package/src/cli/select.ts +16 -6
- package/src/cli/show.ts +15 -6
- package/src/cli/upgrade.ts +215 -55
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +22 -6
- package/src/cli/watch-judge.ts +49 -5
- package/src/cli/watch-set.ts +18 -8
- package/src/cli/watch.ts +103 -16
- package/src/cli/why.ts +9 -7
- package/src/config.ts +19 -21
- package/src/exec/executor.ts +5 -5
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/dependency-spec.ts +2 -2
- package/src/graph/folds.ts +236 -0
- package/src/graph/index.ts +17 -2
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +45 -31
- package/src/graph/task-graph.ts +190 -58
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +1 -1
- package/src/{orchestrator → history}/history.ts +1 -1
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +7 -20
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +11 -29
- package/src/orchestrator/affected-tasks.ts +20 -19
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +16 -22
- package/src/orchestrator/excluded-keys.ts +4 -5
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +50 -411
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +2 -73
- package/src/orchestrator/keyed-projects.ts +12 -51
- package/src/orchestrator/local-shortcircuit.ts +1 -1
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +5 -5
- package/src/orchestrator/plan.ts +19 -6
- package/src/orchestrator/plugin-host.ts +129 -69
- package/src/orchestrator/plugin.ts +18 -8
- package/src/orchestrator/prepare.ts +40 -34
- package/src/orchestrator/projects.ts +7 -10
- package/src/orchestrator/remote-prefetch.ts +14 -1
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +35 -3
- package/src/orchestrator/run-report.ts +151 -171
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-steps.ts +57 -0
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +293 -604
- package/src/orchestrator/sandbox-fix.ts +1 -17
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +164 -27
- package/src/orchestrator/steps.ts +70 -0
- package/src/orchestrator/task-hash.ts +34 -25
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +4 -4
- package/src/orchestrator/telemetry.ts +14 -86
- package/src/orchestrator/upstream.ts +4 -181
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +40 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +158 -20
- package/src/{orchestrator → report}/status-line.ts +88 -7
- package/src/{orchestrator → report}/summary.ts +126 -35
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +12 -39
- package/src/util/colors.ts +44 -4
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +19 -4
- package/src/util/paths.ts +1 -1
- package/src/util/report.ts +13 -5
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +17 -6
- package/src/workspace/affected.ts +81 -2
- package/src/workspace/config-imports.ts +1 -1
- package/src/workspace/config-schema.ts +142 -143
- package/src/workspace/lockfile.ts +1 -1
- package/src/workspace/migration.ts +19 -3
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -280
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
|
@@ -36,43 +36,7 @@ export { exitSignal, shellQuote } from '../exec/index.js'
|
|
|
36
36
|
export { forwardedSignal } from './signals.js'
|
|
37
37
|
// `vx cache clean` waits for a run on the workspace before it evicts.
|
|
38
38
|
export { acquireRunLock } from './run-lock.js'
|
|
39
|
-
export { defaultLogger, resolveOutputView } from './logger.js'
|
|
40
|
-
export type { Logger, OutputView } from './logger.js'
|
|
41
39
|
export type { RunPlan, PlannedTask, PlanPrediction, CacheStatus } from './plan.js'
|
|
42
|
-
export {
|
|
43
|
-
createEventBus,
|
|
44
|
-
wireForwarder,
|
|
45
|
-
toWireEvent,
|
|
46
|
-
projectNode,
|
|
47
|
-
projectOutcome,
|
|
48
|
-
outcomeWord,
|
|
49
|
-
} from './events.js'
|
|
50
|
-
export type {
|
|
51
|
-
EventBus,
|
|
52
|
-
RunEvent,
|
|
53
|
-
RunEventSubscriber,
|
|
54
|
-
RunStartInfo,
|
|
55
|
-
WireEvent,
|
|
56
|
-
TaskView,
|
|
57
|
-
OutcomeView,
|
|
58
|
-
} from './events.js'
|
|
59
|
-
export { escapeMarkdownCell, formatRunReportMarkdown, type RunResult } from './run-report.js'
|
|
60
|
-
export {
|
|
61
|
-
EmptyHistoryProvider,
|
|
62
|
-
type HistoryProvider,
|
|
63
|
-
type HistoryTable,
|
|
64
|
-
LocalHistoryProvider,
|
|
65
|
-
type TaskHistory,
|
|
66
|
-
type TaskResources,
|
|
67
|
-
} from './history.js'
|
|
68
|
-
export {
|
|
69
|
-
detectFlaky,
|
|
70
|
-
type FailureMode,
|
|
71
|
-
type FlakyCandidate,
|
|
72
|
-
type FlakyFinding,
|
|
73
|
-
type FlakyTask,
|
|
74
|
-
flakyTasks,
|
|
75
|
-
} from './failure-mode.js'
|
|
76
40
|
export {
|
|
77
41
|
installPlugins,
|
|
78
42
|
type CacheContext,
|
|
@@ -133,17 +97,8 @@ export { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
|
133
97
|
// module header for why one implementation, not one per sink.
|
|
134
98
|
export { LOG_WIRE_VERSION, TaskLogBuffer } from './task-log-buffer.js'
|
|
135
99
|
export type { TaskLogBundle, TaskLogEntry } from './task-log-buffer.js'
|
|
136
|
-
export {
|
|
137
|
-
assembleRunSummary,
|
|
138
|
-
createTelemetrySource,
|
|
139
|
-
deriveCacheSource,
|
|
140
|
-
isCacheHit,
|
|
141
|
-
isPassStatus,
|
|
142
|
-
TASK_STATUSES,
|
|
143
|
-
TELEMETRY_SCHEMA_VERSION,
|
|
144
|
-
} from './telemetry.js'
|
|
100
|
+
export { assembleRunSummary, createTelemetrySource, TELEMETRY_SCHEMA_VERSION } from './telemetry.js'
|
|
145
101
|
export type {
|
|
146
|
-
CacheSource,
|
|
147
102
|
RunContextRecord,
|
|
148
103
|
RunSummaryRecord,
|
|
149
104
|
TaskTelemetry,
|
|
@@ -163,18 +118,6 @@ export {
|
|
|
163
118
|
detectCi,
|
|
164
119
|
} from './run-context.js'
|
|
165
120
|
export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './run-context.js'
|
|
166
|
-
export {
|
|
167
|
-
cacheKeyDiff,
|
|
168
|
-
explainCacheKey as explainCacheKeyQuery,
|
|
169
|
-
getInvocation,
|
|
170
|
-
getRun,
|
|
171
|
-
latestRunId,
|
|
172
|
-
listInvocations,
|
|
173
|
-
listRuns,
|
|
174
|
-
inputWriters,
|
|
175
|
-
rootCauses,
|
|
176
|
-
whyDidThisRerun as whyDidThisRerunQuery,
|
|
177
|
-
} from './metrics.js'
|
|
178
121
|
export {
|
|
179
122
|
runFailures,
|
|
180
123
|
type RunFailures,
|
|
@@ -182,20 +125,6 @@ export {
|
|
|
182
125
|
type TaskLog,
|
|
183
126
|
taskLog,
|
|
184
127
|
} from './run-failures.js'
|
|
185
|
-
export type
|
|
128
|
+
export { type RunStep, runSteps } from './run-steps.js'
|
|
186
129
|
export { resolveRunId, shortRunId } from './run-id.js'
|
|
187
|
-
export type {
|
|
188
|
-
CacheEntryRow,
|
|
189
|
-
CacheKeyDiff,
|
|
190
|
-
InputWriter,
|
|
191
|
-
RootCause,
|
|
192
|
-
CacheKeyExplanation,
|
|
193
|
-
InputDiffEntry,
|
|
194
|
-
InvocationDetail,
|
|
195
|
-
ListInvocationsArgs,
|
|
196
|
-
ListRunsArgs,
|
|
197
|
-
RunDetail,
|
|
198
|
-
RunSummaryRow,
|
|
199
|
-
WhyDidThisRerun,
|
|
200
|
-
} from './metrics.js'
|
|
201
130
|
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './doctor.js'
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
// is decided — on the graph, before any hash exists, through the same
|
|
6
6
|
// selection the hash path applies (`selectFoldedDeps`).
|
|
7
7
|
|
|
8
|
-
import { isGroupTask, type TaskNode } from '../graph/index.js'
|
|
9
|
-
import { keyedDeps, selectFoldedDeps, type FoldCandidate } from './upstream.js'
|
|
8
|
+
import { foldedDeps, isGroupTask, type FoldCandidate, type TaskNode } from '../graph/index.js'
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* A run's keyed-set lookup: for a task, the directories of every project
|
|
@@ -14,18 +13,17 @@ import { keyedDeps, selectFoldedDeps, type FoldCandidate } from './upstream.js'
|
|
|
14
13
|
* task itself is not counted.
|
|
15
14
|
*
|
|
16
15
|
* The fold relation is the hash path's, computed on the graph:
|
|
17
|
-
* - an exec task folds its dependencies as its own `
|
|
18
|
-
* selects them (absent
|
|
19
|
-
* - a group folds every dependency (`computeGroupKey`)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* - a persistent task is keyed as
|
|
16
|
+
* - an exec task folds its dependencies as its own `inputs.tasks`
|
|
17
|
+
* selects them (absent means all);
|
|
18
|
+
* - a group folds every dependency (`computeGroupKey`); a keyed one
|
|
19
|
+
* (`inputs`, the default `build`) folds its project's files as a task
|
|
20
|
+
* does;
|
|
21
|
+
* - a persistent task is keyed as any task with no `cache` (A-17).
|
|
23
22
|
*
|
|
24
|
-
* Every exec task reached contributes its project, cached or not:
|
|
25
|
-
* folds
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
* key itself.
|
|
23
|
+
* Every exec task reached contributes its project, cached or not: its key
|
|
24
|
+
* folds its own project's files through its `inputs`, and one with no
|
|
25
|
+
* `inputs` is keyless, so a key that folds it is never stored or read.
|
|
26
|
+
* `tests/keyed-projects.test.ts` holds that to the key itself.
|
|
29
27
|
*
|
|
30
28
|
* Lazy and memoized by task id: only a sandboxed task that executes asks,
|
|
31
29
|
* so a run of hits walks nothing, and a shared subgraph is walked once.
|
|
@@ -57,7 +55,7 @@ export function keyedProjects(
|
|
|
57
55
|
stack.pop()
|
|
58
56
|
const out = new Set<string>()
|
|
59
57
|
for (const { node: dep } of deps) {
|
|
60
|
-
if (!isGroupTask(dep) || dep.config.
|
|
58
|
+
if (!isGroupTask(dep) || dep.config.inputs !== undefined) out.add(dep.projectDir)
|
|
61
59
|
for (const dir of below.get(dep.id)!) out.add(dir)
|
|
62
60
|
}
|
|
63
61
|
below.set(node.id, out)
|
|
@@ -65,40 +63,3 @@ export function keyedProjects(
|
|
|
65
63
|
return below.get(root.id)!
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The dependencies `node`'s key folds, per the hash path's rules above, as
|
|
71
|
-
* `keyUpstream` hands them to the key: the scheduled ones less the edges
|
|
72
|
-
* `--exclude-dependencies` left for order alone, plus the dropped ones it
|
|
73
|
-
* keys all the same (`excludedUpstream`), whose own dependencies the walk
|
|
74
|
-
* reads from the run's `keyOnly` map. `node.deps` held the order-only
|
|
75
|
-
* edges and lacked the dropped ones: the sandbox granted a sibling the key
|
|
76
|
-
* does not answer for.
|
|
77
|
-
*/
|
|
78
|
-
export function foldedDeps(node: TaskNode, nodeOf: (id: string) => TaskNode): FoldCandidate[] {
|
|
79
|
-
const candidates: FoldCandidate[] = []
|
|
80
|
-
for (const id of keyedDeps(node)) {
|
|
81
|
-
const dep = nodeOf(id)
|
|
82
|
-
candidates.push({ node: dep, unit: foldUnit(dep) })
|
|
83
|
-
}
|
|
84
|
-
for (const { node: dep } of node.excludedUpstream ?? []) {
|
|
85
|
-
candidates.push({ node: dep, unit: foldUnit(dep) })
|
|
86
|
-
}
|
|
87
|
-
if (isGroupTask(node) && node.config.cache === undefined) return candidates
|
|
88
|
-
return selectFoldedDeps(candidates, node.config.cache?.inputs?.tasks, node.projectName, node.id)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* What stands in for a dependency's hash in the dedup: equal exactly when
|
|
93
|
-
* the hashes are. An exec task's key folds its own id (`task:<id>`), so its
|
|
94
|
-
* id is enough; a group's hash is a roll-up of its members' ids and hashes
|
|
95
|
-
* with no id of its own, so two groups over the same members hash alike.
|
|
96
|
-
* The members are `keyUpstream`'s, which `computeGroupHash` is handed: under
|
|
97
|
-
* `--exclude-dependencies` `node.deps` holds order-only edges and lacks the
|
|
98
|
-
* dropped ones, and two groups the key told apart shared a unit.
|
|
99
|
-
*/
|
|
100
|
-
function foldUnit(node: TaskNode): string {
|
|
101
|
-
if (!isGroupTask(node) || node.config.cache !== undefined) return `task|${node.id}`
|
|
102
|
-
const members = [...keyedDeps(node), ...(node.excludedUpstream ?? []).map((u) => u.node.id)]
|
|
103
|
-
return `group|${members.sort().join('|')}`
|
|
104
|
-
}
|
|
@@ -232,7 +232,7 @@ export function restoreTierExclusions(
|
|
|
232
232
|
const dependants = new Map<string, string[]>()
|
|
233
233
|
for (const node of nodes.values()) {
|
|
234
234
|
const dir = relPosix(workspaceRoot, node.projectDir)
|
|
235
|
-
const wsInputs = node.config.
|
|
235
|
+
const wsInputs = node.config.inputs?.workspaceFiles ?? []
|
|
236
236
|
if (reaches(dir) || (wsInputs.length > 0 && workspaceInputsReach(wsInputs, prefixes))) {
|
|
237
237
|
out.add(node.id)
|
|
238
238
|
}
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
taskGlob,
|
|
26
26
|
wholeSubtreePrefixes,
|
|
27
27
|
} from '../util/index.js'
|
|
28
|
-
import type
|
|
29
|
-
import type { TaskInputComponent } from '
|
|
28
|
+
import { type Logger, warnOf } from '../report/index.js'
|
|
29
|
+
import type { TaskInputComponent } from '../cache/index.js'
|
|
30
30
|
|
|
31
31
|
/** A snapshot request for `recordOutputDirs`, taken at run end. */
|
|
32
32
|
export interface OutputDirSnapshot {
|
|
@@ -61,6 +61,36 @@ export function entryHolds(
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The run-end output-directory snapshots, taken once the directories are
|
|
66
|
+
* old enough for the snapshot's racy window. Each walk is held to its
|
|
67
|
+
* entry's rows (item 1087), read in one batch: read per snapshot, each read
|
|
68
|
+
* also flushed the snapshots before it, one transaction apiece (+110 ms at
|
|
69
|
+
* 1,000). A few walks at a time: each is an lstat + readdir per prefix; the
|
|
70
|
+
* rows land together at close, in one transaction.
|
|
71
|
+
*/
|
|
72
|
+
export async function recordOutputDirSnapshots(
|
|
73
|
+
cache: CacheLayer,
|
|
74
|
+
snapshots: readonly OutputDirSnapshot[],
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const rowsOf = new Map<string, ReadonlyArray<{ path: string }>>()
|
|
77
|
+
for (let i = 0; i < snapshots.length; i += 900) {
|
|
78
|
+
const batch = cache.loadOutputFilesBatch(snapshots.slice(i, i + 900).map((s) => s.hash))
|
|
79
|
+
for (const [hash, rows] of batch) rowsOf.set(hash, rows)
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i < snapshots.length; i += 32) {
|
|
82
|
+
await Promise.all(
|
|
83
|
+
snapshots
|
|
84
|
+
.slice(i, i + 32)
|
|
85
|
+
.map((s) =>
|
|
86
|
+
cache.recordOutputDirs?.(s.hash, s.projectDir, s.prefixes, (files) =>
|
|
87
|
+
s.holds(files, rowsOf.get(s.hash) ?? []),
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
export interface SaveMissArgs {
|
|
65
95
|
node: TaskNode
|
|
66
96
|
hash: string
|
|
@@ -153,7 +183,7 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
153
183
|
// A `workspaceFiles` directory linked out of the workspace drops every
|
|
154
184
|
// file (M-65); a project one is refused by the resolver (X-88).
|
|
155
185
|
const linkedOut = outputDirLinkedOut(a.workspaceRoot, a.wsOutputs)
|
|
156
|
-
log
|
|
186
|
+
warnOf(log)(
|
|
157
187
|
`[vx] ${node.id}: cache.outputs matched no files (${[...a.outputs, ...a.wsOutputs].join(', ')}) — ` +
|
|
158
188
|
`an empty artifact is saved; a later hit restores nothing` +
|
|
159
189
|
(linkedOut !== undefined
|
|
@@ -6,10 +6,8 @@ import type { CacheFlags, CachePolicy, RemoteCacheLayer } from '../cache/index.j
|
|
|
6
6
|
import type { ContinueMode } from '../graph/index.js'
|
|
7
7
|
import type { TaskOutcome } from '../graph/index.js'
|
|
8
8
|
import type { AffectedReason } from './affected-tasks.js'
|
|
9
|
-
import type { EventBus } from '
|
|
9
|
+
import type { EventBus, Logger } from '../report/index.js'
|
|
10
10
|
import type { TelemetrySink } from './telemetry.js'
|
|
11
|
-
import type { Logger } from './logger.js'
|
|
12
|
-
import type { RunSummaryJson } from './run-artifacts.js'
|
|
13
11
|
import type { AffectedChanges, ProjectEntry, ProjectMeta } from '../workspace/index.js'
|
|
14
12
|
import type { RunSettings } from '../config.js'
|
|
15
13
|
|
|
@@ -164,12 +162,6 @@ export interface RunOptions {
|
|
|
164
162
|
branch?: string
|
|
165
163
|
/** `run.schedule` from its flag; unset, `VX_RUN_SCHEDULE`, then the workspace's. */
|
|
166
164
|
schedule?: NonNullable<RunSettings['schedule']>
|
|
167
|
-
/** `run.killGrace` in ms from its flag; unset, `VX_RUN_KILL_GRACE`, then the workspace's. */
|
|
168
|
-
killGrace?: number
|
|
169
|
-
/** `run.teardownTimeout` in ms from its flag; unset, `VX_RUN_TEARDOWN_TIMEOUT`, then the workspace's. */
|
|
170
|
-
teardownTimeout?: number
|
|
171
|
-
/** `run.readyNotice` in ms from its flag; unset, `VX_RUN_READY_NOTICE`, then the workspace's. */
|
|
172
|
-
readyNotice?: number
|
|
173
165
|
/**
|
|
174
166
|
* Filter `dependsOn` expansion. `'all'` drops every edge (just the
|
|
175
167
|
* requested task runs). A string array drops only those task names
|
|
@@ -178,25 +170,6 @@ export interface RunOptions {
|
|
|
178
170
|
excludeDependencies?: 'all' | readonly string[]
|
|
179
171
|
/** Forwarded to the last step of each task's exec array (shell-quoted). */
|
|
180
172
|
forwardArgs?: readonly string[]
|
|
181
|
-
/**
|
|
182
|
-
* If set, write a per-run JSON summary at end of run. Empty string
|
|
183
|
-
* picks the default path `<cacheDir>/runs/<run_id>.json`; anything
|
|
184
|
-
* else is treated as the literal file path (cwd-relative).
|
|
185
|
-
*/
|
|
186
|
-
summarize?: string
|
|
187
|
-
/**
|
|
188
|
-
* `vx run --format json`: the terminal renderer writes stderr, and the
|
|
189
|
-
* run hands back `RunSummary.json`, the `--summarize` document, for the
|
|
190
|
-
* caller to print on stdout.
|
|
191
|
-
*/
|
|
192
|
-
json?: boolean
|
|
193
|
-
/**
|
|
194
|
-
* Text printed just above the run's footer, given the outcomes the
|
|
195
|
-
* footer counts, after the `summaryTable`: nothing prints below the
|
|
196
|
-
* footer (owner), so a caller's end-of-run report (`--report`) rides
|
|
197
|
-
* here. Not called on a refused run.
|
|
198
|
-
*/
|
|
199
|
-
beforeFooter?: (outcomes: readonly TaskOutcome[], ok: boolean) => string
|
|
200
173
|
/**
|
|
201
174
|
* `false`: no footer, live or printed, and no blank line opening the
|
|
202
175
|
* task log; the run is its task rows (`vx watch`, which frames each
|
|
@@ -283,12 +256,6 @@ export interface RunOptions {
|
|
|
283
256
|
* A stateless `vx run` passes none and is byte-identical to before.
|
|
284
257
|
*/
|
|
285
258
|
inflight?: Map<string, Promise<void>>
|
|
286
|
-
/**
|
|
287
|
-
* `--tag k=v` pairs (Tier-3). Persisted on the run's `invocations`
|
|
288
|
-
* row (JSON object) so dashboards can filter runs by label, a secret
|
|
289
|
-
* value masked (L-38). CLI parsing is the CLI's job.
|
|
290
|
-
*/
|
|
291
|
-
tags?: Record<string, string>
|
|
292
259
|
/**
|
|
293
260
|
* Additional observe-only telemetry sinks, merged with whatever the
|
|
294
261
|
* workspace plugins contribute. The embedder seam: a host executing
|
|
@@ -337,8 +304,6 @@ export interface RunSummary {
|
|
|
337
304
|
* caller prints it; `vx run` writes it to stderr.
|
|
338
305
|
*/
|
|
339
306
|
refused?: string
|
|
340
|
-
/** The `--summarize` document, when `RunOptions.json` asked for it. */
|
|
341
|
-
json?: RunSummaryJson
|
|
342
307
|
}
|
|
343
308
|
|
|
344
309
|
export interface HeldPersistent {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// to telemetry-host.ts. With no plugin declaring the hook nothing here runs,
|
|
3
3
|
// and core's renderer is the run's, byte for byte.
|
|
4
4
|
|
|
5
|
-
import { terminalSubscriber, type RunEventSubscriber } from '
|
|
6
|
-
import type { Logger } from '
|
|
5
|
+
import { terminalSubscriber, type RunEventSubscriber } from '../report/index.js'
|
|
6
|
+
import type { Logger } from '../report/index.js'
|
|
7
7
|
import type { OutputContext, VxPlugin } from './plugin.js'
|
|
8
8
|
|
|
9
9
|
const REQUIRED = ['status', 'taskStdout', 'taskStderr', 'taskComplete'] as const
|
|
10
|
-
const OPTIONAL = ['runStart', 'taskStart', 'runEnd'] as const
|
|
10
|
+
const OPTIONAL = ['runStart', 'taskStart', 'runEnd', 'stepStart', 'stepEnd', 'runDone'] as const
|
|
11
11
|
|
|
12
12
|
/** Reject anything that is not Logger-shaped, naming what is missing. */
|
|
13
13
|
function checkLogger(value: unknown): Logger {
|
|
@@ -64,6 +64,7 @@ export function guardedOutput(
|
|
|
64
64
|
failed = true
|
|
65
65
|
fallback({
|
|
66
66
|
kind: 'run:status',
|
|
67
|
+
warn: true,
|
|
67
68
|
line: `[vx] plugin '${claimed.owner}' output failed: ${err instanceof Error ? err.message : String(err)}; vx renders the rest of the run`,
|
|
68
69
|
})
|
|
69
70
|
fallback(event)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// A persistent task (a dev server, a watcher): keyed like an uncached task,
|
|
2
|
+
// spawned, and held until `readyWhen` matches. Never cached and never routed
|
|
3
|
+
// through an executor. Moved out of execute-task.ts as pure code motion.
|
|
4
|
+
|
|
5
|
+
import type { ExecConfig } from '../config.js'
|
|
6
|
+
import {
|
|
7
|
+
PersistentReadyError,
|
|
8
|
+
releaseBridges,
|
|
9
|
+
runPersistent,
|
|
10
|
+
signalExitCode,
|
|
11
|
+
withForwardArgs,
|
|
12
|
+
wrapSandboxedCommand,
|
|
13
|
+
} from '../exec/index.js'
|
|
14
|
+
import type { TaskOutcome } from '../graph/index.js'
|
|
15
|
+
import { maskedEmitter, secretMask, testTimerMs } from '../util/index.js'
|
|
16
|
+
import { warnOf } from '../report/index.js'
|
|
17
|
+
import type { ExecuteArgs } from './execute-task.js'
|
|
18
|
+
import {
|
|
19
|
+
commandWriteReach,
|
|
20
|
+
mayWriteFingerprint,
|
|
21
|
+
type Placeholder,
|
|
22
|
+
placeholderSweeper,
|
|
23
|
+
sandboxRequestFor,
|
|
24
|
+
sweepPlaceholders,
|
|
25
|
+
undeclaredWriteReach,
|
|
26
|
+
untouchedPlaceholderLine,
|
|
27
|
+
} from './sandbox-request.js'
|
|
28
|
+
import { forwardedSignal, terminateChildren } from './signals.js'
|
|
29
|
+
import { computeTaskHash } from './task-hash.js'
|
|
30
|
+
import { forgetUndeclaredWrites, isAborted, taskEnv, taskTimeoutMs } from './task-process.js'
|
|
31
|
+
|
|
32
|
+
/** How long a server may take to match `readyWhen` before vx says it waits. */
|
|
33
|
+
const READY_NOTICE_MS = 10_000
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Persistent task: dev server / file watcher / daemon. Spawn, wait
|
|
37
|
+
* for ready (regex match or immediate), stash the subprocess in the
|
|
38
|
+
* orchestrator-owned registry, return success. The orchestrator
|
|
39
|
+
* SIGTERMs the registry at end-of-run.
|
|
40
|
+
*
|
|
41
|
+
* Never reads or writes the cache — the project loader rejects
|
|
42
|
+
* `cache + persistent` at config-load time, so by the time we get
|
|
43
|
+
* here, `cache` is guaranteed undefined.
|
|
44
|
+
*
|
|
45
|
+
* Never routed through an executor: a persistent task is local by
|
|
46
|
+
* construction (its port lives on this machine).
|
|
47
|
+
*/
|
|
48
|
+
export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
49
|
+
const { node, log } = args
|
|
50
|
+
// Type narrowing: the dispatcher checks `exec.persistent` before
|
|
51
|
+
// calling us, so `exec` and `exec.persistent` are both present.
|
|
52
|
+
const step = node.config.exec as ExecConfig & {
|
|
53
|
+
persistent: NonNullable<ExecConfig['persistent']>
|
|
54
|
+
}
|
|
55
|
+
const effectiveForwardArgs = node.requested ? (args.forwardArgs ?? []) : []
|
|
56
|
+
const env = taskEnv(node, step, args.workspaceRoot)
|
|
57
|
+
const wallclockStartNs = process.hrtime.bigint() - args.runStartHrTimeNs
|
|
58
|
+
// Keyed on its inputs like any task, so a cached dependant behind a dev
|
|
59
|
+
// server re-runs when the server's sources change; with none it has no
|
|
60
|
+
// key and the dependant always runs (A-17).
|
|
61
|
+
const hash =
|
|
62
|
+
args.noDependants === true
|
|
63
|
+
? undefined
|
|
64
|
+
: (args.upfrontKey ??
|
|
65
|
+
(await computeTaskHash({
|
|
66
|
+
node,
|
|
67
|
+
upstream: args.upstream,
|
|
68
|
+
workspaceRoot: args.workspaceRoot,
|
|
69
|
+
cache: args.cache,
|
|
70
|
+
forwardArgs: args.forwardArgs,
|
|
71
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
72
|
+
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
73
|
+
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
74
|
+
})))
|
|
75
|
+
|
|
76
|
+
// The args after `--` reach a server as they reach any task. A readyWhen
|
|
77
|
+
// server once got none — "so the matcher sees the unmodified output",
|
|
78
|
+
// though the args change the command, not what the matcher reads — and
|
|
79
|
+
// `vx run dev -- --port 4000` on the documented Vite task dropped the
|
|
80
|
+
// port in silence (item 1060).
|
|
81
|
+
const plainCommand = withForwardArgs(step.command, effectiveForwardArgs)
|
|
82
|
+
// A persistent task declaring `exec.sandbox` runs inside it like any
|
|
83
|
+
// other: the same grants, the same walls. What it cannot have is the
|
|
84
|
+
// violation REPORT — that reads the trace after the child exits, and a
|
|
85
|
+
// server exits when the run tears it down. Enforced, not reported.
|
|
86
|
+
// (Until 2026-09-09 the block was accepted and silently ignored.)
|
|
87
|
+
let command = plainCommand
|
|
88
|
+
let bridgeTag: string | undefined
|
|
89
|
+
let signalChannel = false
|
|
90
|
+
// The empty files the request pre-created for a literal write grant; a
|
|
91
|
+
// server that never writes one gets it taken back when it exits, and a
|
|
92
|
+
// server that dies before readiness is told the directory spelling — the
|
|
93
|
+
// same trap and the same answer as a one-shot task's (sandbox-request.ts).
|
|
94
|
+
let placeholders: Placeholder[] = []
|
|
95
|
+
if (step.sandbox !== undefined) {
|
|
96
|
+
await args.armSandbox?.()
|
|
97
|
+
// A persistent task never declares `cache` (the loader refuses it), so
|
|
98
|
+
// it keeps the whole linked grant: no key of its can be stale.
|
|
99
|
+
const sb = await sandboxRequestFor(
|
|
100
|
+
node,
|
|
101
|
+
step.sandbox,
|
|
102
|
+
args.workspaceRoot,
|
|
103
|
+
undefined,
|
|
104
|
+
args.nestedProjectDirs,
|
|
105
|
+
args.cacheDir,
|
|
106
|
+
)
|
|
107
|
+
placeholders = sb.placeholders
|
|
108
|
+
// A wrap that refuses (a port the host holds) leaves the server unspawned
|
|
109
|
+
// and the placeholders vx made for it in the project for every later run.
|
|
110
|
+
const wrapped = await wrapSandboxedCommand({
|
|
111
|
+
command: plainCommand,
|
|
112
|
+
cwd: node.projectDir,
|
|
113
|
+
env,
|
|
114
|
+
...sb.sandbox,
|
|
115
|
+
server: true,
|
|
116
|
+
}).catch(async (err: unknown) => {
|
|
117
|
+
await sweepPlaceholders(placeholders)
|
|
118
|
+
throw err
|
|
119
|
+
})
|
|
120
|
+
command = wrapped.wrapped
|
|
121
|
+
bridgeTag = wrapped.tag
|
|
122
|
+
signalChannel = wrapped.forwardsSignals
|
|
123
|
+
}
|
|
124
|
+
// A server's output masked as a one-shot task's is (L-11); `readyWhen`
|
|
125
|
+
// is matched on the raw chunks, before this. Its held tail is flushed by
|
|
126
|
+
// the idle timer: a server never ends on the way to a flush.
|
|
127
|
+
const serverSecrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
128
|
+
const serverOut = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStdout(node, t))
|
|
129
|
+
const serverErr = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStderr(node, t))
|
|
130
|
+
// A server writes until the run stops it: its window stays open.
|
|
131
|
+
args.commandWindows?.open(node.id, node.projectDir, commandWriteReach(node, args.workspaceRoot))
|
|
132
|
+
const persistentOpts: Parameters<typeof runPersistent>[0] = {
|
|
133
|
+
command,
|
|
134
|
+
cwd: node.projectDir,
|
|
135
|
+
env,
|
|
136
|
+
onStdout: serverOut ? (chunk) => serverOut.push(chunk) : (chunk) => log.taskStdout(node, chunk),
|
|
137
|
+
onStderr: serverErr ? (chunk) => serverErr.push(chunk) : (chunk) => log.taskStderr(node, chunk),
|
|
138
|
+
...(args.liveChildren !== undefined ? { liveChildren: args.liveChildren } : {}),
|
|
139
|
+
...(args.track !== undefined
|
|
140
|
+
? { onSpawn: (pid: number) => void args.track!(node.id, pid) }
|
|
141
|
+
: {}),
|
|
142
|
+
...(signalChannel ? { signalChannel } : {}),
|
|
143
|
+
...(args.terminal === true ? { terminal: true } : {}),
|
|
144
|
+
}
|
|
145
|
+
if (step.persistent.readyWhen !== undefined) {
|
|
146
|
+
persistentOpts.readyWhen = step.persistent.readyWhen
|
|
147
|
+
}
|
|
148
|
+
// For a persistent task the timeout bounds the readiness wait. Per-task
|
|
149
|
+
// `exec.timeout` wins; else the run-level default (env/workspace/--timeout).
|
|
150
|
+
const effectiveTimeout = taskTimeoutMs(step) ?? args.timeout
|
|
151
|
+
if (effectiveTimeout !== undefined) {
|
|
152
|
+
persistentOpts.timeoutMs = effectiveTimeout
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A stop that landed during the awaits above (the key, the sandbox's
|
|
156
|
+
// arming, request and wrap) leaves nothing to kill yet: spawned now, the
|
|
157
|
+
// server came up after the teardown and held the run's exit 7 s.
|
|
158
|
+
if (args.stopSignal?.aborted === true) {
|
|
159
|
+
if (bridgeTag !== undefined) releaseBridges(bridgeTag)
|
|
160
|
+
await placeholderSweeper(placeholders)()
|
|
161
|
+
return {
|
|
162
|
+
node,
|
|
163
|
+
status: 'aborted',
|
|
164
|
+
// The signal a spawned server would have been sent (a hang-up forwards SIGTERM).
|
|
165
|
+
exitCode: signalExitCode(forwardedSignal(args.stopSignal.reason)),
|
|
166
|
+
durationMs: 0,
|
|
167
|
+
wallclockStartNs,
|
|
168
|
+
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const spawn = runPersistent(persistentOpts)
|
|
172
|
+
// The host side of a port bridge lives exactly as long as the server:
|
|
173
|
+
// released on the child's exit, whether the run tore it down or it died.
|
|
174
|
+
// A placeholder the server never wrote goes back the same way.
|
|
175
|
+
// ONE sweep, shared by the exit handler and the readiness failure below
|
|
176
|
+
// — whichever asks second must get the SAME list. A second sweep cannot
|
|
177
|
+
// produce it: the first one's `rm` already happened, so the file is gone
|
|
178
|
+
// and `sweepPlaceholders` skips it. Collecting both lists and unioning
|
|
179
|
+
// them does not close that, because the union only covers an exit
|
|
180
|
+
// handler that FINISHED; one still between its `rm` and its return has
|
|
181
|
+
// published nothing yet, and the readiness path then reports no
|
|
182
|
+
// placeholder at all. The failure then says only "File exists", which is
|
|
183
|
+
// the message the hint exists to explain (seen under the gate's parallel
|
|
184
|
+
// load, and reproduced deterministically by delaying each side, item 450).
|
|
185
|
+
const sweptUntouched = placeholderSweeper(placeholders)
|
|
186
|
+
if (bridgeTag !== undefined || placeholders.length > 0) {
|
|
187
|
+
const tag = bridgeTag
|
|
188
|
+
const onExit = async (): Promise<void> => {
|
|
189
|
+
if (tag !== undefined) releaseBridges(tag)
|
|
190
|
+
await sweptUntouched()
|
|
191
|
+
}
|
|
192
|
+
// A spawn that failed has no child to wait on: release now, or the
|
|
193
|
+
// tag would hold the sandbox's reset for the rest of the process.
|
|
194
|
+
if (spawn.child === undefined) void onExit()
|
|
195
|
+
else void spawn.child.exited.then(onExit, onExit)
|
|
196
|
+
}
|
|
197
|
+
// Said once if readiness is slow: a dependency's output is hidden unless
|
|
198
|
+
// it fails, and with no `exec.timeout` the wait never ends, so a run
|
|
199
|
+
// whose `readyWhen` never matched showed nothing at all.
|
|
200
|
+
const readyWhen = step.persistent.readyWhen
|
|
201
|
+
// How long a server may take to match `readyWhen` before vx says it waits.
|
|
202
|
+
const noticeMs = testTimerMs('READY_NOTICE', READY_NOTICE_MS)
|
|
203
|
+
const notice =
|
|
204
|
+
readyWhen === undefined
|
|
205
|
+
? undefined
|
|
206
|
+
: setTimeout(() => {
|
|
207
|
+
if (isAborted(args.stopSignal)) return
|
|
208
|
+
const after = noticeMs < 1000 ? `${noticeMs} ms` : `${noticeMs / 1000} s`
|
|
209
|
+
const unbounded = effectiveTimeout === undefined ? ', with no exec.timeout' : ''
|
|
210
|
+
warnOf(log)(
|
|
211
|
+
`vx: ${node.id} not ready after ${after}: waiting for a line matching /${serverSecrets?.mask(readyWhen) ?? readyWhen}/ (readyWhen)${unbounded}`,
|
|
212
|
+
)
|
|
213
|
+
}, noticeMs)
|
|
214
|
+
if (notice !== undefined) {
|
|
215
|
+
const quiet = (): void => clearTimeout(notice)
|
|
216
|
+
void spawn.ready.then(quiet, quiet)
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await spawn.ready
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// A shell that exited before ready can leave its group running
|
|
222
|
+
// (`server & …; exit`): never registered, so no teardown reached it,
|
|
223
|
+
// and it held its port against the next `vx watch` cycle's server.
|
|
224
|
+
if (err instanceof PersistentReadyError && err.reason === 'exited')
|
|
225
|
+
await terminateChildren(() => [spawn.child])
|
|
226
|
+
// A server the run's stop killed while it started is aborted, as any
|
|
227
|
+
// task the stop kills (item 962): it read `failed (never ready:
|
|
228
|
+
// exited, exit 130)` with a recap after every Ctrl-C (C-62). The stop
|
|
229
|
+
// aborts before it kills, so it is set by the time the child is gone.
|
|
230
|
+
// Read through a call: the early return above narrows `aborted` to
|
|
231
|
+
// false, but the stop can land during `spawn.ready`.
|
|
232
|
+
if (isAborted(args.stopSignal)) {
|
|
233
|
+
return {
|
|
234
|
+
node,
|
|
235
|
+
status: 'aborted',
|
|
236
|
+
exitCode: err instanceof PersistentReadyError ? (err.exitCode ?? 1) : 1,
|
|
237
|
+
durationMs: spawn.readyMs(),
|
|
238
|
+
wallclockStartNs,
|
|
239
|
+
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
243
|
+
// The task's OWN stream, not the process's: the frame is where a
|
|
244
|
+
// reader looks for why a task failed, and a run with a custom logger
|
|
245
|
+
// (an embedder) never saw a bare stderr write at all.
|
|
246
|
+
log.taskStderr(node, `\n[vx] ${node.id}: persistent task failed to become ready: ${message}\n`)
|
|
247
|
+
// A server is never traced, so the sandbox's refusals reach no report:
|
|
248
|
+
// a dev server that died on a file outside its grants read only as the
|
|
249
|
+
// tool's own "not found" (2026-10-03). Said for a sandboxed server that
|
|
250
|
+
// exited failing, which is the shape a refusal takes.
|
|
251
|
+
if (
|
|
252
|
+
step.sandbox !== undefined &&
|
|
253
|
+
err instanceof PersistentReadyError &&
|
|
254
|
+
err.reason === 'exited' &&
|
|
255
|
+
err.exitCode !== 0
|
|
256
|
+
) {
|
|
257
|
+
log.taskStderr(
|
|
258
|
+
node,
|
|
259
|
+
`[vx] ${node.id} ran in the sandbox, which reports nothing for a server: a path outside ` +
|
|
260
|
+
`its grants reads as missing (ENOENT), a refused write as read-only (EROFS). Check ` +
|
|
261
|
+
`exec.sandbox.allow, or run the command as a one-shot sandboxed task to see what it was refused.\n`,
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
// The server is dead or being torn down, so the sweep on its exit may
|
|
265
|
+
// already be running: ask the shared one rather than starting a second.
|
|
266
|
+
for (const p of await sweptUntouched()) {
|
|
267
|
+
log.taskStderr(node, `${untouchedPlaceholderLine(node.projectDir, p)}\n`)
|
|
268
|
+
}
|
|
269
|
+
// The reason rides the outcome (every label reads it), and a child that
|
|
270
|
+
// exited before ready keeps its own exit code rather than a made-up 1.
|
|
271
|
+
// One the readiness timeout is killing reports the signal's, as an
|
|
272
|
+
// ordinary timeout does (X-24).
|
|
273
|
+
// A readiness timeout rejects once the group is gone (runner.ts), so a
|
|
274
|
+
// server that ignored the TERM holds no port into the next `vx watch`
|
|
275
|
+
// cycle, and a second signal here would cut a one-shot handler short.
|
|
276
|
+
const ready = err instanceof PersistentReadyError ? err : undefined
|
|
277
|
+
return {
|
|
278
|
+
node,
|
|
279
|
+
status: 'failed',
|
|
280
|
+
// A server that traps the TERM and exits 0 still timed out, as a
|
|
281
|
+
// one-shot task that does so reports 143 (X-116).
|
|
282
|
+
exitCode:
|
|
283
|
+
ready?.reason === 'timeout'
|
|
284
|
+
? (await spawn.child.exited) || signalExitCode('SIGTERM')
|
|
285
|
+
: (ready?.exitCode ?? 1),
|
|
286
|
+
durationMs: spawn.readyMs(),
|
|
287
|
+
...(ready !== undefined ? { notReady: ready.reason } : {}),
|
|
288
|
+
wallclockStartNs,
|
|
289
|
+
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Ready only once the stop had landed: a trap that prints the marker on
|
|
294
|
+
// the way down, or a race with the signal. The stop is killing it (it
|
|
295
|
+
// is in `liveChildren`), and it served nobody, as a one-shot that exits
|
|
296
|
+
// 0 on the stop is aborted (item 962).
|
|
297
|
+
if (isAborted(args.stopSignal)) {
|
|
298
|
+
return {
|
|
299
|
+
node,
|
|
300
|
+
status: 'aborted',
|
|
301
|
+
exitCode: signalExitCode(forwardedSignal(args.stopSignal!.reason)),
|
|
302
|
+
durationMs: spawn.readyMs(),
|
|
303
|
+
wallclockStartNs,
|
|
304
|
+
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
args.persistentRegistry?.set(node.id, spawn.child)
|
|
308
|
+
forgetUndeclaredWrites(args, undeclaredWriteReach(node, args.workspaceRoot))
|
|
309
|
+
if (mayWriteFingerprint(node, args.workspaceRoot, args.fingerprintWatch?.names ?? [])) {
|
|
310
|
+
args.fingerprintWatch?.wrote()
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
node,
|
|
314
|
+
status: 'success',
|
|
315
|
+
exitCode: 0,
|
|
316
|
+
durationMs: spawn.readyMs(),
|
|
317
|
+
...(hash !== undefined ? { hash } : {}),
|
|
318
|
+
wallclockStartNs,
|
|
319
|
+
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
320
|
+
}
|
|
321
|
+
}
|