rvlib-shipkit 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +5 -8
- package/src/checks/SK001-package-json-shape.check.ts +5 -4
- package/src/checks/SK003-claude-md-shape.check.ts +2 -2
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +10 -6
- package/src/cli/cmd_CacheAudit.ts +43 -0
- package/src/cli/cmd_CrossRepoActivity.ts +2 -1
- package/src/cli/cmd_GlobalProcesses.ts +4 -3
- package/src/cli/cmd_Home.ts +3 -2
- package/src/cli/cmd_Keys.ts +12 -1
- package/src/cli/cmd_Overview.ts +1 -1
- package/src/cli/cmd_Serve.ts +2 -1
- package/src/cli/cmd_TraceAudit.ts +62 -0
- package/src/cli.ts +4 -0
- package/src/collections/collection-types.ts +3 -0
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +6 -3
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/one-lib.ts +59 -4
- package/src/core/read-set.ts +97 -9
- package/src/core/run-ci.ts +6 -1
- package/src/core/run-fixers.ts +8 -2
- package/src/core/trace-audit.ts +120 -0
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-start.ts +11 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/via-hub.ts +14 -0
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +21 -3
- package/src/ledger/git-key.ts +148 -30
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +131 -16
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +210 -25
- package/src/problems/extract.ts +3 -0
- package/src/repo-config.ts +18 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +33 -1
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +71 -5
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/home-st.ts +1 -1
- package/src/serve/hub-app/home-strip.css +5 -9
- package/src/serve/hub-app/home-strip.tsx +4 -17
- package/src/serve/hub-app/hub-app.tsx +18 -4
- package/src/serve/hub-app/hub.css +24 -20
- package/src/serve/hub-app/keys.ts +10 -2
- package/src/serve/hub-app/processes-view.tsx +16 -9
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +10 -0
- package/src/serve/hub-app/repo-sections.ts +31 -17
- package/src/serve/hub-app/repo-st.ts +24 -5
- package/src/serve/hub-app/sidebar.tsx +5 -5
- package/src/serve/hub-app/tree-build.ts +2 -0
- package/src/serve/hub-app/tree-row.tsx +18 -5
- package/src/serve/hub-app/tree-svc.ts +27 -7
- package/src/serve/hub-runtime.ts +4 -2
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// TRACING A TASK: the interposer built once per machine and version, the env a
|
|
2
|
+
// traced spawn carries, and the traceable copies of the binaries macOS would
|
|
3
|
+
// otherwise run blind.
|
|
4
|
+
//
|
|
5
|
+
// Why a copy works: dyld ignores DYLD_INSERT_LIBRARIES in a binary signed with
|
|
6
|
+
// the hardened runtime or library validation (Microsoft's native tsc, Xcode's
|
|
7
|
+
// git), and SIP drops it for everything under /usr and /bin. An ad hoc signed
|
|
8
|
+
// COPY of the same bytes is neither, so it runs traced. It sits beside the
|
|
9
|
+
// original, because a tool finds its own files next to itself (tsc's lib
|
|
10
|
+
// files), or for git in a prefix of links to the original's dirs. The system
|
|
11
|
+
// shells cannot be copied: they are arm64e, which macOS runs only when Apple
|
|
12
|
+
// signed them, so a script that needs /bin/sh stays untraced and says so.
|
|
13
|
+
//
|
|
14
|
+
// The tracer only AUDITS (core/trace-audit.ts): no verdict depends on a trace.
|
|
15
|
+
// It is on wherever it can run (Apple silicon macOS and a C compiler). A
|
|
16
|
+
// machine opts out with `sk trace-audit --disable`, which the daemon's health
|
|
17
|
+
// pass sees, and one run with SHIPKIT_TRACE=0.
|
|
18
|
+
import { createHash } from 'crypto'
|
|
19
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'fs'
|
|
20
|
+
import { homedir, tmpdir } from 'os'
|
|
21
|
+
import { basename, dirname, join } from 'path'
|
|
22
|
+
import { machineDelete, machineGet, machineSet } from '../core/machine-db.ts'
|
|
23
|
+
|
|
24
|
+
/** the env var that turns tracing on (1) or off (0) for one run, over the
|
|
25
|
+
* machine's setting */
|
|
26
|
+
export const TRACE_ENV = 'SHIPKIT_TRACE'
|
|
27
|
+
|
|
28
|
+
/** the machine setting `sk trace-audit --disable` writes (false) */
|
|
29
|
+
export const TRACE_SETTING = 'trace.enabled'
|
|
30
|
+
|
|
31
|
+
let settingMemo: { at: number; on: boolean } | null = null
|
|
32
|
+
/** whether this machine left tracing on, asked at most every few seconds:
|
|
33
|
+
* the composer asks it for every repo */
|
|
34
|
+
export function traceEnabledHere(now = Date.now()): boolean {
|
|
35
|
+
if (settingMemo != null && now - settingMemo.at < 5_000) return settingMemo.on
|
|
36
|
+
settingMemo = { at: now, on: machineGet<boolean>(TRACE_SETTING) !== false }
|
|
37
|
+
return settingMemo.on
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** turn tracing back on for this machine (the default), or off */
|
|
41
|
+
export function setTraceEnabled(on: boolean): void {
|
|
42
|
+
if (on) machineDelete(TRACE_SETTING)
|
|
43
|
+
else machineSet(TRACE_SETTING, false)
|
|
44
|
+
settingMemo = null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SOURCE = join(import.meta.dir, 'interpose.c')
|
|
48
|
+
|
|
49
|
+
/** where the built interposer and the traceable copies live */
|
|
50
|
+
export const traceCache = (): string => process.env['SHIPKIT_TRACE_CACHE'] ?? join(homedir(), '.cache', 'shipkit', 'trace')
|
|
51
|
+
|
|
52
|
+
/** why this machine cannot trace, null when it can. Cheap: no build */
|
|
53
|
+
export function traceUnavailable(env: Record<string, string | undefined> = process.env): string | null {
|
|
54
|
+
const on = env[TRACE_ENV] === '1' || (env[TRACE_ENV] !== '0' && traceEnabledHere())
|
|
55
|
+
if (!on) return env[TRACE_ENV] === '0' ? 'tracing is off for this run (SHIPKIT_TRACE=0)' : 'tracing is off on this machine: sk trace-audit --enable turns it back on'
|
|
56
|
+
if (process.platform !== 'darwin' || process.arch !== 'arm64') return 'tracing runs on Apple silicon macOS only'
|
|
57
|
+
if (compiler() == null) return 'no C compiler to build the tracer (xcode-select --install)'
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const compiler = (): string | null => (existsSync('/usr/bin/clang') ? '/usr/bin/clang' : Bun.which('clang'))
|
|
62
|
+
|
|
63
|
+
let built: { path: string } | { reason: string } | null = null
|
|
64
|
+
|
|
65
|
+
/** the interposer for this source, built on first use. A build that fails is
|
|
66
|
+
* a reason, never a throw: the task then runs untraced and is not kept */
|
|
67
|
+
export function tracerLib(env: Record<string, string | undefined> = process.env): { path: string } | { reason: string } {
|
|
68
|
+
// the switch is asked every time: only the build is remembered
|
|
69
|
+
const off = traceUnavailable(env)
|
|
70
|
+
if (off != null) return { reason: off }
|
|
71
|
+
if (built != null) return built
|
|
72
|
+
let src: Buffer
|
|
73
|
+
try { src = readFileSync(SOURCE) } catch (e) { return (built = { reason: `the tracer source is missing: ${(e as Error).message}` }) }
|
|
74
|
+
const id = createHash('sha256').update(src).digest('hex').slice(0, 16)
|
|
75
|
+
const out = join(traceCache(), `interpose-${id}.dylib`)
|
|
76
|
+
if (existsSync(out)) return (built = { path: out })
|
|
77
|
+
mkdirSync(traceCache(), { recursive: true })
|
|
78
|
+
// built beside its final name and renamed in, so a parallel gate never
|
|
79
|
+
// loads a half-written library
|
|
80
|
+
const tmp = `${out}.${process.pid}.tmp`
|
|
81
|
+
const r = Bun.spawnSync([compiler()!, '-dynamiclib', '-O2', '-arch', 'arm64', '-arch', 'arm64e', '-Wno-deprecated-declarations', '-o', tmp, SOURCE], { stdout: 'pipe', stderr: 'pipe' })
|
|
82
|
+
if (r.exitCode !== 0) {
|
|
83
|
+
rmSync(tmp, { force: true })
|
|
84
|
+
return (built = { reason: `the tracer did not build: ${r.stderr.toString().trim().split('\n')[0] ?? `exit ${r.exitCode}`}` })
|
|
85
|
+
}
|
|
86
|
+
renameSync(tmp, out)
|
|
87
|
+
return (built = { path: out })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** the remap file the interposer reads: original path, copy, and the size and
|
|
91
|
+
* mtime the copy was made from, so a replaced original is never swapped */
|
|
92
|
+
const remapFile = (): string => join(traceCache(), 'remap.tsv')
|
|
93
|
+
|
|
94
|
+
type Remap = { from: string; to: string; size: number; mtime: number; env: string }
|
|
95
|
+
|
|
96
|
+
function readRemaps(): Remap[] {
|
|
97
|
+
try {
|
|
98
|
+
return readFileSync(remapFile(), 'utf8').split('\n').filter((l) => l !== '' && !l.startsWith('#')).map((l) => {
|
|
99
|
+
const [from, to, size, mtime, env] = l.split('\t')
|
|
100
|
+
return { from: from!, to: to!, size: Number(size), mtime: Number(mtime), env: env ?? '' }
|
|
101
|
+
})
|
|
102
|
+
} catch {
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeRemaps(rows: Remap[]): void {
|
|
108
|
+
mkdirSync(traceCache(), { recursive: true })
|
|
109
|
+
const tmp = `${remapFile()}.${process.pid}.tmp`
|
|
110
|
+
writeFileSync(tmp, rows.map((r) => [r.from, r.to, r.size, r.mtime, r.env].join('\t')).join('\n') + '\n')
|
|
111
|
+
renameSync(tmp, remapFile())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fresh = (r: Remap): boolean => {
|
|
115
|
+
try {
|
|
116
|
+
const s = statSync(r.from)
|
|
117
|
+
return s.size === r.size && Math.floor(s.mtimeMs / 1000) === r.mtime && existsSync(r.to)
|
|
118
|
+
} catch {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function upsert(rows: Remap[], from: string, to: string, env = ''): Remap[] {
|
|
124
|
+
const s = statSync(from)
|
|
125
|
+
return [...rows.filter((r) => r.from !== from), { from, to, size: s.size, mtime: Math.floor(s.mtimeMs / 1000), env }]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** an ad hoc signed copy of `bin` at `to`: the same bytes, no hardened runtime */
|
|
129
|
+
function signedCopy(bin: string, to: string): string | null {
|
|
130
|
+
mkdirSync(dirname(to), { recursive: true })
|
|
131
|
+
const tmp = `${to}.${process.pid}.tmp`
|
|
132
|
+
try {
|
|
133
|
+
copyFileSync(bin, tmp)
|
|
134
|
+
chmodSync(tmp, 0o755)
|
|
135
|
+
const r = Bun.spawnSync(['/usr/bin/codesign', '-f', '-s', '-', tmp], { stdout: 'pipe', stderr: 'pipe' })
|
|
136
|
+
if (r.exitCode !== 0) { rmSync(tmp, { force: true }); return `codesign refused it: ${r.stderr.toString().trim().split('\n').at(-1)}` }
|
|
137
|
+
renameSync(tmp, to)
|
|
138
|
+
return null
|
|
139
|
+
} catch (e) {
|
|
140
|
+
rmSync(tmp, { force: true })
|
|
141
|
+
return (e as Error).message
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** /usr/bin/git is a shim into Xcode's git, and both are blind. The copy
|
|
146
|
+
* lives in a prefix whose other dirs link to Xcode's, so it finds its
|
|
147
|
+
* exec-path, templates and system config exactly where the original does */
|
|
148
|
+
function gitRemap(rows: Remap[]): Remap[] {
|
|
149
|
+
if (!existsSync('/usr/bin/git')) return rows
|
|
150
|
+
const found = Bun.spawnSync(['/usr/bin/xcrun', '-f', 'git'], { stdout: 'pipe', stderr: 'pipe' })
|
|
151
|
+
const dev = found.exitCode === 0 ? found.stdout.toString().trim() : ''
|
|
152
|
+
if (dev === '' || !existsSync(dev)) return rows
|
|
153
|
+
const s = statSync(dev)
|
|
154
|
+
const prefix = join(traceCache(), `git-${createHash('sha256').update(`${dev}\0${s.size}\0${s.mtimeMs}`).digest('hex').slice(0, 12)}`)
|
|
155
|
+
const copy = join(prefix, 'usr', 'bin', 'git')
|
|
156
|
+
if (!existsSync(copy)) {
|
|
157
|
+
const devUsr = dirname(dirname(dev))
|
|
158
|
+
mkdirSync(join(prefix, 'usr'), { recursive: true })
|
|
159
|
+
for (const d of readdirSync(devUsr)) if (d !== 'bin' && !existsSync(join(prefix, 'usr', d))) symlinkSync(join(devUsr, d), join(prefix, 'usr', d))
|
|
160
|
+
if (signedCopy(dev, copy) != null) return rows
|
|
161
|
+
}
|
|
162
|
+
return upsert(upsert(rows, '/usr/bin/git', copy), dev, copy)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** the remap file for this run, with every entry still describing its
|
|
166
|
+
* original. Rebuilt when one went stale (a `bun install` replaced a copy) */
|
|
167
|
+
export function remaps(): string {
|
|
168
|
+
let rows = readRemaps()
|
|
169
|
+
const git = rows.find((r) => r.from === '/usr/bin/git')
|
|
170
|
+
if (git == null || !fresh(git)) rows = gitRemap(rows)
|
|
171
|
+
const kept = rows.filter(fresh)
|
|
172
|
+
if (kept.length !== rows.length || !existsSync(remapFile())) writeRemaps(kept)
|
|
173
|
+
return remapFile()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** SIP paths, where no copy runs traced */
|
|
177
|
+
const PROTECTED = /^\/(usr\/(bin|sbin|libexec|lib)|bin|sbin|System)\//
|
|
178
|
+
|
|
179
|
+
/** a binary that ran blind, made traceable for the next run when it can be:
|
|
180
|
+
* a hardened binary a repo installed. Returns what happened, in words, for
|
|
181
|
+
* the line that says the answer was not kept */
|
|
182
|
+
export function learnBlind(paths: string[]): string | null {
|
|
183
|
+
const said: string[] = []
|
|
184
|
+
let rows = readRemaps()
|
|
185
|
+
for (const p of paths) {
|
|
186
|
+
if (PROTECTED.test(p)) { said.push(`macOS lets no tool trace ${p}`); continue }
|
|
187
|
+
let head: Buffer
|
|
188
|
+
try { head = readFileSync(p).subarray(0, 256) } catch { continue }
|
|
189
|
+
if (head.subarray(0, 2).toString() === '#!') {
|
|
190
|
+
const interp = head.toString().slice(2).split('\n')[0]!.trim().split(/\s+/)[0] ?? ''
|
|
191
|
+
said.push(`${basename(p)} runs through ${interp}${PROTECTED.test(interp) ? ', which macOS lets no tool trace' : ''}`)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
const magic = head.readUInt32BE(0)
|
|
195
|
+
if (![0xcffaedfe, 0xcafebabe, 0xfeedfacf].includes(magic)) continue
|
|
196
|
+
// a copy goes beside the original, so only into an install the repo
|
|
197
|
+
// owns, and only for a binary whose signature is what blinds it
|
|
198
|
+
const sign = Bun.spawnSync(['/usr/bin/codesign', '-dv', p], { stdout: 'pipe', stderr: 'pipe' }).stderr.toString()
|
|
199
|
+
if (!p.includes('/node_modules/') || !/flags=0x[0-9a-f]+\((?:[^)]*)(runtime|library-validation)/.test(sign)) { said.push(`${p} runs untraced and cannot be made traceable`); continue }
|
|
200
|
+
const copy = join(dirname(p), `.${basename(p)}.shipkit-traced`)
|
|
201
|
+
const failed = signedCopy(p, copy)
|
|
202
|
+
if (failed != null) { said.push(`${p} cannot be copied: ${failed}`); continue }
|
|
203
|
+
rows = upsert(rows, p, copy)
|
|
204
|
+
said.push(`a traceable copy of ${basename(p)} is ready for the next run`)
|
|
205
|
+
}
|
|
206
|
+
writeRemaps(rows)
|
|
207
|
+
return said.length === 0 ? null : said.join('; ')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** a plain script: commands, arguments, quotes, `&&`, `||`, `;` and `|`.
|
|
211
|
+
* Bun's shell runs exactly that the way sh does, in process, so a traced
|
|
212
|
+
* task keeps its trace; anything else is left to the system shell */
|
|
213
|
+
export function plainScript(body: string): boolean {
|
|
214
|
+
if (!/^[\w\s@%+=:,./'"&|;-]*$/.test(body)) return false
|
|
215
|
+
// an unclosed quote runs the rest of the line into one word
|
|
216
|
+
if ((body.match(/'/g) ?? []).length % 2 === 1 || (body.match(/"/g) ?? []).length % 2 === 1) return false
|
|
217
|
+
const words = body.split(/[\s&|;]+/).filter((w) => w !== '')
|
|
218
|
+
const shellOnly = new Set(['export', 'set', 'source', '.', 'eval', 'exec', 'trap', 'if', 'then', 'else', 'fi', 'for', 'do', 'done', 'while', 'case', 'esac', 'function', 'unset', 'shift', 'read'])
|
|
219
|
+
return !words.some((w) => shellOnly.has(w))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** what a traced task actually runs, where its trace goes, and the env that
|
|
223
|
+
* traces it, or why it cannot be traced. `bun run <script>` runs a plain
|
|
224
|
+
* script through bun's shell, the only one macOS lets a trace into */
|
|
225
|
+
export function tracedCommand(cmd: string[], cwd: string, env: Record<string, string | undefined> = process.env): { cmd: string[]; dir: string; env: Record<string, string>; note: string | null } | { reason: string } {
|
|
226
|
+
const lib = tracerLib(env)
|
|
227
|
+
if ('reason' in lib) return lib
|
|
228
|
+
// the copies first: the root command may be one of the binaries they stand for
|
|
229
|
+
const remap = remaps()
|
|
230
|
+
let run = rootCommand(cmd)
|
|
231
|
+
let note: string | null = null
|
|
232
|
+
if (cmd.length === 3 && basename(cmd[0]!) === 'bun' && cmd[1] === 'run') {
|
|
233
|
+
let body: string | null = null
|
|
234
|
+
try { body = (JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')) as { scripts?: Record<string, string> }).scripts?.[cmd[2]!] ?? null } catch { body = null }
|
|
235
|
+
if (body != null && plainScript(body)) run = [cmd[0]!, 'run', '--shell=bun', cmd[2]!]
|
|
236
|
+
else if (body != null) note = `its script \`${cmd[2]}\` uses shell syntax only /bin/sh runs`
|
|
237
|
+
}
|
|
238
|
+
return { cmd: run, ...traceSpawn(lib.path, remap), note }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** the task's own command, spawned the way the interposer spawns a child:
|
|
242
|
+
* the gate itself runs untraced, so nobody else rewrites a
|
|
243
|
+
* `#!/usr/bin/env node` launcher (a floor slot's `node_modules/.bin/tsc`)
|
|
244
|
+
* or swaps a hardened binary for its traceable copy */
|
|
245
|
+
export function rootCommand(cmd: string[]): string[] {
|
|
246
|
+
if (cmd.length === 0) return cmd
|
|
247
|
+
const first = cmd[0]!
|
|
248
|
+
const path = first.includes('/') ? first : Bun.which(first) ?? first
|
|
249
|
+
let head = ''
|
|
250
|
+
try { head = readFileSync(path).subarray(0, 512).toString() } catch { return cmd }
|
|
251
|
+
if (head.startsWith('#!')) {
|
|
252
|
+
const [interp, prog, ...rest] = head.slice(2).split('\n')[0]!.trim().split(/\s+/)
|
|
253
|
+
if (interp === '/usr/bin/env' && prog != null && !prog.startsWith('-') && rest.length === 0) {
|
|
254
|
+
const resolved = Bun.which(prog)
|
|
255
|
+
if (resolved != null) return [resolved, path, ...cmd.slice(1)]
|
|
256
|
+
}
|
|
257
|
+
return cmd
|
|
258
|
+
}
|
|
259
|
+
const copy = readRemaps().find((r) => r.from === path && fresh(r))
|
|
260
|
+
return copy == null ? cmd : [copy.to, ...cmd.slice(1)]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** a fresh dir for one traced spawn, and the env that traces it. A gate
|
|
264
|
+
* running inside a traced task keeps the outer dir, so both see the run */
|
|
265
|
+
export function traceSpawn(lib: string, remap: string = remaps()): { dir: string; env: Record<string, string> } {
|
|
266
|
+
const dir = mkdtempSync(join(tmpdir(), 'shipkit-trace-'))
|
|
267
|
+
const outer = process.env['SHIPKIT_TRACE_DIR']
|
|
268
|
+
const inserted = process.env['DYLD_INSERT_LIBRARIES']
|
|
269
|
+
return {
|
|
270
|
+
dir,
|
|
271
|
+
env: {
|
|
272
|
+
SHIPKIT_TRACE_DIR: outer == null || outer === '' ? dir : `${outer}:${dir}`,
|
|
273
|
+
DYLD_INSERT_LIBRARIES: inserted == null || inserted === '' ? lib : inserted.split(':').includes(lib) ? inserted : `${lib}:${inserted}`,
|
|
274
|
+
SHIPKIT_TRACE_REMAP: remap,
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** a trace dir goes once it is read */
|
|
280
|
+
export function dropTrace(dir: string): void {
|
|
281
|
+
try { if (lstatSync(dir).isDirectory()) rmSync(dir, { recursive: true, force: true }) } catch { /* gone either way */ }
|
|
282
|
+
}
|