@andersbakken/fisk 5.0.12 → 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,20 +3027,18 @@ 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);
3030
+ // Flags asking the compiler to record its own argv, mapped to the negation the
3031
+ // same compiler spells it with. -g* forms land in DW_AT_producer, -f* forms in a
3032
+ // .GCC.command.line section; clang uses the command-line names and also accepts
3033
+ // the gcc-switches ones as aliases.
3034
+ const RECORD_FLAG_NEGATIONS = {
3035
+ "-grecord-command-line": "-gno-record-command-line",
3036
+ "-frecord-command-line": "-fno-record-command-line",
3037
+ "-grecord-gcc-switches": "-gno-record-gcc-switches",
3038
+ "-frecord-gcc-switches": "-fno-record-gcc-switches"
3039
+ };
3042
3040
  class Compile extends EventEmitter__default["default"] {
3043
- constructor(args, argv0, dir, debug, sourceFileName, paddedPaths) {
3041
+ constructor(args, argv0, dir, debug, sourceFileName, { clientSourcePath, clientCwd } = {}) {
3044
3042
  super();
3045
3043
  if (!args || !args.length || !dir || !argv0) {
3046
3044
  console.error(argv0, args, dir);
@@ -3165,25 +3163,48 @@ class Compile extends EventEmitter__default["default"] {
3165
3163
  throw new Error("No sourcefile");
3166
3164
  }
3167
3165
  const sourceFileInDir = path__default["default"].join(dir, sourceFileName || path__default["default"].basename(sourcePath));
3168
- if (paddedPaths) {
3169
- // -grecord-command-line embeds our own argv verbatim into
3170
- // DW_AT_producer, which would (a) add two PATH_MAX pads to every CU
3171
- // and (b) leave the builder's /compiles path in the producer, where
3172
- // the client's byte scan would hit the embedded pad and NUL-truncate
3173
- // the rest of the recorded command line. The recorded line would be
3174
- // the builder's rewritten argv anyway -- not the client's -- so drop
3175
- // it rather than record something both wrong and mangled.
3176
- for (let i = args.length - 1; i >= 0; --i) {
3177
- if (args[i] === "-grecord-command-line" || args[i] === "-frecord-command-line") {
3178
- args.splice(i, 1);
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.
3170
+ //
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) {
3176
+ // -grecord-command-line (clang) / -frecord-gcc-switches (gcc) embed
3177
+ // our own argv verbatim into DW_AT_producer or .GCC.command.line,
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.
3181
+ //
3182
+ // Negate each flag in place rather than dropping it and appending one
3183
+ // fixed negation: whichever compiler accepted the positive spelling
3184
+ // necessarily accepts its own negation, whereas a fixed flag is a
3185
+ // guess about the compiler.
3186
+ for (let i = 0; i < args.length; ++i) {
3187
+ const negation = RECORD_FLAG_NEGATIONS[args[i]];
3188
+ if (negation) {
3189
+ args[i] = negation;
3179
3190
  }
3180
3191
  }
3181
- args.push("-gno-record-command-line");
3182
- // The more specific source-file rule must come last: both clang and
3183
- // gcc let a later -fdebug-prefix-map win over an earlier one, and
3184
- // the directory rule is a prefix of the source-file rule.
3185
- args.push(`-fdebug-prefix-map=${dir}=${FISK_CDIR_PAD}`);
3186
- 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}`);
3187
3208
  }
3188
3209
  if (!hasDashX) {
3189
3210
  switch (path__default["default"].extname(sourcePath)) {
@@ -3771,7 +3792,10 @@ process.on("message", (msg) => {
3771
3792
  if (argv.debug) {
3772
3793
  console.log("Creating new compile", msg.commandLine, msg.argv0, msg.dir);
3773
3794
  }
3774
- 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
+ });
3775
3799
  // console.log("running thing", msg.commandLine);
3776
3800
  compile.on("stdout", (data) => {
3777
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.12",
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,7 +58011,32 @@ 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
- const clientMinimumVersion = "5.0.6";
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";
58021
+ // compareVersions throws on an empty or malformed version rather than returning
58022
+ // an ordering, and npmVersion is "" for any client that sends no
58023
+ // x-fisk-npm-version header. Thrown out of the compile handler that would be a
58024
+ // silent hang: uncaughtException keeps the scheduler up, but the request is
58025
+ // abandoned half-handled, so the client waits for a reply that never comes
58026
+ // instead of being told to update. A version we cannot read is not >= the
58027
+ // minimum, so treat it as too old and reject it properly.
58028
+ function clientTooOld(npmVersion) {
58029
+ if (!npmVersion) {
58030
+ return true;
58031
+ }
58032
+ try {
58033
+ return compareVersions(clientMinimumVersion, npmVersion) >= 1;
58034
+ }
58035
+ catch (err) {
58036
+ console.error(`Unparseable client npm version: "${npmVersion}"`, err);
58037
+ return true;
58038
+ }
58039
+ }
58002
58040
  const serverStartTime = Date.now();
58003
58041
  // A stalled event loop stops calling accept(), the listen backlog fills and the
58004
58042
  // kernel then silently drops SYNs, which clients see as a connect timeout rather
@@ -58848,7 +58886,7 @@ function requestEnvironment(compile) {
58848
58886
  return true;
58849
58887
  }
58850
58888
  server.on("clientVerify", (clientVerify) => {
58851
- if (compareVersions(clientMinimumVersion, clientVerify.npmVersion) >= 1) {
58889
+ if (clientTooOld(clientVerify.npmVersion)) {
58852
58890
  clientVerify.send("version_mismatch", { minimum_version: `${clientMinimumVersion}` });
58853
58891
  }
58854
58892
  else {
@@ -58859,7 +58897,7 @@ server.on("compile", (compile) => {
58859
58897
  compile.on("log", (event) => {
58860
58898
  addLogFile({ source: "client", ip: compile.ip, contents: event.message });
58861
58899
  });
58862
- if (compareVersions(clientMinimumVersion, compile.npmVersion) >= 1) {
58900
+ if (clientTooOld(compile.npmVersion)) {
58863
58901
  ++jobsFailed;
58864
58902
  compile.send("version_mismatch", { minimum_version: `${clientMinimumVersion}` });
58865
58903
  return;