@andersbakken/fisk 5.0.13 → 5.0.14

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.
@@ -3027,18 +3027,6 @@ var output = {
3027
3027
 
3028
3028
  var fs = libExports;
3029
3029
 
3030
- // Padded canonical prefixes for DWARF path patching. The fisk client scans
3031
- // compiled objects for these byte patterns and overwrites them in-place with
3032
- // the real paths. Because the replacement is always shorter, a NUL terminator
3033
- // followed by NUL fill fits inside the original region. This works on both ELF
3034
- // objects and LTO bitcode.
3035
- //
3036
- // The length is PATH_MAX so any path the client could legally hand us fits
3037
- // without truncation.
3038
- // Keep in sync with FISK_PAD_LENGTH in src/client/FiskPathPatcher.cpp.
3039
- const FISK_PAD_LENGTH = 4096;
3040
- const FISK_NAME_PAD = "/fisk-name" + "_".repeat(FISK_PAD_LENGTH - "/fisk-name".length);
3041
- const FISK_CDIR_PAD = "/fisk-cdir" + "_".repeat(FISK_PAD_LENGTH - "/fisk-cdir".length);
3042
3030
  // Flags asking the compiler to record its own argv, mapped to the negation the
3043
3031
  // same compiler spells it with. -g* forms land in DW_AT_producer, -f* forms in a
3044
3032
  // .GCC.command.line section; clang uses the command-line names and also accepts
@@ -3050,7 +3038,7 @@ const RECORD_FLAG_NEGATIONS = {
3050
3038
  "-frecord-gcc-switches": "-fno-record-gcc-switches"
3051
3039
  };
3052
3040
  class Compile extends EventEmitter__default["default"] {
3053
- constructor(args, argv0, dir, debug, sourceFileName, paddedPaths) {
3041
+ constructor(args, argv0, dir, debug, sourceFileName, { clientSourcePath, clientCwd } = {}) {
3054
3042
  super();
3055
3043
  if (!args || !args.length || !dir || !argv0) {
3056
3044
  console.error(argv0, args, dir);
@@ -3175,57 +3163,48 @@ class Compile extends EventEmitter__default["default"] {
3175
3163
  throw new Error("No sourcefile");
3176
3164
  }
3177
3165
  const sourceFileInDir = path__default["default"].join(dir, sourceFileName || path__default["default"].basename(sourcePath));
3178
- // Padding only pays off where the client's byte scan can reach the pads,
3179
- // and it is only *needed* for LLVM bitcode, which DwarfPatcher cannot
3180
- // load. Everywhere else DwarfPatcher already works, and padding actively
3181
- // breaks it: it matches on the old /compiles path still being in
3182
- // .debug_str, so once the pads have replaced it there is nothing for the
3183
- // fallback to match and the pads stay in the debug info.
3166
+ // Bake the client's real paths into the object instead of emitting the
3167
+ // builder's /compiles path for the client to rewrite afterwards. The
3168
+ // client folds these paths into its object-cache key, so a cached object
3169
+ // is only ever handed to a client that wants exactly these values.
3184
3170
  //
3185
- // - non-clang: gcc never emits bitcode, and some gcc builds (the nrdp
3186
- // desktop toolchain, for one) compress .debug_str by default with no
3187
- // flag asking for it, which the byte scan cannot see into.
3188
- // - -gz: same problem, explicitly requested. -Wl, forms are link-time
3189
- // and do not affect the object we hand back, so they do not count.
3190
- const wantsCompressedDebug = args.some((arg) => {
3191
- if (arg.startsWith("-Wl,")) {
3192
- return false;
3193
- }
3194
- return (arg.startsWith("-gz") && arg !== "-gz=none") || arg.includes("compress-debug-sections");
3195
- });
3196
- if (paddedPaths && (!isClang || wantsCompressedDebug)) {
3197
- if (debug) {
3198
- console.log("Not padding paths, leaving them to DwarfPatcher:", "isClang", isClang, "wantsCompressedDebug", wantsCompressedDebug);
3199
- }
3200
- paddedPaths = false;
3201
- }
3202
- if (paddedPaths) {
3171
+ // This is the only approach that covers LTO: with -flto the output is
3172
+ // bitcode, which no ELF-level patcher can load. It also needs no help
3173
+ // for compressed debug sections or for gcc, both of which defeat a byte
3174
+ // scan over the finished object.
3175
+ if (clientSourcePath && clientCwd) {
3203
3176
  // -grecord-command-line (clang) / -frecord-gcc-switches (gcc) embed
3204
3177
  // our own argv verbatim into DW_AT_producer or .GCC.command.line,
3205
- // which would (a) add two PATH_MAX pads to every CU and (b) leave
3206
- // the builder's /compiles path in there, where the client's byte
3207
- // scan would hit the embedded pad and NUL-truncate the rest of the
3208
- // recorded command line. The recorded line would be the builder's
3209
- // rewritten argv anyway -- not the client's -- so turn it off rather
3210
- // than record something both wrong and mangled.
3178
+ // which would leave the builder's /compiles path in there. The
3179
+ // recorded line is our rewritten argv rather than the client's
3180
+ // anyway, so it is misleading as well as leaky -- turn it off.
3211
3181
  //
3212
- // Negate each flag in place instead of dropping it and appending one
3182
+ // Negate each flag in place rather than dropping it and appending one
3213
3183
  // fixed negation: whichever compiler accepted the positive spelling
3214
3184
  // necessarily accepts its own negation, whereas a fixed flag is a
3215
- // guess about the compiler. That guess is what put
3216
- // -gno-record-command-line (clang 11+, and gcc spells it
3217
- // -gno-record-gcc-switches) on gcc command lines.
3185
+ // guess about the compiler.
3218
3186
  for (let i = 0; i < args.length; ++i) {
3219
3187
  const negation = RECORD_FLAG_NEGATIONS[args[i]];
3220
3188
  if (negation) {
3221
3189
  args[i] = negation;
3222
3190
  }
3223
3191
  }
3224
- // The more specific source-file rule must come last: both clang and
3225
- // gcc let a later -fdebug-prefix-map win over an earlier one, and
3226
- // the directory rule is a prefix of the source-file rule.
3227
- args.push(`-fdebug-prefix-map=${dir}=${FISK_CDIR_PAD}`);
3228
- args.push(`-fdebug-prefix-map=${sourceFileInDir}=${FISK_NAME_PAD}`);
3192
+ // -fdebug-prefix-map only, for both compilers. clang also has
3193
+ // -Xclang -main-file-name / -Xclang -fdebug-compilation-dir, which
3194
+ // set the two values outright with no rule-precedence subtlety, but
3195
+ // those are cc1 internals reached through -Xclang and carry no
3196
+ // cross-version compatibility guarantee. -fdebug-prefix-map is a
3197
+ // driver flag that predates every compiler fisk supports (gcc 4.3,
3198
+ // clang 3.8), so it cannot fail on an older toolchain.
3199
+ //
3200
+ // The directory rule comes first and the more specific source-file
3201
+ // rule last: both compilers let a later mapping win, and the
3202
+ // directory is a prefix of the file. On gcc the file rule is inert --
3203
+ // gcc takes DW_AT_name from the #line markers in the preprocessed
3204
+ // source, which already name the client's file -- but it is harmless
3205
+ // there and needed for clang.
3206
+ args.push(`-fdebug-prefix-map=${dir}=${clientCwd}`);
3207
+ args.push(`-fdebug-prefix-map=${sourceFileInDir}=${clientSourcePath}`);
3229
3208
  }
3230
3209
  if (!hasDashX) {
3231
3210
  switch (path__default["default"].extname(sourcePath)) {
@@ -3813,7 +3792,10 @@ process.on("message", (msg) => {
3813
3792
  if (argv.debug) {
3814
3793
  console.log("Creating new compile", msg.commandLine, msg.argv0, msg.dir);
3815
3794
  }
3816
- const compile = new Compile(msg.commandLine, msg.argv0, msg.dir, argv.debug, msg.sourceFileName, msg.paddedPaths);
3795
+ const compile = new Compile(msg.commandLine, msg.argv0, msg.dir, argv.debug, msg.sourceFileName, {
3796
+ clientSourcePath: msg.clientSourcePath,
3797
+ clientCwd: msg.clientCwd
3798
+ });
3817
3799
  // console.log("running thing", msg.commandLine);
3818
3800
  compile.on("stdout", (data) => {
3819
3801
  send({ type: "compileStdOut", id: msg.id, data: data });
@@ -8171,7 +8171,6 @@ class Job extends EventEmitter__default["default"] {
8171
8171
  this.sha1 = data.sha1;
8172
8172
  this.id = data.id;
8173
8173
  this.builderIp = data.builderIp;
8174
- this.paddedPaths = data.paddedPaths;
8175
8174
  this.supportsCompressedResponse = data.supportsCompressedResponse;
8176
8175
  }
8177
8176
  get readyState() {
@@ -54955,7 +54954,6 @@ class Server extends EventEmitter__default["default"] {
54955
54954
  sourcePath: String(req.headers["x-fisk-sourcefile"]),
54956
54955
  user: String(req.headers["x-fisk-user"]),
54957
54956
  supportsCompressedResponse: req.headers["x-fisk-supports-compressed-response"] === "true",
54958
- paddedPaths: req.headers["x-fisk-padded-paths"] === "true",
54959
54957
  ws
54960
54958
  });
54961
54959
  break;
@@ -54990,6 +54988,7 @@ class Server extends EventEmitter__default["default"] {
54990
54988
  client.compressed = json.compressed;
54991
54989
  client.commandLine = json.commandLine;
54992
54990
  client.argv0 = json.argv0;
54991
+ client.cwd = json.cwd;
54993
54992
  client.connectTime = connectTime;
54994
54993
  client.wait = json.wait;
54995
54994
  this.emit("job", client);
@@ -55051,13 +55050,14 @@ class Server extends EventEmitter__default["default"] {
55051
55050
  }
55052
55051
 
55053
55052
  class CompileJob extends EventEmitter__default["default"] {
55054
- constructor(commandLine, argv0, id, vm, sourcePath, paddedPaths) {
55053
+ constructor(commandLine, argv0, id, vm, sourcePath, clientCwd) {
55055
55054
  super();
55056
55055
  this.commandLine = commandLine;
55057
55056
  this.argv0 = argv0;
55058
55057
  this.id = id;
55059
55058
  this.vm = vm;
55060
- this.paddedPaths = paddedPaths;
55059
+ this.sourcePath = sourcePath;
55060
+ this.clientCwd = clientCwd;
55061
55061
  this.dir = path__default["default"].join(vm.root, "compiles", String(this.id));
55062
55062
  this.vmDir = path__default["default"].join("/", "compiles", String(this.id));
55063
55063
  this.sourceFileName = sourcePath ? path__default["default"].basename(sourcePath) : "sourcefile";
@@ -55088,7 +55088,16 @@ class CompileJob extends EventEmitter__default["default"] {
55088
55088
  this.startCompile = Date.now();
55089
55089
  fs$3.closeSync(this.fd);
55090
55090
  this.fd = undefined;
55091
- this.vm.child.send({ type: "compile", commandLine: this.commandLine, argv0: this.argv0, id: this.id, dir: this.vmDir, sourceFileName: this.sourceFileName, paddedPaths: this.paddedPaths }, this.sendCallback.bind(this));
55091
+ this.vm.child.send({
55092
+ type: "compile",
55093
+ commandLine: this.commandLine,
55094
+ argv0: this.argv0,
55095
+ id: this.id,
55096
+ dir: this.vmDir,
55097
+ sourceFileName: this.sourceFileName,
55098
+ clientSourcePath: this.sourcePath,
55099
+ clientCwd: this.clientCwd
55100
+ }, this.sendCallback.bind(this));
55092
55101
  }
55093
55102
  cancel() {
55094
55103
  this.vm.child.send({ type: "cancel", id: this.id }, this.sendCallback.bind(this));
@@ -55199,8 +55208,8 @@ class VM extends EventEmitter__default["default"] {
55199
55208
  }
55200
55209
  });
55201
55210
  }
55202
- startCompile(commandLine, argv0, id, sourcePath, paddedPaths) {
55203
- const compile = new CompileJob(commandLine, argv0, id, this, sourcePath, paddedPaths);
55211
+ startCompile(commandLine, argv0, id, sourcePath, cwd) {
55212
+ const compile = new CompileJob(commandLine, argv0, id, this, sourcePath, cwd);
55204
55213
  this.compiles[compile.id] = compile;
55205
55214
  // console.log("startCompile " + compile.id);
55206
55215
  return compile;
@@ -55211,7 +55220,10 @@ class VM extends EventEmitter__default["default"] {
55211
55220
  }
55212
55221
 
55213
55222
  const Version = 5;
55214
- const ObjectCacheFormatVersion = 5;
55223
+ // 6: objects now carry the client's real source path and compilation dir,
55224
+ // baked in at compile time instead of the builder's /compiles paths, and the
55225
+ // stored response no longer keeps the paths the client used to patch with.
55226
+ const ObjectCacheFormatVersion = 6;
55215
55227
  function cacheDir(option) {
55216
55228
  let dir = option("cache-dir");
55217
55229
  if (!dir) {
@@ -55265,9 +55277,16 @@ function validateObjectCache(option) {
55265
55277
  buf.writeUInt32BE(ObjectCacheFormatVersion);
55266
55278
  fs$3.writeFileSync(file, buf);
55267
55279
  }
55268
- function common$2(option) {
55280
+ // Only the builder keeps an object cache on disk. The scheduler tracks which
55281
+ // builder holds which sha1 in memory, and the daemon uses cacheDir purely for
55282
+ // the default socket path -- validating an object cache for either created a
55283
+ // directory they never read and, on a format bump, tried to destroy one they do
55284
+ // not necessarily own.
55285
+ function common$2(option, hasObjectCache = false) {
55269
55286
  validateCache(option);
55270
- validateObjectCache(option);
55287
+ if (hasObjectCache) {
55288
+ validateObjectCache(option);
55289
+ }
55271
55290
  return {
55272
55291
  cacheDir: cacheDir.bind(undefined, option),
55273
55292
  Version,
@@ -60125,25 +60144,25 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
60125
60144
  console.log(`Usage: fisk-builder [options]
60126
60145
 
60127
60146
  Options:
60128
- --scheduler=URL Scheduler URL (default: localhost:8097)
60129
- --port=PORT Listen port (default: 8096)
60130
- --slots=N Number of compile slots
60131
- --debug Enable debug logging
60132
- --object-cache-size=SIZE Object cache size (e.g. "10gb")
60133
- --object-cache-dir=PATH Object cache directory
60134
- --object-cache-purge-size=N Size to purge cache down to
60147
+ --scheduler=URL Scheduler URL (default: localhost:8097)
60148
+ --port=PORT Listen port (default: 8096)
60149
+ --slots=N Number of compile slots
60150
+ --debug Enable debug logging
60151
+ --object-cache-size=SIZE Object cache size (e.g. "10gb")
60152
+ --object-cache-dir=PATH Object cache directory
60153
+ --object-cache-purge-size=N Size to purge cache down to
60135
60154
  --restart-on-new-environments Restart when new environments arrive
60136
- --name=NAME Builder name
60137
- --hostname=HOST Builder hostname (default: os.hostname())
60138
- --labels=LABELS Builder labels
60139
- --npm-version-file=PATH Path to npm version file
60140
- --keep-compiles Keep compile directories after completion
60141
- --vm-user=USER User for VM processes
60142
- --inform-delay=MS Delay before informing scheduler (default: 5000)
60143
- --quit-on-error-delay=MS Delay before quitting on error
60144
- --loadInterval=MS Load reporting interval (default: 1000)
60145
- --backlog=N Listen backlog (default: 50)
60146
- --cache-dir=PATH Cache directory (default: ~/.cache/fisk/builder)
60155
+ --name=NAME Builder name
60156
+ --hostname=HOST Builder hostname (default: os.hostname())
60157
+ --labels=LABELS Builder labels
60158
+ --npm-version-file=PATH Path to npm version file
60159
+ --keep-compiles Keep compile directories after completion
60160
+ --vm-user=USER User for VM processes
60161
+ --inform-delay=MS Delay before informing scheduler (default: 5000)
60162
+ --quit-on-error-delay=MS Delay before quitting on error
60163
+ --loadInterval=MS Load reporting interval (default: 1000)
60164
+ --backlog=N Listen backlog (default: 50)
60165
+ --cache-dir=PATH Cache directory (default: ~/.cache/fisk/builder)
60147
60166
 
60148
60167
  Config files: ~/.config/fisk/builder.conf, /etc/xdg/fisk/builder.conf
60149
60168
  Environment variables: FISK_BUILDER_SCHEDULER, FISK_BUILDER_PORT, etc.`);
@@ -60154,7 +60173,7 @@ const option = createOptions({
60154
60173
  noApplicationPath: true,
60155
60174
  additionalFiles: ["fisk/builder.conf.override"]
60156
60175
  });
60157
- const common = common$2(option);
60176
+ const common = common$2(option, true);
60158
60177
  if (process.getuid() !== 0) {
60159
60178
  console.error("fisk builder needs to run as root to be able to chroot");
60160
60179
  process.exit(1);
@@ -60922,7 +60941,7 @@ server.on("job", (job) => {
60922
60941
  console.log("Starting job", j.id, jobJob.sourcePath, "for", jobJob.ip, jobJob.name, "wait", jobJob.wait);
60923
60942
  assert__default["default"](jobJob.commandLine, "Must have commandLine");
60924
60943
  assert__default["default"](jobJob.argv0, "Must have argv0");
60925
- j.op = vm.startCompile(jobJob.commandLine, jobJob.argv0, jobJob.id, jobJob.sourcePath, jobJob.paddedPaths);
60944
+ j.op = vm.startCompile(jobJob.commandLine, jobJob.argv0, jobJob.id, jobJob.sourcePath, jobJob.cwd);
60926
60945
  if (j.buffer) {
60927
60946
  j.op.feed(j.buffer);
60928
60947
  j.buffer = undefined;
@@ -60988,8 +61007,13 @@ server.on("job", (job) => {
60988
61007
  success: event.success,
60989
61008
  exitCode: event.exitCode,
60990
61009
  sha1: jobJob.sha1,
60991
- sourcePath: path__default["default"].join(j.op.vmDir, j.op.sourceFileName),
60992
- originalSourcePath: jobJob.sourcePath,
61010
+ // Just the basename. This goes into the object cache and is
61011
+ // replayed on every hit, so it has to still mean something
61012
+ // later: the /compiles/<id> directory belongs to this one
61013
+ // job's chroot, and the requesting client's own path belongs
61014
+ // to whichever client happened to compile it first. Only the
61015
+ // file name survives being shared.
61016
+ sourcePath: j.op.sourceFileName,
60993
61017
  stderr: j.stderr,
60994
61018
  stdout: j.stdout
60995
61019
  };
@@ -2,62 +2,30 @@
2
2
  'use strict';
3
3
 
4
4
  var crypto = require('crypto');
5
- var child_process = require('child_process');
6
- var require$$1 = require('fs');
7
- var require$$4 = require('util');
8
- var path$h = require('path');
9
5
  var EventEmitter = require('events');
6
+ var require$$1 = require('fs');
10
7
  var require$$0 = require('constants');
11
8
  var require$$0$1 = require('stream');
9
+ var require$$4 = require('util');
12
10
  var assert$1 = require('assert');
11
+ var path$h = require('path');
13
12
  var os$1 = require('os');
14
13
  var net = require('net');
15
14
  var require$$1$1 = require('module');
16
15
 
17
16
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
18
17
 
19
- var require$$1__default = /*#__PURE__*/_interopDefaultLegacy(require$$1);
20
- var require$$4__default = /*#__PURE__*/_interopDefaultLegacy(require$$4);
21
- var path__default = /*#__PURE__*/_interopDefaultLegacy(path$h);
22
18
  var EventEmitter__default = /*#__PURE__*/_interopDefaultLegacy(EventEmitter);
19
+ var require$$1__default = /*#__PURE__*/_interopDefaultLegacy(require$$1);
23
20
  var require$$0__default = /*#__PURE__*/_interopDefaultLegacy(require$$0);
24
21
  var require$$0__default$1 = /*#__PURE__*/_interopDefaultLegacy(require$$0$1);
22
+ var require$$4__default = /*#__PURE__*/_interopDefaultLegacy(require$$4);
25
23
  var assert__default = /*#__PURE__*/_interopDefaultLegacy(assert$1);
24
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path$h);
26
25
  var os__default = /*#__PURE__*/_interopDefaultLegacy(os$1);
27
26
  var net__default = /*#__PURE__*/_interopDefaultLegacy(net);
28
27
  var require$$1__default$1 = /*#__PURE__*/_interopDefaultLegacy(require$$1$1);
29
28
 
30
- /******************************************************************************
31
- Copyright (c) Microsoft Corporation.
32
-
33
- Permission to use, copy, modify, and/or distribute this software for any
34
- purpose with or without fee is hereby granted.
35
-
36
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
37
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
38
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
39
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
40
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
41
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
42
- PERFORMANCE OF THIS SOFTWARE.
43
- ***************************************************************************** */
44
-
45
- function __awaiter(thisArg, _arguments, P, generator) {
46
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
47
- return new (P || (P = Promise))(function (resolve, reject) {
48
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
49
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
50
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
51
- step((generator = generator.apply(thisArg, _arguments || [])).next());
52
- });
53
- }
54
-
55
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
56
- var e = new Error(message);
57
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
58
- };
59
-
60
- const execFileAsync = require$$4.promisify(child_process.execFile);
61
29
  // Fingerprinting strategy:
62
30
  //
63
31
  // A compiler's "identity" for distributed-compile purposes is the set of
@@ -85,8 +53,17 @@ const execFileAsync = require$$4.promisify(child_process.execFile);
85
53
  // macros. Those strings are frozen at compiler-build time, not install
86
54
  // time, so they are identical across machines that installed the same
87
55
  // compiler package.
56
+ //
57
+ // Who runs the probes:
58
+ //
59
+ // The daemon never executes a compiler. The compiler generally lives inside
60
+ // the client's container and its path does not resolve in the daemon's mount
61
+ // namespace, so the daemon cannot stat it let alone run it. Instead the daemon
62
+ // asks one client to run the probes and send the raw output back, and the
63
+ // daemon does the parsing and hashing here. Keeping canonicalisation on this
64
+ // side means there is exactly one implementation of it -- a second one in the
65
+ // client would eventually diverge and silently break client/builder matching.
88
66
  const PROBE_TIMEOUT_MS = 10000;
89
- const PROBE_MAX_BUFFER = 4 * 1024 * 1024;
90
67
  const PROBES = [
91
68
  { label: "dumpmachine", args: ["-dumpmachine"], required: true },
92
69
  { label: "dumpversion", args: ["-dumpversion"], required: true },
@@ -94,23 +71,6 @@ const PROBES = [
94
71
  { label: "builtins-c", args: ["-x", "c", "-E", "-dM", "/dev/null"], required: true },
95
72
  { label: "builtins-cxx", args: ["-x", "c++", "-E", "-dM", "/dev/null"], required: true }
96
73
  ];
97
- function runProbe(exec, probe) {
98
- return __awaiter(this, void 0, void 0, function* () {
99
- try {
100
- const { stdout, stderr } = yield execFileAsync(exec, [...probe.args], {
101
- timeout: PROBE_TIMEOUT_MS,
102
- maxBuffer: PROBE_MAX_BUFFER
103
- });
104
- return `${stdout}${stderr}`;
105
- }
106
- catch (err) {
107
- if (probe.required) {
108
- throw new Error(`Probe '${probe.label}' failed for ${exec}: ${err instanceof Error ? err.message : String(err)}`);
109
- }
110
- return null;
111
- }
112
- });
113
- }
114
74
  // Emulate the C++ sscanf cascade "%d.%d.%d" -> "%d.%d" -> "%d".
115
75
  function parseVersion(text) {
116
76
  const three = /^(\d+)\.(\d+)\.(\d+)/.exec(text);
@@ -182,19 +142,26 @@ function versionFromMacros(macros, type) {
182
142
  }
183
143
  return { major: 0, minor: 0, patch: 0 };
184
144
  }
185
- function gatherProbes(exec) {
186
- return __awaiter(this, void 0, void 0, function* () {
187
- const results = yield Promise.all(PROBES.map((p) => runProbe(exec, p)));
188
- const [dumpmachine, dumpversion, dumpfullversion, builtinsC, builtinsCxx] = results;
189
- // The required probes cannot be null because runProbe would have thrown.
190
- return {
191
- dumpmachine: (dumpmachine !== null && dumpmachine !== void 0 ? dumpmachine : "").trim(),
192
- dumpversion: (dumpversion !== null && dumpversion !== void 0 ? dumpversion : "").trim(),
193
- dumpfullversion: dumpfullversion === null ? null : dumpfullversion.trim(),
194
- builtinsC: builtinsC !== null && builtinsC !== void 0 ? builtinsC : "",
195
- builtinsCxx: builtinsCxx !== null && builtinsCxx !== void 0 ? builtinsCxx : ""
196
- };
197
- });
145
+ // Turn a client's reported results into the shape the fingerprint wants,
146
+ // failing if a required probe is missing. A client that reports nothing for a
147
+ // required probe is telling us it could not identify the compiler, which must
148
+ // not silently become a fingerprint of empty strings -- every such compiler
149
+ // would hash the same.
150
+ function toProbeOutputs(results) {
151
+ for (const probe of PROBES) {
152
+ if (probe.required && !results[probe.label]) {
153
+ throw new Error(`Required probe '${probe.label}' produced no output`);
154
+ }
155
+ }
156
+ const value = (label) => { var _a; return (_a = results[label]) !== null && _a !== void 0 ? _a : ""; };
157
+ const full = results.dumpfullversion;
158
+ return {
159
+ dumpmachine: value("dumpmachine").trim(),
160
+ dumpversion: value("dumpversion").trim(),
161
+ dumpfullversion: full ? full.trim() : null,
162
+ builtinsC: value("builtins-c"),
163
+ builtinsCxx: value("builtins-cxx")
164
+ };
198
165
  }
199
166
  // Build the canonical fingerprint blob whose SHA becomes the compiler hash.
200
167
  // Fields are separated by NUL to avoid ambiguity if any probe output
@@ -218,76 +185,163 @@ function canonicalFingerprint(p) {
218
185
  ];
219
186
  return Buffer.from(parts.join("\0"), "utf8");
220
187
  }
221
- function createCompilerInfo(exec) {
188
+ function createCompilerInfo(results) {
222
189
  var _a;
223
- return __awaiter(this, void 0, void 0, function* () {
224
- const probes = yield gatherProbes(exec);
225
- const type = detectTypeFromMacros(probes.builtinsC);
226
- const versionFromMac = versionFromMacros(probes.builtinsC, type);
227
- const version = versionFromMac.major !== 0
228
- ? versionFromMac
229
- : parseVersion((_a = probes.dumpfullversion) !== null && _a !== void 0 ? _a : probes.dumpversion);
230
- const blob = canonicalFingerprint(probes);
231
- const hash = crypto.createHash("sha1").update(blob).digest("hex").toUpperCase();
232
- // `input` is retained for debug/traceability: it lets a human see what
233
- // went into the hash without needing to re-probe the compiler. Keep it
234
- // small: just the identifying strings, not the full macro dumps.
235
- const input = [
236
- `type=${type}`,
237
- `target=${probes.dumpmachine}`,
238
- `version=${version.major}.${version.minor}.${version.patch}`,
239
- `dumpversion=${probes.dumpversion}`,
240
- probes.dumpfullversion ? `dumpfullversion=${probes.dumpfullversion}` : ""
241
- ]
242
- .filter((s) => s.length > 0)
243
- .join("\n");
244
- return { hash, input, type, version };
245
- });
246
- }
247
- class CompilerInfoCache {
248
- constructor() {
190
+ const probes = toProbeOutputs(results);
191
+ const type = detectTypeFromMacros(probes.builtinsC);
192
+ const versionFromMac = versionFromMacros(probes.builtinsC, type);
193
+ const version = versionFromMac.major !== 0
194
+ ? versionFromMac
195
+ : parseVersion((_a = probes.dumpfullversion) !== null && _a !== void 0 ? _a : probes.dumpversion);
196
+ const blob = canonicalFingerprint(probes);
197
+ const hash = crypto.createHash("sha1").update(blob).digest("hex").toUpperCase();
198
+ // `input` is retained for debug/traceability: it lets a human see what
199
+ // went into the hash without needing to re-probe the compiler. Keep it
200
+ // small: just the identifying strings, not the full macro dumps.
201
+ const input = [
202
+ `type=${type}`,
203
+ `target=${probes.dumpmachine}`,
204
+ `version=${version.major}.${version.minor}.${version.patch}`,
205
+ `dumpversion=${probes.dumpversion}`,
206
+ probes.dumpfullversion ? `dumpfullversion=${probes.dumpfullversion}` : ""
207
+ ]
208
+ .filter((s) => s.length > 0)
209
+ .join("\n");
210
+ return { hash, input, type, version };
211
+ }
212
+ // A client-supplied key must not be trusted to be small: it lands in a Map
213
+ // that lives as long as the daemon.
214
+ const MAX_KEY_LENGTH = 256;
215
+ function clearTimer(entry) {
216
+ if (entry.timer) {
217
+ clearTimeout(entry.timer);
218
+ entry.timer = undefined;
219
+ }
220
+ }
221
+ // Caches compiler fingerprints, obtaining them from clients rather than by
222
+ // running anything.
223
+ //
224
+ // The key is opaque here and comes from the client -- it identifies "the same
225
+ // compiler file" well enough to decide whether to re-probe. It deliberately is
226
+ // not the fingerprint: we need something cheap to compute *before* probing.
227
+ //
228
+ // Only one client is asked per key. Everyone else waits on the same answer,
229
+ // which is what keeps a cold parallel build from probing the same compiler
230
+ // once per job. Callers get a promise, so the daemon's existing "await the
231
+ // info, then hand back a slot" flow already holds those clients' slots for
232
+ // the duration without any extra slot bookkeeping.
233
+ class CompilerInfoStore {
234
+ constructor(timeoutMs = PROBE_TIMEOUT_MS * 2, log = () => {
235
+ /* quiet by default */
236
+ }) {
237
+ this.timeoutMs = timeoutMs;
238
+ this.log = log;
249
239
  this.cache = new Map();
250
240
  this.pending = new Map();
251
241
  }
252
- get(compilerPath) {
253
- return __awaiter(this, void 0, void 0, function* () {
254
- if (typeof compilerPath !== "string" || compilerPath.length === 0) {
255
- throw new Error("CompilerInfoCache.get: compilerPath must be a non-empty string");
256
- }
257
- // Resolve symlinks so that /usr/bin/clang and /usr/bin/clang-18
258
- // (when the former is a symlink to the latter) share a cache entry.
259
- const absPath = yield require$$1.promises.realpath(path__default["default"].resolve(compilerPath));
260
- const stat = yield require$$1.promises.stat(absPath);
261
- const key = `${absPath}:${stat.mtimeMs}`;
262
- const cached = this.cache.get(key);
263
- if (cached) {
264
- return cached;
242
+ get(key, requester) {
243
+ if (typeof key !== "string" || key.length === 0 || key.length > MAX_KEY_LENGTH) {
244
+ return Promise.reject(new Error("compiler key must be a non-empty string of sane length"));
245
+ }
246
+ const cached = this.cache.get(key);
247
+ if (cached) {
248
+ return Promise.resolve(cached);
249
+ }
250
+ return new Promise((resolve, reject) => {
251
+ let entry = this.pending.get(key);
252
+ if (!entry) {
253
+ entry = { waiters: [], triedIds: new Set() };
254
+ this.pending.set(key, entry);
265
255
  }
266
- const inflight = this.pending.get(key);
267
- if (inflight) {
268
- return inflight;
256
+ entry.waiters.push({ requester, resolve, reject });
257
+ // Someone is already probing this compiler; just wait for them.
258
+ if (entry.electedId === undefined) {
259
+ this.elect(key, entry);
269
260
  }
270
- const compute = CompilerInfoCache.compute(absPath).then((info) => {
271
- this.cache.set(key, info);
272
- return info;
273
- });
274
- this.pending.set(key, compute);
275
- // Clean up the pending map on both success and failure so a failed
276
- // lookup doesn't wedge the key forever.
277
- compute
278
- .finally(() => {
279
- this.pending.delete(key);
280
- })
281
- .catch(() => {
282
- /* rejection observed by caller via the returned promise */
283
- });
284
- return compute;
285
261
  });
286
262
  }
287
- static compute(absPath) {
288
- return __awaiter(this, void 0, void 0, function* () {
289
- return createCompilerInfo(absPath);
290
- });
263
+ // The elected client reported probe output.
264
+ provide(key, results) {
265
+ const entry = this.pending.get(key);
266
+ let info;
267
+ try {
268
+ info = createCompilerInfo(results);
269
+ }
270
+ catch (err) {
271
+ const message = err instanceof Error ? err.message : String(err);
272
+ this.log("compilerInfo for", key, "was unusable:", message);
273
+ if (entry) {
274
+ this.reelect(key, entry, message);
275
+ }
276
+ return;
277
+ }
278
+ this.cache.set(key, info);
279
+ if (!entry) {
280
+ return;
281
+ }
282
+ this.finish(key, entry);
283
+ for (const waiter of entry.waiters) {
284
+ waiter.resolve(info);
285
+ }
286
+ }
287
+ // The elected client could not probe the compiler.
288
+ fail(key, error) {
289
+ const entry = this.pending.get(key);
290
+ if (entry) {
291
+ this.reelect(key, entry, error);
292
+ }
293
+ }
294
+ // A client went away. If it owed us an answer, ask someone else.
295
+ clientGone(requester) {
296
+ for (const [key, entry] of this.pending) {
297
+ entry.waiters = entry.waiters.filter((w) => w.requester.id !== requester.id);
298
+ if (entry.electedId === requester.id) {
299
+ this.reelect(key, entry, "client disconnected before reporting compiler info");
300
+ }
301
+ else if (!entry.waiters.length) {
302
+ this.finish(key, entry);
303
+ }
304
+ }
305
+ }
306
+ elect(key, entry) {
307
+ var _a, _b;
308
+ const next = entry.waiters.find((w) => !entry.triedIds.has(w.requester.id));
309
+ if (!next) {
310
+ // Nobody left who has not already failed us.
311
+ const waiters = entry.waiters;
312
+ this.finish(key, entry);
313
+ const err = new Error("no client could provide compiler info");
314
+ for (const waiter of waiters) {
315
+ waiter.reject(err);
316
+ }
317
+ return;
318
+ }
319
+ entry.electedId = next.requester.id;
320
+ entry.triedIds.add(next.requester.id);
321
+ entry.timer = setTimeout(() => {
322
+ this.log("compilerInfo probe timed out for", key, "client", next.requester.id);
323
+ this.reelect(key, entry, "timed out waiting for compiler info");
324
+ }, this.timeoutMs);
325
+ // Do not let a pending probe hold the process open.
326
+ (_b = (_a = entry.timer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
327
+ this.log("asking client", next.requester.id, "to probe compiler", key);
328
+ try {
329
+ next.requester.requestCompilerInfo(key, PROBES, PROBE_TIMEOUT_MS);
330
+ }
331
+ catch (err) {
332
+ this.log("failed to ask client", next.requester.id, err);
333
+ this.reelect(key, entry, "could not ask client to probe");
334
+ }
335
+ }
336
+ reelect(key, entry, why) {
337
+ this.log("re-electing for", key, "-", why);
338
+ clearTimer(entry);
339
+ entry.electedId = undefined;
340
+ this.elect(key, entry);
341
+ }
342
+ finish(key, entry) {
343
+ clearTimer(entry);
344
+ this.pending.delete(key);
291
345
  }
292
346
  }
293
347
 
@@ -3762,7 +3816,10 @@ class Slots extends EventEmitter__default["default"] {
3762
3816
  }
3763
3817
 
3764
3818
  const Version = 5;
3765
- const ObjectCacheFormatVersion = 5;
3819
+ // 6: objects now carry the client's real source path and compilation dir,
3820
+ // baked in at compile time instead of the builder's /compiles paths, and the
3821
+ // stored response no longer keeps the paths the client used to patch with.
3822
+ const ObjectCacheFormatVersion = 6;
3766
3823
  function cacheDir(option) {
3767
3824
  let dir = option("cache-dir");
3768
3825
  if (!dir) {
@@ -3816,9 +3873,16 @@ function validateObjectCache(option) {
3816
3873
  buf.writeUInt32BE(ObjectCacheFormatVersion);
3817
3874
  fs.writeFileSync(file, buf);
3818
3875
  }
3819
- function common$1(option) {
3876
+ // Only the builder keeps an object cache on disk. The scheduler tracks which
3877
+ // builder holds which sha1 in memory, and the daemon uses cacheDir purely for
3878
+ // the default socket path -- validating an object cache for either created a
3879
+ // directory they never read and, on a format bump, tried to destroy one they do
3880
+ // not necessarily own.
3881
+ function common$1(option, hasObjectCache = false) {
3820
3882
  validateCache(option);
3821
- validateObjectCache(option);
3883
+ if (hasObjectCache) {
3884
+ validateObjectCache(option);
3885
+ }
3822
3886
  return {
3823
3887
  cacheDir: cacheDir.bind(undefined, option),
3824
3888
  Version,
@@ -4536,7 +4600,9 @@ const localSlotCount = option.int("local-slots", 0);
4536
4600
  const localSlots = new Slots(localSlotCount, "local", debug);
4537
4601
  const localSlotsMaxLoad = option("local-slots-max-load") || 0;
4538
4602
  console.log(`cpp slots: ${cppSlots.capacity}, compile slots: ${compileSlots.capacity}, local slots: ${localSlots.capacity}, local max load: ${localSlotsMaxLoad}`);
4539
- const compilerInfoCache = new CompilerInfoCache();
4603
+ const compilerInfoStore = new CompilerInfoStore(undefined, (...args) => {
4604
+ console.log("compilerInfo:", ...args);
4605
+ });
4540
4606
  const slotSubscribers = [];
4541
4607
  function slotsInfo() {
4542
4608
  return {
@@ -4659,18 +4725,27 @@ server.on("compile", (compile) => {
4659
4725
  compileSlots.release(compile.id);
4660
4726
  }
4661
4727
  });
4728
+ // The daemon cannot see, let alone run, the compiler: it usually lives in
4729
+ // the client's container. Clients identify it with a key they compute
4730
+ // themselves and run the probes on our behalf when asked.
4731
+ const requester = {
4732
+ id: compile.id,
4733
+ requestCompilerInfo(key, probes, timeoutMs) {
4734
+ compile.send({ type: "compilerInfoRequest", key, probes, timeoutMs });
4735
+ }
4736
+ };
4662
4737
  compile.on("acquireSlot", (msg) => {
4663
4738
  console.log("acquireSlot", msg);
4664
- const compilerPath = msg && typeof msg.compiler === "string" && msg.compiler.length > 0 ? msg.compiler : null;
4665
- const infoResult = compilerPath
4666
- ? compilerInfoCache.get(compilerPath).then((info) => ({ info, error: null }), (err) => {
4739
+ const compilerKey = msg && typeof msg.compilerKey === "string" && msg.compilerKey.length > 0 ? msg.compilerKey : null;
4740
+ const infoResult = compilerKey
4741
+ ? compilerInfoStore.get(compilerKey, requester).then((info) => ({ info, error: null }), (err) => {
4667
4742
  const message = err instanceof Error ? err.message : String(err);
4668
4743
  if (debug) {
4669
- console.log("acquireSlot -> compilerInfoCache failed", compilerPath, message);
4744
+ console.log("acquireSlot -> compilerInfoStore failed", compilerKey, message);
4670
4745
  }
4671
4746
  return { info: null, error: message };
4672
4747
  })
4673
- : Promise.resolve({ info: null, error: "acquireSlot missing compiler path" });
4748
+ : Promise.resolve({ info: null, error: "acquireSlot missing compiler key" });
4674
4749
  infoResult
4675
4750
  .then(({ info, error }) => {
4676
4751
  if (compileClosed) {
@@ -4710,6 +4785,22 @@ server.on("compile", (compile) => {
4710
4785
  console.error("acquireSlot handler failed unexpectedly", err);
4711
4786
  });
4712
4787
  });
4788
+ compile.on("compilerInfoResponse", (msg) => {
4789
+ const key = msg && typeof msg.key === "string" ? msg.key : "";
4790
+ if (!key) {
4791
+ console.error("compilerInfoResponse without a key from", compile.id);
4792
+ return;
4793
+ }
4794
+ if (typeof (msg === null || msg === void 0 ? void 0 : msg.error) === "string" && msg.error.length) {
4795
+ compilerInfoStore.fail(key, msg.error);
4796
+ return;
4797
+ }
4798
+ if (!(msg === null || msg === void 0 ? void 0 : msg.results) || typeof msg.results !== "object") {
4799
+ compilerInfoStore.fail(key, "compilerInfoResponse without results");
4800
+ return;
4801
+ }
4802
+ compilerInfoStore.provide(key, msg.results);
4803
+ });
4713
4804
  compile.on("releaseLocalSlot", () => {
4714
4805
  if (debug) {
4715
4806
  console.log("releaseLocalSlot");
@@ -4725,6 +4816,8 @@ server.on("compile", (compile) => {
4725
4816
  console.error("Got error from fiskc", compile.id, compile.pid, err);
4726
4817
  }
4727
4818
  compileClosed = true;
4819
+ // If this client owed us compiler info, hand the job to another waiter.
4820
+ compilerInfoStore.clientGone(requester);
4728
4821
  if (requestedCppSlot) {
4729
4822
  requestedCppSlot = false;
4730
4823
  cppSlots.release(compile.id);
@@ -4743,6 +4836,8 @@ server.on("compile", (compile) => {
4743
4836
  console.log("got end from", compile.id, compile.pid);
4744
4837
  }
4745
4838
  compileClosed = true;
4839
+ // If this client owed us compiler info, hand the job to another waiter.
4840
+ compilerInfoStore.clientGone(requester);
4746
4841
  if (requestedCppSlot) {
4747
4842
  requestedCppSlot = false;
4748
4843
  cppSlots.release(compile.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andersbakken/fisk",
3
- "version": "5.0.13",
3
+ "version": "5.0.14",
4
4
  "description": "Fisk, a distributed compile system",
5
5
  "scripts": {
6
6
  "lint": "eslint . --ext .ts",
@@ -56152,6 +56152,9 @@ class Server extends require$$0__default$2["default"] {
56152
56152
  ws.close();
56153
56153
  return;
56154
56154
  }
56155
+ // Exact match: this is a wire format, not a compatibility surface. A peer
56156
+ // on a different version is not something to negotiate with, and NaN from
56157
+ // a missing or unparseable header fails this comparison too.
56155
56158
  const configVersion = parseInt(header(req, "x-fisk-config-version") || "");
56156
56159
  if (configVersion !== this.configVersion) {
56157
56160
  ws.send(`{"error": "Bad config version, expected ${this.configVersion}, got ${configVersion}"}`);
@@ -56418,7 +56421,10 @@ class Server extends require$$0__default$2["default"] {
56418
56421
  }
56419
56422
 
56420
56423
  const Version = 5;
56421
- const ObjectCacheFormatVersion = 5;
56424
+ // 6: objects now carry the client's real source path and compilation dir,
56425
+ // baked in at compile time instead of the builder's /compiles paths, and the
56426
+ // stored response no longer keeps the paths the client used to patch with.
56427
+ const ObjectCacheFormatVersion = 6;
56422
56428
  function cacheDir(option) {
56423
56429
  let dir = option("cache-dir");
56424
56430
  if (!dir) {
@@ -56472,9 +56478,16 @@ function validateObjectCache(option) {
56472
56478
  buf.writeUInt32BE(ObjectCacheFormatVersion);
56473
56479
  fs$4.writeFileSync(file, buf);
56474
56480
  }
56475
- function common$1(option) {
56481
+ // Only the builder keeps an object cache on disk. The scheduler tracks which
56482
+ // builder holds which sha1 in memory, and the daemon uses cacheDir purely for
56483
+ // the default socket path -- validating an object cache for either created a
56484
+ // directory they never read and, on a format bump, tried to destroy one they do
56485
+ // not necessarily own.
56486
+ function common$1(option, hasObjectCache = false) {
56476
56487
  validateCache(option);
56477
- validateObjectCache(option);
56488
+ if (hasObjectCache) {
56489
+ validateObjectCache(option);
56490
+ }
56478
56491
  return {
56479
56492
  cacheDir: cacheDir.bind(undefined, option),
56480
56493
  Version,
@@ -57998,12 +58011,13 @@ const option = options({
57998
58011
  const common = common$1(option);
57999
58012
  let nextCommandId = 0;
58000
58013
  const server = new Server(option, common.Version);
58001
- // 5.0.11 is the first client that asks for padded paths and can patch them out
58002
- // of LTO bitcode. An older client silently produces objects with the builder's
58003
- // /compiles paths baked into the debug info -- and because paddedPaths is not
58004
- // part of the object cache key, those objects are then served to up-to-date
58005
- // clients too, so one stale client poisons everyone's backtraces.
58006
- const clientMinimumVersion = "5.0.11";
58014
+ // 5.0.14 is the first client that folds its debug paths into the object cache
58015
+ // key and asks the builder to bake those paths in at compile time. An older
58016
+ // client produces objects carrying the builder's /compiles paths -- unpatchable
58017
+ // for LTO bitcode -- and, because it does not key on those paths, stores them
58018
+ // under a key up-to-date clients also use. One stale client therefore poisons
58019
+ // everyone's backtraces, so it is refused rather than allowed to contribute.
58020
+ const clientMinimumVersion = "5.0.14";
58007
58021
  // compareVersions throws on an empty or malformed version rather than returning
58008
58022
  // an ordering, and npmVersion is "" for any client that sends no
58009
58023
  // x-fisk-npm-version header. Thrown out of the compile handler that would be a