@redue/cli 0.1.0-alpha.21 → 0.1.0-alpha.22
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 +31 -0
- package/docs/user/node-script.md +20 -4
- package/docs/user/tests-direct.md +8 -1
- package/package.json +1 -1
- package/src/cli.mjs +12 -6
- package/src/node-script.mjs +9 -0
- package/src/test-direct.mjs +39 -7
- package/src/test-recorder.cjs +31 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.22 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
A correctness fix for recorded runs. Earlier alphas could report CURRENT for
|
|
6
|
+
a script that replaced itself with another program.
|
|
7
|
+
|
|
8
|
+
- **Fixed: a script that replaced itself with another program could be
|
|
9
|
+
CURRENT.** Node's `process.execve` (Node 22.15 and later) swaps a process
|
|
10
|
+
for another program under the same PID. The recorder did not see it, so
|
|
11
|
+
that program ran unseen. TypeScript 7's `tsc` does this to start its
|
|
12
|
+
native compiler, so alphas 19 to 21 could report a TypeScript 7 typecheck
|
|
13
|
+
as CURRENT while REDUE saw none of what the compiler did. The general
|
|
14
|
+
recipe and the direct test recipe now judge the new program like any
|
|
15
|
+
program a run starts: TypeScript 7's `tsc` makes the run UNVERIFIED and
|
|
16
|
+
names it. Support for TypeScript 7 is planned.
|
|
17
|
+
- Runs that use Node's bindings with file, network or program access
|
|
18
|
+
(`process.binding('fs')`, `'spawn_sync'` and others), or run Node with
|
|
19
|
+
`--expose-internals`, are UNVERIFIED: the recorder cannot follow them.
|
|
20
|
+
- Direct test recipe: a shell a test starts is read by its command, as in the
|
|
21
|
+
general recipe. One that runs another program before it becomes Node makes
|
|
22
|
+
the run UNVERIFIED.
|
|
23
|
+
- General recipe, on Linux: pnpm 12 writes a file with no name in
|
|
24
|
+
`node_modules` when it rewrites its workspace state, which left the first
|
|
25
|
+
run after an install UNVERIFIED. That file now counts as the run's output
|
|
26
|
+
when the pnpm executable ran and no file has its name. Known limit: another
|
|
27
|
+
program's file with no name there, during such a run, is taken as pnpm's.
|
|
28
|
+
- UNVERIFIED reasons for an input that changed during a run name up to five
|
|
29
|
+
files and a count of the rest.
|
|
30
|
+
|
|
31
|
+
- Contributors: the census traces each run on Linux and flags programs the
|
|
32
|
+
recorder missed.
|
|
33
|
+
|
|
3
34
|
## 0.1.0-alpha.21 — 2026-10-25
|
|
4
35
|
|
|
5
36
|
`init` finds more typecheck scripts and no longer stops in large monorepos.
|
package/docs/user/node-script.md
CHANGED
|
@@ -69,12 +69,16 @@ anywhere in it.
|
|
|
69
69
|
|
|
70
70
|
Two installed files are rewritten by tools during a run. pnpm rewrites
|
|
71
71
|
`node_modules/.pnpm-workspace-state-v1.json` at the first run after an
|
|
72
|
-
install, through a temporary `node_modules/.tmpXXXXXX` file
|
|
72
|
+
install, through a temporary `node_modules/.tmpXXXXXX` file, and on Linux
|
|
73
|
+
writes a file with no name there (`O_TMPFILE`), whose writes the file
|
|
74
|
+
watcher reports as `node_modules/#NUMBER`. vue-tsc writes
|
|
73
75
|
`node_modules/.vue-global-types/*.d.ts` before it reads them. These files stay
|
|
74
76
|
inputs. When they are the only files a run changed, and the run wrote them,
|
|
75
77
|
the result is kept with the files as the run left them. A recorded process
|
|
76
78
|
counts as writing a file it wrote; for pnpm's files, the pnpm executable must
|
|
77
|
-
have run. A
|
|
79
|
+
have run. A `#NUMBER` event counts only directly in the root `node_modules`,
|
|
80
|
+
on Linux, when no file has that name. A later change to them makes the
|
|
81
|
+
result STALE.
|
|
78
82
|
|
|
79
83
|
It does not mean a rerun would give the same result: a test that depends on
|
|
80
84
|
the time or on randomness can pass now and fail later on the same inputs.
|
|
@@ -107,6 +111,10 @@ one REDUE can account for:
|
|
|
107
111
|
content;
|
|
108
112
|
- `ps -A -o ppid,pid`, which pnpm loaded into Corepack runs.
|
|
109
113
|
|
|
114
|
+
A process that replaces itself with another program through
|
|
115
|
+
`process.execve` (TypeScript 7's `tsc` does this to start its native
|
|
116
|
+
compiler) is judged the same way as a program it starts.
|
|
117
|
+
|
|
110
118
|
Anything else makes the result UNVERIFIED and names the program: `git`,
|
|
111
119
|
`sleep`, `find`, and native programs from installed packages such as esbuild,
|
|
112
120
|
turbo, tsgo, tsgolint and biome, whose reads, writes and network use REDUE
|
|
@@ -142,11 +150,19 @@ inputs.
|
|
|
142
150
|
run (pnpm 12 does this) can be reported by the file watcher as an event on
|
|
143
151
|
the root itself, an observation gap that makes the run UNVERIFIED. Linux
|
|
144
152
|
reports the file, which is not an input.
|
|
153
|
+
- On Linux, any program's file with no name in the root `node_modules`,
|
|
154
|
+
written during a run that started the pnpm executable, is taken as pnpm's.
|
|
155
|
+
If that program gives the file a real name (any name, `node_modules`
|
|
156
|
+
included) and then writes through the open file, REDUE misses those
|
|
157
|
+
writes. A real file named `node_modules/#NUMBER` that is created and
|
|
158
|
+
removed within such a run is treated the same way. Outside a pnpm run,
|
|
159
|
+
these writes leave the run UNVERIFIED.
|
|
145
160
|
- The pnpm executable and the pnpm launchers' helpers are trusted as
|
|
146
161
|
toolchain: what they read is not checked.
|
|
147
162
|
- Code that copies the environment and reads the copy, and native code that
|
|
148
|
-
reads files or variables (native addons such as
|
|
149
|
-
not seen.
|
|
163
|
+
reads files or variables or starts programs (native addons such as
|
|
164
|
+
Rust-based resolvers), is not seen. Neither are files opened by
|
|
165
|
+
`node:sqlite` or preopened for `node:wasi`.
|
|
150
166
|
- A project whose `.npmrc` sets `node-options` stays UNVERIFIED: npm replaces
|
|
151
167
|
`NODE_OPTIONS` with it, which drops REDUE's recorder.
|
|
152
168
|
- A script that runs a build first (`npm run build && tsc --noEmit`) usually
|
|
@@ -80,7 +80,14 @@ kept but marked UNVERIFIED, with the reason, when the run:
|
|
|
80
80
|
- connected to anything other than loopback or a local socket, or asked DNS
|
|
81
81
|
about a name other than localhost;
|
|
82
82
|
- started a process that did not load the recorder: a shell, a binary, or a
|
|
83
|
-
Node process with its own environment
|
|
83
|
+
Node process with its own environment. A process that replaces itself with
|
|
84
|
+
another program (`process.execve`) counts as starting it, and it must
|
|
85
|
+
become a recorded Node process. A shell is read by its command, as the
|
|
86
|
+
[general recipe](node-script.md#programs) reads it: one that runs another
|
|
87
|
+
program before it becomes Node counts too;
|
|
88
|
+
- used one of Node's bindings with file, network or program access
|
|
89
|
+
(`process.binding('fs')`, `'spawn_sync'` and others), or ran Node with
|
|
90
|
+
`--expose-internals`: the recorder cannot follow those;
|
|
84
91
|
- had an installed file with another hard link, other than a link into the
|
|
85
92
|
pnpm store the install recorded in `node_modules/.modules.yaml` (pnpm's
|
|
86
93
|
default on Linux). A change through a link elsewhere, such as Yarn's
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import {directQualification,directInterpretation,observedNpmContext} from './dir
|
|
|
5
5
|
import {directRunCertificateAllowed,directRunCertificate,directRunRecordable,directRunRecording,probeQuestions,validateRunRecord} from './direct-run-certificate.mjs';
|
|
6
6
|
import {queryAnswer,runFactChange} from './typescript-list.mjs';
|
|
7
7
|
import {changedFiles,coverageReadable,gitIgnored,recordedEnvironment,testEnvironmentIssue,testGuardConfig,testRunGuard,testRunFacts,sharedInstalledFile,testsInterpretation,testsQualification} from './test-direct.mjs';
|
|
8
|
-
import {childProgram,lookup,pnpmConfigFiles,scriptCaches,scriptInstalledPaths,scriptInterpretation,scriptQualification,scriptRunFacts,toolOutput,toolchainDirs} from './node-script.mjs';
|
|
8
|
+
import {childProgram,lookup,pnpmConfigFiles,scriptCaches,scriptInstalledPaths,scriptInterpretation,scriptQualification,scriptRunFacts,pnpmNameless,toolOutput,toolchainDirs} from './node-script.mjs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import net from 'node:net';
|
|
11
11
|
import os from 'node:os';
|
|
@@ -400,7 +400,7 @@ async function execute() {
|
|
|
400
400
|
// what was there (test/tmp). When they are the only observed change, the
|
|
401
401
|
// run is kept with the end checkpoint, which includes them.
|
|
402
402
|
let outputs=null;
|
|
403
|
-
let eventsExplained=false;
|
|
403
|
+
let eventsExplained=false,unexplained=end?.inputEventPaths||null;
|
|
404
404
|
if(guard&&snap&&end&&snap.planId===end.planId&&snap.probeHash===end.probeHash&&snap.observationHealthy&&end.observationHealthy&&
|
|
405
405
|
(snap.inputEventSerial===end.inputEventSerial||end.inputEventPathsComplete===true)){
|
|
406
406
|
// A directory event is not an input change; a change to its files shows
|
|
@@ -408,9 +408,10 @@ async function execute() {
|
|
|
408
408
|
const directory=rel=>{try{return fs.lstatSync(path.join(root,rel)).isDirectory();}catch{return false;}};
|
|
409
409
|
const changed=changedFiles(snap.files,end.files),events=(end.inputEventPaths||[]).filter(rel=>!directory(rel));
|
|
410
410
|
const touched=[...new Set([...changed,...events])].sort();
|
|
411
|
-
const output=rel=>guard.isOutput(rel)||general&&toolOutput(rel,guard);
|
|
411
|
+
const output=rel=>guard.isOutput(rel)||general&&(toolOutput(rel,guard)||pnpmNameless(rel,guard,root,[snap.files,end.files]));
|
|
412
412
|
const ignored=touched.length&&touched.every(output)?gitIgnored(root,touched):new Set();
|
|
413
|
-
|
|
413
|
+
unexplained=touched.filter(file=>!ignored.has(file));
|
|
414
|
+
if(!unexplained.length){eventsExplained=true;if(changed.length)outputs=changed;}
|
|
414
415
|
}
|
|
415
416
|
// The end certificate, recorded or probed, must cover what this compile received.
|
|
416
417
|
// With no input event, gap or content change in between, it also holds at the start.
|
|
@@ -442,8 +443,13 @@ async function execute() {
|
|
|
442
443
|
captureIssues.push('observer generation changed during execution');
|
|
443
444
|
if(snap?.planGeneration!==end?.planGeneration)
|
|
444
445
|
captureIssues.push('input plan rebuilt during execution');
|
|
445
|
-
|
|
446
|
-
|
|
446
|
+
// The reason names up to five files the run did not explain, and a count of the rest.
|
|
447
|
+
if(snap?.inputEventSerial!==end?.inputEventSerial&&!eventsExplained){
|
|
448
|
+
const named=[...new Set(unexplained||[])],more=named.length-5,
|
|
449
|
+
unrecorded=end?.inputEventPathsComplete===false?' (not every event path was kept)':'';
|
|
450
|
+
captureIssues.push('declared input event occurred during execution'+(named.length?
|
|
451
|
+
`: ${named.slice(0,5).join(', ')}${more>0?` and ${more} more`:''}`:'')+unrecorded);
|
|
452
|
+
}
|
|
447
453
|
// A recorded certificate changes only the probe part of the fingerprint.
|
|
448
454
|
if(!outputs&&(!snap||!end||snap.planId!==end.planId||(recording?snap.inputKey!==end.inputKey:snap.fingerprint!==end.fingerprint)||
|
|
449
455
|
snap.revision!==end.revision))captureIssues.push('declared inputs changed or were not observed');
|
package/src/node-script.mjs
CHANGED
|
@@ -57,6 +57,15 @@ export const toolOutputPath=rel=>pnpmState(rel)||pnpmTemp(rel)||vueTypes(rel);
|
|
|
57
57
|
// The run wrote it: a recorded process did, or, for pnpm's files, the pnpm
|
|
58
58
|
// executable ran, which writes them natively.
|
|
59
59
|
export const toolOutput=(rel,guard)=>toolOutputPath(rel)&&(guard.wroteInstalled(rel)||(pnpmState(rel)||pnpmTemp(rel))&&guard.opaque);
|
|
60
|
+
// On Linux, pnpm 12 also writes a file with no name (O_TMPFILE) in the root
|
|
61
|
+
// node_modules, whose writes inotify reports as '#<inode>'. Such an event is
|
|
62
|
+
// the run's output when the pnpm executable ran and no file has that name, at
|
|
63
|
+
// either checkpoint or now. Limit: another program's file with no name there,
|
|
64
|
+
// during a run that started the pnpm executable, is taken as pnpm's too; once
|
|
65
|
+
// linked in under any name, its writes through the open file are missed.
|
|
66
|
+
export const pnpmNameless=(rel,guard,root,checkpoints,platform=process.platform)=>platform==='linux'&&/^node_modules\/#\d+$/.test(rel)&&
|
|
67
|
+
guard.opaque&&checkpoints.every(files=>files&&!Object.hasOwn(files,rel))&&
|
|
68
|
+
(()=>{try{fs.lstatSync(path.join(root,rel));return false;}catch(error){return error.code==='ENOENT';}})();
|
|
60
69
|
// The installed paths the recorder treats as observed by identity.
|
|
61
70
|
export const scriptInstalledPaths=(root,dirs=[])=>[...modulesDirs(dirs),...installState].map(at=>path.join(root,at));
|
|
62
71
|
// Variables a recorded run cannot be seen reading: Node's and libc's (as in
|
package/src/test-direct.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import {queryAnswer} from './typescript-list.mjs';
|
|
|
16
16
|
import {pnpmInstallInfo,pnpmProjectInfo,yaml} from './pnpm-inputs.mjs';
|
|
17
17
|
import {workspaceManifests} from './workspace-manifests.mjs';
|
|
18
18
|
import {atomicJson} from './state-store.mjs';
|
|
19
|
+
// node-script.mjs imports this module too; childProgram is only called at run time.
|
|
20
|
+
import {childProgram} from './node-script.mjs';
|
|
19
21
|
import {racyUntil} from './path-identity.mjs';
|
|
20
22
|
|
|
21
23
|
const digest=value=>createHash('sha256').update(value).digest('hex');
|
|
@@ -73,7 +75,7 @@ export function testRunGuard(dir,{root,sourcePatterns,files,runner,environment=t
|
|
|
73
75
|
const issues=[],add=issue=>{if(!issues.includes(issue))issues.push(issue);};
|
|
74
76
|
const included=patternMatcher(sourcePatterns);
|
|
75
77
|
const observed=rel=>Object.hasOwn(files||{},rel);
|
|
76
|
-
const headers=new Map(),records=[];
|
|
78
|
+
const headers=new Map(),records=[],headerCount=new Map(),threadsStarted=new Map();
|
|
77
79
|
let names=[];
|
|
78
80
|
try{names=fs.readdirSync(dir).filter(name=>name.endsWith('.jsonl'));}
|
|
79
81
|
catch{return {issues:['the run record is unavailable'],runFacts:[],environmentReads:{},isOutput:()=>false};}
|
|
@@ -84,6 +86,9 @@ export function testRunGuard(dir,{root,sourcePatterns,files,runner,environment=t
|
|
|
84
86
|
const header=rows[0]?.[0]==='H'?rows[0][1]:null;
|
|
85
87
|
if(!header||!Number.isSafeInteger(header.pid)||!Number.isSafeInteger(header.threadId)){add('a run record is unreadable');continue;}
|
|
86
88
|
headers.set(`${header.pid}-${header.threadId}`,header);
|
|
89
|
+
// After process.execve, the new program's threads append to the same
|
|
90
|
+
// records: each thread started needs its own header.
|
|
91
|
+
headerCount.set(`${header.pid}-${header.threadId}`,rows.filter(row=>row?.[0]==='H'&&!row[1]?.internal).length);
|
|
87
92
|
records.push({header,rows:rows.slice(1)});
|
|
88
93
|
}
|
|
89
94
|
let opaque=false;
|
|
@@ -101,7 +106,24 @@ export function testRunGuard(dir,{root,sourcePatterns,files,runner,environment=t
|
|
|
101
106
|
// PIDs is that child, not an execSync child (which reports no PID).
|
|
102
107
|
const installedWrites=new Set();
|
|
103
108
|
const reads=new Map(),produced=new Set(),created=new Set(),environmentReads={},spawned=new Map(),childPids=new Map();
|
|
109
|
+
// Whether a started program is accounted for: {issue} or {needsHeader}.
|
|
110
|
+
// The tests recipe wants every program to become a recorded process; a
|
|
111
|
+
// shell, which can run other programs first, is read by its command as
|
|
112
|
+
// the general recipe reads it.
|
|
113
|
+
const judge=(child,main)=>{
|
|
114
|
+
if(!programs){
|
|
115
|
+
const judged=/^(sh|bash|dash|zsh|ksh|fish|csh|tcsh)$/.test(path.basename(String(child.file)))?childProgram(child,{parentMain:main}):{};
|
|
116
|
+
return judged.issue?judged:{needsHeader:true};}
|
|
117
|
+
const judged=programs.judge(child,main);
|
|
118
|
+
if(!judged.issue)opaque||=Boolean(judged.opaque);
|
|
119
|
+
return judged;
|
|
120
|
+
};
|
|
104
121
|
for(const {header,rows} of records){
|
|
122
|
+
// A program this process replaced itself with (process.execve) that must
|
|
123
|
+
// load the recorder: its header follows in this record, from the same
|
|
124
|
+
// parent. Later rows come from the program now running. (A reused PID
|
|
125
|
+
// also appends a header to an old record.)
|
|
126
|
+
let replaced=null,main=header.main??null;
|
|
105
127
|
for(const row of rows){
|
|
106
128
|
const [kind,...value]=Array.isArray(row)?row:[];
|
|
107
129
|
if(kind==='U')add(`recorder: ${value[0]}`);
|
|
@@ -121,16 +143,26 @@ export function testRunGuard(dir,{root,sourcePatterns,files,runner,environment=t
|
|
|
121
143
|
if(kind==='C')childPids.set(header.pid,[...childPids.get(header.pid)||[],value[0]]);
|
|
122
144
|
// The general recipe accounts for each program; one that needs no
|
|
123
145
|
// recorder (a shell, the pnpm executable) is not matched to a header.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if(!judged.needsHeader)continue;}
|
|
146
|
+
const judged=judge({file,args,cwd,searchPath,env,script},main);
|
|
147
|
+
if(judged.issue){add(judged.issue);continue;}
|
|
148
|
+
if(!judged.needsHeader)continue;
|
|
128
149
|
const list=spawned.get(header.pid)||[];list.push({pid:kind==='C'?value[0]:null,file});
|
|
129
150
|
spawned.set(header.pid,list);}
|
|
130
|
-
else if(kind==='
|
|
131
|
-
|
|
151
|
+
else if(kind==='X'){
|
|
152
|
+
const [file,args,cwd,searchPath,env,script]=value;
|
|
153
|
+
const judged=judge({file,args,cwd,searchPath,env,script},main);
|
|
154
|
+
if(judged.issue){add(judged.issue);replaced=null;continue;}
|
|
155
|
+
replaced=judged.needsHeader?file:null;}
|
|
156
|
+
else if(kind==='Y')replaced=null;
|
|
157
|
+
else if(kind==='T'){const key=`${header.pid}-${value[0]}`;threadsStarted.set(key,(threadsStarted.get(key)||0)+1);}
|
|
158
|
+
else if(kind==='H'){
|
|
159
|
+
if(replaced!==null&&value[0]?.ppid!==header.ppid)add(`replaced itself with ${path.basename(String(replaced))}, which REDUE does not record`);
|
|
160
|
+
replaced=null;main=value[0]?.main??null;}
|
|
161
|
+
else add('a run record is unreadable');
|
|
132
162
|
}
|
|
163
|
+
if(replaced!==null)add(`replaced itself with ${path.basename(String(replaced))}, which REDUE does not record`);
|
|
133
164
|
}
|
|
165
|
+
for(const [key,count] of threadsStarted)if((headerCount.get(key)||0)<count)add('a worker thread did not load the REDUE recorder');
|
|
134
166
|
// Every child must be a recorded Node process: by PID when known, otherwise
|
|
135
167
|
// by the number of recorded processes whose parent started it.
|
|
136
168
|
for(const [parent,children] of spawned){
|
package/src/test-recorder.cjs
CHANGED
|
@@ -15,7 +15,7 @@ if(configFile)install();
|
|
|
15
15
|
function install(){
|
|
16
16
|
const original={openSync:fs.openSync,writeSync:fs.writeSync,readFileSync:fs.readFileSync,
|
|
17
17
|
statSync:fs.statSync,readdirSync:fs.readdirSync,real:fs.realpathSync.native};
|
|
18
|
-
const {threadId}=require('node:worker_threads');
|
|
18
|
+
const {threadId,isInternalThread}=require('node:worker_threads');
|
|
19
19
|
let config,fd;
|
|
20
20
|
try{config=JSON.parse(original.readFileSync(configFile,'utf8'));
|
|
21
21
|
fd=original.openSync(path.join(config.dir,`${process.pid}-${threadId}.jsonl`),'a',0o600);}
|
|
@@ -25,7 +25,10 @@ function install(){
|
|
|
25
25
|
const failed=reason=>emit(['U',reason]);
|
|
26
26
|
// The main script tells the guard which program a process runs (a package
|
|
27
27
|
// manager may start helpers of its own).
|
|
28
|
-
|
|
28
|
+
// Node's own threads (the ESM hooks thread) load it too; the guard does not
|
|
29
|
+
// count them as threads the run started.
|
|
30
|
+
emit(['H',{pid:process.pid,ppid:process.ppid,threadId,main:typeof process.argv[1]==='string'?process.argv[1]:null,
|
|
31
|
+
...(isInternalThread===true?{internal:true}:{})}]);
|
|
29
32
|
// Every node_modules observed as installed: the root's and each workspace package's.
|
|
30
33
|
const sep=path.sep,root=config.root,installed=Array.isArray(config.installed)?config.installed:[path.join(root,'node_modules')],
|
|
31
34
|
tmp=config.tmp;
|
|
@@ -395,6 +398,32 @@ function install(){
|
|
|
395
398
|
catch{failed('recorder failure');}}
|
|
396
399
|
};
|
|
397
400
|
}
|
|
401
|
+
// process.execve replaces this process with another program, under the
|
|
402
|
+
// same PID: the row comes first, since a successful call does not return
|
|
403
|
+
// (a failed one aborts the process). Node refuses it in a worker and for
|
|
404
|
+
// bad arguments by throwing; the row is then withdrawn. The kernel does not
|
|
405
|
+
// search PATH, so the file is taken from the working directory. A Node
|
|
406
|
+
// program that replaces this one loads the recorder again and appends a
|
|
407
|
+
// header to the same record.
|
|
408
|
+
if(typeof process.execve==='function'){const execve=process.execve;
|
|
409
|
+
process.execve=function(file,args,env,...rest){
|
|
410
|
+
let row=false;
|
|
411
|
+
if(threadId===0)try{const options={env:env??process.env},full=path.resolve(String(file)),where_=where(options);
|
|
412
|
+
emit(['X',full,argsOf(Array.isArray(args)?args.slice(1):[]),...where_,scriptOf(full,where_[0],null)]);row=true;}
|
|
413
|
+
catch{failed('recorder failure');}
|
|
414
|
+
try{return execve.call(this,file,args,env,...rest);}
|
|
415
|
+
catch(error){if(row)emit(['Y']);throw error;}};}
|
|
416
|
+
// Bindings that read files, use the network or start programs out of the
|
|
417
|
+
// recorder's sight. Only bindings without such access are allowed.
|
|
418
|
+
const harmlessBindings=new Set(['async_wrap','buffer','config','constants','contextify','crypto','http_parser','icu',
|
|
419
|
+
'js_stream','natives','os','signal_wrap','stream_wrap','tty_wrap','url','util','uv','v8','zlib']);
|
|
420
|
+
if(typeof process.binding==='function'){const binding=process.binding;
|
|
421
|
+
process.binding=function(name,...rest){
|
|
422
|
+
const result=binding.call(this,name,...rest);
|
|
423
|
+
if(!harmlessBindings.has(name))failed(`process.binding('${String(name)}') reads files, uses the network or starts programs out of REDUE's sight`);
|
|
424
|
+
return result;};}
|
|
425
|
+
// Node's internal modules include child_process's unhooked internals.
|
|
426
|
+
if(process.execArgv.some(arg=>/^--expose[-_]internals(=|$)/.test(arg)))failed('a Node process exposed its internal modules (--expose-internals)');
|
|
398
427
|
const workers=require('node:worker_threads'),Worker=workers.Worker;
|
|
399
428
|
workers.Worker=class extends Worker{
|
|
400
429
|
constructor(...args){super(...args);try{emit(['T',this.threadId]);}catch{failed('recorder failure');}}
|