-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrstest.worker-isolation.ts
More file actions
181 lines (167 loc) · 8.02 KB
/
Copy pathrstest.worker-isolation.ts
File metadata and controls
181 lines (167 loc) · 8.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import {
rstestRunIdVariable,
rstestWorkerRootOwnerFile,
rstestWorkerRootPrefix,
rstestWorkerRootsParent,
} from './scripts/rstest-worker-roots.mjs';
export const rstestWorkerId = (): string => process.env['RSTEST_WORKER_ID'] ?? '0';
const hostTemporaryRoot = tmpdir();
/**
* Owner marker for a hashed worker root. The root's name is not predictable
* from outside (the hash includes this process id), so the marker is how a
* sweeper recognizes and removes the roots a finished run left behind without
* touching another run's live roots: the pool's own teardown
* (rstest.global-setup.ts) matches `runId`, the id of the Rstest invocation
* this worker belongs to; scripts/local-ci.mjs matches `temporaryRoot`, its
* per-leg TMPDIR. `runId` is absent when the pool ran without the global
* setup, so no teardown can claim such a root.
*/
export interface RstestWorkerRootOwner {
readonly cwd: string;
readonly pid: number;
readonly runId?: string;
readonly temporaryRoot: string;
readonly workerId: string;
}
export const rstestWorkerRootOwner = (root: string): RstestWorkerRootOwner | undefined => {
const path = join(root, rstestWorkerRootOwnerFile);
if (!existsSync(path)) return undefined;
return JSON.parse(readFileSync(path, 'utf8')) as RstestWorkerRootOwner;
};
const writeOwnerMarker = (root: string, workerId: string): void => {
const path = join(root, rstestWorkerRootOwnerFile);
if (existsSync(path)) return;
const runId = process.env[rstestRunIdVariable];
const owner: RstestWorkerRootOwner = {
cwd: process.cwd(),
pid: process.pid,
...(runId === undefined || runId === '' ? {} : { runId }),
temporaryRoot: hostTemporaryRoot,
workerId,
};
try {
writeFileSync(path, `${JSON.stringify(owner)}\n`, { flag: 'wx' });
} catch (error) {
// Another module of this same invocation won the race; its marker names
// the same owner.
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
}
};
export const rstestWorkerRootPath = (
temporaryRoot: string,
workerId: string,
platform: NodeJS.Platform = process.platform,
invocationId: string = process.cwd() + '\0' + String(process.pid),
): string => {
const hash = createHash('sha256')
.update(temporaryRoot, 'utf8')
.update('\0', 'utf8')
.update(workerId, 'utf8')
.update('\0', 'utf8')
.update(invocationId, 'utf8')
.digest('hex')
.slice(0, 16);
const name = rstestWorkerRootPrefix + hash;
// Linux AF_UNIX fixtures cap the pathname, so Unix roots live under `/tmp`
// rather than a long host TMPDIR. Windows has no such cap and cannot use
// `/tmp`; it still hashes cwd+pid so two processes that reuse worker id 1
// do not share one TEMP directory.
return platform === 'win32' ? join(temporaryRoot, name) : join(rstestWorkerRootsParent, name);
};
export const rstestWorkerRoot = (): string => {
const workerId = rstestWorkerId();
const root = rstestWorkerRootPath(hostTemporaryRoot, workerId);
mkdirSync(root, { recursive: true });
writeOwnerMarker(root, workerId);
// macOS `/tmp` is a symlink to `/private/tmp`. Windows TEMP is often the
// 8.3 form `C:\Users\RUNNER~1\...` while `realpath` of a file under it
// expands to `C:\Users\runneradmin\...`. Install, receipt, and durable-fs
// code realpath destinations; tests that compare `os.tmpdir()` strings to
// those results must see the same spelling. Prefer the owner marker file:
// GetFinalPathNameByHandle expands 8.3 names more reliably for files than
// for the directory handle used to create this root.
const marker = join(root, rstestWorkerRootOwnerFile);
return existsSync(marker) ? dirname(realpathSync(marker)) : realpathSync(root);
};
export const rstestWorkerCacheDirectory = (name: string): string => {
const directory = join(rstestWorkerRoot(), 'cache', name);
mkdirSync(directory, { recursive: true });
return directory;
};
/**
* Where Playwright keeps its bundled browsers while `PLAYWRIGHT_BROWSERS_PATH`
* is unset — the same resolution as playwright-core's registry directory:
* `$XDG_CACHE_HOME/ms-playwright` on Linux (`~/.cache` when the variable is
* unset or empty), `~/Library/Caches/ms-playwright` on macOS, and
* `%LOCALAPPDATA%\ms-playwright` on Windows.
*/
export const playwrightBrowsersPath = (
env: Readonly<Record<string, string | undefined>>,
platform: NodeJS.Platform = process.platform,
home: string = homedir(),
): string => {
if (platform === 'win32') return join(env['LOCALAPPDATA'] ?? join(home, 'AppData', 'Local'), 'ms-playwright');
if (platform === 'darwin') return join(home, 'Library', 'Caches', 'ms-playwright');
const xdgCacheHome = env['XDG_CACHE_HOME'];
return join(xdgCacheHome !== undefined && xdgCacheHome.length > 0 ? xdgCacheHome : join(home, '.cache'), 'ms-playwright');
};
export const isolateWorkerEnvironment = (): void => {
const root = rstestWorkerRoot();
const cache = rstestWorkerCacheDirectory('xdg');
const env = process.env;
// Playwright's bundled-browser registry is a machine-level cache that the
// per-worker XDG_CACHE_HOME below would otherwise hide: with
// AGENT_BUNDLE_PLAYWRIGHT_CHANNEL=chromium (CI) every launch would look for
// the build `playwright install chromium` downloaded in an empty per-worker
// directory. Pin the registry to where Playwright resolved it before the
// override; an explicit PLAYWRIGHT_BROWSERS_PATH (including `0`) wins.
env['PLAYWRIGHT_BROWSERS_PATH'] ??= playwrightBrowsersPath(env);
env['TMPDIR'] = root;
env['TMP'] = root;
env['TEMP'] = root;
env['XDG_CACHE_HOME'] = cache;
// Generated shells with workspace-durable state derive their user state
// root from XDG_STATE_HOME (`resolvePluginRoot` with the `user-data`
// anchor), so every shell a pool spawns writes SQLite under this worker's
// root and never beneath the developer's home.
env['XDG_STATE_HOME'] = rstestWorkerCacheDirectory('xdg-state');
};
let commandSerial = 0;
/**
* The worker's npm cache. It is shared by every command this worker spawns
* (not per command) on purpose: a packed consumer install pulls the package's
* full dependency tree (~180 MB of registry tarballs), and a per-command cache
* made every install in a file start cold — on a CI runner, whose npm cache is
* always empty at job start, that was the whole 30 s budget of each
* public-api-packed test. Sequential commands in one worker now hit the cache
* after the first install, and `--prefer-offline` then skips the registry
* round trips entirely. Concurrent installs within one worker (packed-consumer
* installs two consumers at once) share it safely: cacache is content
* addressed with atomic writes, the same property every developer machine
* relies on for parallel `npm install`s against ~/.npm. Workers never share a
* cache with each other.
*/
export const rstestWorkerNpmCacheDirectory = (): string => rstestWorkerCacheDirectory('npm');
export const isolatedCommandEnvironment = (base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv => {
commandSerial += 1;
const stamp = String(process.pid) + '-' + String(commandSerial);
const cache = rstestWorkerCacheDirectory('cmd-' + stamp);
const tmp = join(rstestWorkerRoot(), 'cmd-tmp-' + stamp);
mkdirSync(tmp, { recursive: true });
const { NODE_PATH: _nodePath, ...rest } = base;
const environment: NodeJS.ProcessEnv = { ...rest };
environment['npm_config_cache'] = rstestWorkerNpmCacheDirectory();
// Rslib's persistent Rspack build cache is keyed by the built config's
// root (`<package>/node_modules/.cache/rspack`), not by `--dist-path`, so
// two workers rebuilding the same package into isolated dists would share
// one cache lock. packages/*/rslib.config.ts honor this override.
environment['AGENT_BUNDLE_RSLIB_CACHE_DIRECTORY'] = join(cache, 'rslib');
environment['TMPDIR'] = tmp;
environment['TMP'] = tmp;
environment['TEMP'] = tmp;
return environment;
};