Remove unused legacy relay native-cache creation

This commit is contained in:
m4air
2026-09-28 06:42:56 -07:00
parent 6e94a242c3
commit b026aee8b7
9 changed files with 24 additions and 1315 deletions
@@ -1,69 +0,0 @@
Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
The MIT License (MIT)
Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License
Copyright (c) 2018 - present Microsoft Corporation
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,9 +0,0 @@
# Legacy SSH teardown patch fixtures
`windowsPtyAgent.js.txt` and `windowsTerminal.js.txt` are unmodified `lib/` files
from the published npm `node-pty@1.1.0` tarball. The upstream MIT license is in
`LICENSE`. These are test inputs only, not a shipped runtime dependency.
The legacy SSH repair asset verifies both original and patched SHA-256 hashes.
Keeping its published input here lets clean installs test that compatibility
path after the desktop dependency is retired.
@@ -1,320 +0,0 @@
"use strict";
/**
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.argsToCommandLine = exports.WindowsPtyAgent = void 0;
var fs = require("fs");
var os = require("os");
var path = require("path");
var child_process_1 = require("child_process");
var net_1 = require("net");
var windowsConoutConnection_1 = require("./windowsConoutConnection");
var utils_1 = require("./utils");
var conptyNative;
var winptyNative;
/**
* The amount of time to wait for additional data after the conpty shell process has exited before
* shutting down the socket. The timer will be reset if a new data event comes in after the timer
* has started.
*/
var FLUSH_DATA_INTERVAL = 1000;
/**
* This agent sits between the WindowsTerminal class and provides a common interface for both conpty
* and winpty.
*/
var WindowsPtyAgent = /** @class */ (function () {
function WindowsPtyAgent(file, args, env, cwd, cols, rows, debug, _useConpty, _useConptyDll, conptyInheritCursor) {
var _this = this;
if (_useConptyDll === void 0) { _useConptyDll = false; }
if (conptyInheritCursor === void 0) { conptyInheritCursor = false; }
this._useConpty = _useConpty;
this._useConptyDll = _useConptyDll;
this._pid = 0;
this._innerPid = 0;
if (this._useConpty === undefined || this._useConpty === true) {
this._useConpty = this._getWindowsBuildNumber() >= 18309;
}
if (this._useConpty) {
if (!conptyNative) {
conptyNative = utils_1.loadNativeModule('conpty').module;
}
}
else {
if (!winptyNative) {
winptyNative = utils_1.loadNativeModule('pty').module;
}
}
this._ptyNative = this._useConpty ? conptyNative : winptyNative;
// Sanitize input variable.
cwd = path.resolve(cwd);
// Compose command line
var commandLine = argsToCommandLine(file, args);
// Open pty session.
var term;
if (this._useConpty) {
term = this._ptyNative.startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor, this._useConptyDll);
}
else {
term = this._ptyNative.startProcess(file, commandLine, env, cwd, cols, rows, debug);
this._pid = term.pid;
this._innerPid = term.innerPid;
}
// Not available on windows.
this._fd = term.fd;
// Generated incremental number that has no real purpose besides using it
// as a terminal id.
this._pty = term.pty;
// Create terminal pipe IPC channel and forward to a local unix socket.
this._outSocket = new net_1.Socket();
this._outSocket.setEncoding('utf8');
// The conout socket must be ready out on another thread to avoid deadlocks
this._conoutSocketWorker = new windowsConoutConnection_1.ConoutConnection(term.conout, this._useConptyDll);
this._conoutSocketWorker.onReady(function () {
_this._conoutSocketWorker.connectSocket(_this._outSocket);
});
this._outSocket.on('connect', function () {
_this._outSocket.emit('ready_datapipe');
});
var inSocketFD = fs.openSync(term.conin, 'w');
this._inSocket = new net_1.Socket({
fd: inSocketFD,
readable: false,
writable: true
});
this._inSocket.setEncoding('utf8');
if (this._useConpty) {
var connect = this._ptyNative.connect(this._pty, commandLine, cwd, env, this._useConptyDll, function (c) { return _this._$onProcessExit(c); });
this._innerPid = connect.pid;
}
}
Object.defineProperty(WindowsPtyAgent.prototype, "inSocket", {
get: function () { return this._inSocket; },
enumerable: false,
configurable: true
});
Object.defineProperty(WindowsPtyAgent.prototype, "outSocket", {
get: function () { return this._outSocket; },
enumerable: false,
configurable: true
});
Object.defineProperty(WindowsPtyAgent.prototype, "fd", {
get: function () { return this._fd; },
enumerable: false,
configurable: true
});
Object.defineProperty(WindowsPtyAgent.prototype, "innerPid", {
get: function () { return this._innerPid; },
enumerable: false,
configurable: true
});
Object.defineProperty(WindowsPtyAgent.prototype, "pty", {
get: function () { return this._pty; },
enumerable: false,
configurable: true
});
WindowsPtyAgent.prototype.resize = function (cols, rows) {
if (this._useConpty) {
if (this._exitCode !== undefined) {
throw new Error('Cannot resize a pty that has already exited');
}
this._ptyNative.resize(this._pty, cols, rows, this._useConptyDll);
return;
}
this._ptyNative.resize(this._pid, cols, rows);
};
WindowsPtyAgent.prototype.clear = function () {
if (this._useConpty) {
this._ptyNative.clear(this._pty, this._useConptyDll);
}
};
WindowsPtyAgent.prototype.kill = function () {
var _this = this;
// Tell the agent to kill the pty, this releases handles to the process
if (this._useConpty) {
if (!this._useConptyDll) {
this._inSocket.readable = false;
this._outSocket.readable = false;
this._getConsoleProcessList().then(function (consoleProcessList) {
consoleProcessList.forEach(function (pid) {
try {
process.kill(pid);
}
catch (e) {
// Ignore if process cannot be found (kill ESRCH error)
}
});
});
this._ptyNative.kill(this._pty, this._useConptyDll);
this._conoutSocketWorker.dispose();
}
else {
// Close the input write handle to signal the end of session.
this._inSocket.destroy();
this._ptyNative.kill(this._pty, this._useConptyDll);
this._outSocket.on('data', function () {
_this._conoutSocketWorker.dispose();
});
}
}
else {
// Because pty.kill closes the handle, it will kill most processes by itself.
// Process IDs can be reused as soon as all handles to them are
// dropped, so we want to immediately kill the entire console process list.
// If we do not force kill all processes here, node servers in particular
// seem to become detached and remain running (see
// Microsoft/vscode#26807).
var processList = this._ptyNative.getProcessList(this._pid);
this._ptyNative.kill(this._pid, this._innerPid);
processList.forEach(function (pid) {
try {
process.kill(pid);
}
catch (e) {
// Ignore if process cannot be found (kill ESRCH error)
}
});
}
};
WindowsPtyAgent.prototype._getConsoleProcessList = function () {
var _this = this;
return new Promise(function (resolve) {
var agent = child_process_1.fork(path.join(__dirname, 'conpty_console_list_agent'), [_this._innerPid.toString()]);
agent.on('message', function (message) {
clearTimeout(timeout);
resolve(message.consoleProcessList);
});
var timeout = setTimeout(function () {
// Something went wrong, just send back the shell PID
agent.kill();
resolve([_this._innerPid]);
}, 5000);
});
};
Object.defineProperty(WindowsPtyAgent.prototype, "exitCode", {
get: function () {
if (this._useConpty) {
return this._exitCode;
}
var winptyExitCode = this._ptyNative.getExitCode(this._innerPid);
return winptyExitCode === -1 ? undefined : winptyExitCode;
},
enumerable: false,
configurable: true
});
WindowsPtyAgent.prototype._getWindowsBuildNumber = function () {
var osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release());
var buildNumber = 0;
if (osVersion && osVersion.length === 4) {
buildNumber = parseInt(osVersion[3]);
}
return buildNumber;
};
WindowsPtyAgent.prototype._generatePipeName = function () {
return "conpty-" + Math.random() * 10000000;
};
/**
* Triggered from the native side when a contpy process exits.
*/
WindowsPtyAgent.prototype._$onProcessExit = function (exitCode) {
var _this = this;
this._exitCode = exitCode;
if (!this._useConptyDll) {
this._flushDataAndCleanUp();
this._outSocket.on('data', function () { return _this._flushDataAndCleanUp(); });
}
};
WindowsPtyAgent.prototype._flushDataAndCleanUp = function () {
var _this = this;
if (this._useConptyDll) {
return;
}
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
}
this._closeTimeout = setTimeout(function () { return _this._cleanUpProcess(); }, FLUSH_DATA_INTERVAL);
};
WindowsPtyAgent.prototype._cleanUpProcess = function () {
if (this._useConptyDll) {
return;
}
this._inSocket.readable = false;
this._outSocket.readable = false;
this._outSocket.destroy();
};
return WindowsPtyAgent;
}());
exports.WindowsPtyAgent = WindowsPtyAgent;
// Convert argc/argv into a Win32 command-line following the escaping convention
// documented on MSDN (e.g. see CommandLineToArgvW documentation). Copied from
// winpty project.
function argsToCommandLine(file, args) {
if (isCommandLine(args)) {
if (args.length === 0) {
return file;
}
return argsToCommandLine(file, []) + " " + args;
}
var argv = [file];
Array.prototype.push.apply(argv, args);
var result = '';
for (var argIndex = 0; argIndex < argv.length; argIndex++) {
if (argIndex > 0) {
result += ' ';
}
var arg = argv[argIndex];
// if it is empty or it contains whitespace and is not already quoted
var hasLopsidedEnclosingQuote = xOr((arg[0] !== '"'), (arg[arg.length - 1] !== '"'));
var hasNoEnclosingQuotes = ((arg[0] !== '"') && (arg[arg.length - 1] !== '"'));
var quote = arg === '' ||
(arg.indexOf(' ') !== -1 ||
arg.indexOf('\t') !== -1) &&
((arg.length > 1) &&
(hasLopsidedEnclosingQuote || hasNoEnclosingQuotes));
if (quote) {
result += '\"';
}
var bsCount = 0;
for (var i = 0; i < arg.length; i++) {
var p = arg[i];
if (p === '\\') {
bsCount++;
}
else if (p === '"') {
result += repeatText('\\', bsCount * 2 + 1);
result += '"';
bsCount = 0;
}
else {
result += repeatText('\\', bsCount);
bsCount = 0;
result += p;
}
}
if (quote) {
result += repeatText('\\', bsCount * 2);
result += '\"';
}
else {
result += repeatText('\\', bsCount);
}
}
return result;
}
exports.argsToCommandLine = argsToCommandLine;
function isCommandLine(args) {
return typeof args === 'string';
}
function repeatText(text, count) {
var result = '';
for (var i = 0; i < count; i++) {
result += text;
}
return result;
}
function xOr(arg1, arg2) {
return ((arg1 && !arg2) || (!arg1 && arg2));
}
//# sourceMappingURL=windowsPtyAgent.js.map
@@ -1,199 +0,0 @@
"use strict";
/**
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WindowsTerminal = void 0;
var terminal_1 = require("./terminal");
var windowsPtyAgent_1 = require("./windowsPtyAgent");
var utils_1 = require("./utils");
var DEFAULT_FILE = 'cmd.exe';
var DEFAULT_NAME = 'Windows Shell';
var WindowsTerminal = /** @class */ (function (_super) {
__extends(WindowsTerminal, _super);
function WindowsTerminal(file, args, opt) {
var _this = _super.call(this, opt) || this;
_this._checkType('args', args, 'string', true);
// Initialize arguments
args = args || [];
file = file || DEFAULT_FILE;
opt = opt || {};
opt.env = opt.env || process.env;
if (opt.encoding) {
console.warn('Setting encoding on Windows is not supported');
}
var env = utils_1.assign({}, opt.env);
_this._cols = opt.cols || terminal_1.DEFAULT_COLS;
_this._rows = opt.rows || terminal_1.DEFAULT_ROWS;
var cwd = opt.cwd || process.cwd();
var name = opt.name || env.TERM || DEFAULT_NAME;
var parsedEnv = _this._parseEnv(env);
// If the terminal is ready
_this._isReady = false;
// Functions that need to run after `ready` event is emitted.
_this._deferreds = [];
// Create new termal.
_this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
_this._socket = _this._agent.outSocket;
// Not available until `ready` event emitted.
_this._pid = _this._agent.innerPid;
_this._fd = _this._agent.fd;
_this._pty = _this._agent.pty;
// The forked windows terminal is not available until `ready` event is
// emitted.
_this._socket.on('ready_datapipe', function () {
// Run deferreds and set ready state once the first data event is received.
_this._socket.once('data', function () {
// Wait until the first data event is fired then we can run deferreds.
if (!_this._isReady) {
// Terminal is now ready and we can avoid having to defer method
// calls.
_this._isReady = true;
// Execute all deferred methods
_this._deferreds.forEach(function (fn) {
// NB! In order to ensure that `this` has all its references
// updated any variable that need to be available in `this` before
// the deferred is run has to be declared above this forEach
// statement.
fn.run();
});
// Reset
_this._deferreds = [];
}
});
// Shutdown if `error` event is emitted.
_this._socket.on('error', function (err) {
// Close terminal session.
_this._close();
// EIO, happens when someone closes our child process: the only process
// in the terminal.
// node < 0.6.14: errno 5
// node >= 0.6.14: read EIO
if (err.code) {
if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO'))
return;
}
// Throw anything else.
if (_this.listeners('error').length < 2) {
throw err;
}
});
// Cleanup after the socket is closed.
_this._socket.on('close', function () {
_this.emit('exit', _this._agent.exitCode);
_this._close();
});
});
_this._file = file;
_this._name = name;
_this._readable = true;
_this._writable = true;
_this._forwardEvents();
return _this;
}
WindowsTerminal.prototype._write = function (data) {
this._defer(this._doWrite, data);
};
WindowsTerminal.prototype._doWrite = function (data) {
this._agent.inSocket.write(data);
};
/**
* openpty
*/
WindowsTerminal.open = function (options) {
throw new Error('open() not supported on windows, use Fork() instead.');
};
/**
* TTY
*/
WindowsTerminal.prototype.resize = function (cols, rows) {
var _this = this;
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
throw new Error('resizing must be done using positive cols and rows');
}
this._deferNoArgs(function () {
_this._agent.resize(cols, rows);
_this._cols = cols;
_this._rows = rows;
});
};
WindowsTerminal.prototype.clear = function () {
var _this = this;
this._deferNoArgs(function () {
_this._agent.clear();
});
};
WindowsTerminal.prototype.destroy = function () {
var _this = this;
this._deferNoArgs(function () {
_this.kill();
});
};
WindowsTerminal.prototype.kill = function (signal) {
var _this = this;
this._deferNoArgs(function () {
if (signal) {
throw new Error('Signals not supported on windows.');
}
_this._close();
_this._agent.kill();
});
};
WindowsTerminal.prototype._deferNoArgs = function (deferredFn) {
var _this = this;
// If the terminal is ready, execute.
if (this._isReady) {
deferredFn.call(this);
return;
}
// Queue until terminal is ready.
this._deferreds.push({
run: function () { return deferredFn.call(_this); }
});
};
WindowsTerminal.prototype._defer = function (deferredFn, arg) {
var _this = this;
// If the terminal is ready, execute.
if (this._isReady) {
deferredFn.call(this, arg);
return;
}
// Queue until terminal is ready.
this._deferreds.push({
run: function () { return deferredFn.call(_this, arg); }
});
};
Object.defineProperty(WindowsTerminal.prototype, "process", {
get: function () { return this._name; },
enumerable: false,
configurable: true
});
Object.defineProperty(WindowsTerminal.prototype, "master", {
get: function () { throw new Error('master is not supported on Windows'); },
enumerable: false,
configurable: true
});
Object.defineProperty(WindowsTerminal.prototype, "slave", {
get: function () { throw new Error('slave is not supported on Windows'); },
enumerable: false,
configurable: true
});
return WindowsTerminal;
}(terminal_1.Terminal));
exports.WindowsTerminal = WindowsTerminal;
//# sourceMappingURL=windowsTerminal.js.map
@@ -1,160 +1,19 @@
/**
* The remote shell for the shared native-deps cache (`ssh-relay-native-deps-cache.ts`).
*
* Every script here is POSIX `sh` and answers with one token, because the only alternative to a
* token is inferring success from an exit status the transport can also produce. A command that
* cannot answer is a cache miss, never a licence to delete: `MISS` and `NOT_PROMOTED` both leave
* the relay directory owning its own `node_modules`, which is exactly today's behaviour.
*/
/** Historical cache scans require complete reference evidence before removal. */
import { shellEscape } from './ssh-connection-utils'
import {
RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME,
RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX,
LEGACY_RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX,
relayNativeDepsCacheBaseDir,
relayNativeDepsCacheEntryDir,
relayNativeDepsCacheNodeModulesPath,
remoteInstallRootDir
} from './ssh-relay-native-deps-cache'
import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import type { RemoteHostPlatform } from './ssh-remote-platform'
export const RELAY_NATIVE_CACHE_LINKED = '__ORCA_NATIVE_CACHE__LINKED'
export const RELAY_NATIVE_CACHE_SEEDED = '__ORCA_NATIVE_CACHE__SEEDED'
export const RELAY_NATIVE_CACHE_MISS = '__ORCA_NATIVE_CACHE__MISS'
export const RELAY_NATIVE_CACHE_PROMOTED = '__ORCA_NATIVE_CACHE__PROMOTED'
export const RELAY_NATIVE_CACHE_NOT_PROMOTED = '__ORCA_NATIVE_CACHE__NOT_PROMOTED'
export const RELAY_NATIVE_CACHE_LIST_OK = '__ORCA_NATIVE_CACHE__LIST_OK'
export const RELAY_NATIVE_CACHE_REFS_OK = '__ORCA_NATIVE_CACHE__REFS_OK'
export const RELAY_NATIVE_CACHE_REFS_ERR = '__ORCA_NATIVE_CACHE__REFS_ERR'
/**
* How old an entry without `.deps-complete` must be before another deploy may reclaim it. Well
* past the 15-minute deploy ceiling, so a live installer is never mistaken for a crashed one.
* Reclaiming is safe at any age in principle — nothing links an entry until it is complete — but
* the margin is what keeps that argument from resting on a single `[ -f ]`.
*/
const CACHE_TAKEOVER_MINUTES = 120
/** Bounds every listing, matching `MAX_RELAY_GC_LISTING_ENTRIES`' role for version dirs. */
export const MAX_RELAY_NATIVE_CACHE_LISTING_ENTRIES = 64
export type RelayNativeDepsCachePaths = {
host: RemoteHostPlatform
remoteHome: string
relayDir: string
key: string
}
function cachePaths(paths: RelayNativeDepsCachePaths): {
base: string
entry: string
target: string
nodeModules: string
root: string
} {
const { host, remoteHome, relayDir, key } = paths
return {
base: relayNativeDepsCacheBaseDir(host, remoteHome),
entry: relayNativeDepsCacheEntryDir(host, remoteHome, key),
target: relayNativeDepsCacheNodeModulesPath(host, remoteHome, key),
nodeModules: joinRemotePath(host, relayDir, 'node_modules'),
root: remoteInstallRootDir(host, remoteHome)
}
}
/**
* Link a complete entry into the relay directory, or seed a private tree from a sibling relay
* directory that already has a matching one.
*
* The seed exists so the first deploy after this ships does not recompile once more on a host
* that already paid for the compile. It is not trusted: the copy is a plain private install until
* the normal probe loads both addons, and only then is it promoted.
*/
export function ensureRelayNativeDepsCacheCommand(
paths: RelayNativeDepsCachePaths,
deps: Readonly<Record<string, string>>
): string {
const { entry, target, nodeModules, root } = cachePaths(paths)
// Why grep the sibling's manifest: an older Orca pinned different versions, and a
// toolchain-skip host wrote one with node-pty removed. Both must fail to qualify.
const depGuards = Object.entries(deps).map(
([name, version]) => `grep -F -q ${shellEscape(`"${name}":"${version}"`)} "$pj" || continue`
)
return [
`cache=${shellEscape(entry)}`,
`target=${shellEscape(target)}`,
`nm=${shellEscape(nodeModules)}`,
`root=${shellEscape(root)}`,
`if [ -f "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" ] && [ -d "$target" ]; then`,
' if [ -L "$nm" ]; then',
` if [ "$(readlink "$nm" 2>/dev/null)" = "$target" ]; then printf '%s\\n' ${RELAY_NATIVE_CACHE_LINKED}; exit 0; fi`,
' rm -f "$nm" 2>/dev/null || true',
' fi',
` if [ ! -e "$nm" ] && ln -s "$target" "$nm" 2>/dev/null; then printf '%s\\n' ${RELAY_NATIVE_CACHE_LINKED}; exit 0; fi`,
` printf '%s\\n' ${RELAY_NATIVE_CACHE_MISS}; exit 0`,
'fi',
'if [ ! -e "$nm" ] && [ ! -L "$nm" ]; then',
' for cand in "$root"/relay-*/node_modules; do',
' [ -d "$cand" ] || continue',
' [ -L "$cand" ] && continue',
' [ -d "$cand/node-pty" ] || continue',
' [ -d "$cand/@parcel/watcher" ] || continue',
' pj="${cand%/node_modules}/package.json"',
' [ -f "$pj" ] || continue',
...depGuards.map((guard) => ` ${guard}`),
' seed="$nm.seed.$$"',
' rm -rf "$seed" 2>/dev/null || true',
' if cp -Rp "$cand" "$seed" 2>/dev/null && mv "$seed" "$nm" 2>/dev/null; then',
` printf '%s\\n' ${RELAY_NATIVE_CACHE_SEEDED}; exit 0`,
' fi',
' rm -rf "$seed" 2>/dev/null || true',
' break',
' done',
'fi',
`printf '%s\\n' ${RELAY_NATIVE_CACHE_MISS}`
].join('\n')
}
/**
* Publish a probe-verified private tree as the shared entry, then link the relay directory to it.
*
* `mkdir "$cache"` is the election: exactly one deploy creates the directory, and a loser keeps
* its own tree rather than writing into someone else's. `.deps-complete` is written last, after
* the symlink exists, so an entry is never linkable before it is referenced.
*/
export function promoteRelayNativeDepsCacheCommand(paths: RelayNativeDepsCachePaths): string {
const { base, entry, target, nodeModules } = cachePaths(paths)
const notPromoted = `printf '%s\\n' ${RELAY_NATIVE_CACHE_NOT_PROMOTED}`
return [
`base=${shellEscape(base)}`,
`cache=${shellEscape(entry)}`,
`target=${shellEscape(target)}`,
`nm=${shellEscape(nodeModules)}`,
`[ -d "$nm" ] || { ${notPromoted}; exit 0; }`,
`if [ -L "$nm" ]; then ${notPromoted}; exit 0; fi`,
`mkdir -p "$base" 2>/dev/null || { ${notPromoted}; exit 0; }`,
`if [ -d "$cache" ] && [ ! -f "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" ]; then`,
` if [ -n "$(find "$cache" -maxdepth 0 -mmin +${CACHE_TAKEOVER_MINUTES} 2>/dev/null)" ]; then`,
' rm -rf "$cache" 2>/dev/null || true',
' fi',
'fi',
`mkdir "$cache" 2>/dev/null || { ${notPromoted}; exit 0; }`,
'if mv "$nm" "$target" 2>/dev/null; then',
' if ln -s "$target" "$nm" 2>/dev/null; then',
` : > "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" 2>/dev/null || true`,
` if [ -f "$cache/${RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME}" ]; then printf '%s\\n' ${RELAY_NATIVE_CACHE_PROMOTED}; exit 0; fi`,
' fi',
' rm -f "$nm" 2>/dev/null || true',
// Why the rm is conditional on the move back: a failed restore leaves the only copy of the
// tree inside an incomplete entry. Deleting it there would cost the relay its native deps.
' if mv "$target" "$nm" 2>/dev/null; then rm -rf "$cache" 2>/dev/null || true; fi',
` ${notPromoted}; exit 0`,
'fi',
'rm -rf "$cache" 2>/dev/null || true',
notPromoted
].join('\n')
}
/** Complete entries and tombstones; deletion requires a separate reference scan. */
export function listRelayNativeDepsCacheEntriesCommand(
host: RemoteHostPlatform,
@@ -1,209 +0,0 @@
/**
* The deploy-side half of the shared native-deps cache: resolve this build's key, try to link an
* existing entry, and publish a probe-verified tree afterwards.
*
* Both entry points answer with a value, never an exception. The cache is an optimization on a
* path that must still connect a host with a read-only home, no `ln`, or an SSH server that drops
* the channel — every one of those is a plain per-directory install, which is what the relay did
* before this existed.
*/
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { SshConnection } from './ssh-connection'
import { RELAY_ARTIFACTS } from '../../shared/relay-artifacts'
import { execCommand } from './ssh-relay-deploy-helpers'
import { NATIVE_DEPS_COMMAND_TIMEOUT_MS } from './ssh-relay-deploy-timing'
import {
computeRelayNativeDepsCacheKey,
supportsRelayNativeDepsCache,
RELAY_NATIVE_DEPS_PATCH_ARTIFACT_PATTERN,
type RelayNativeDepsCachePatchSource
} from './ssh-relay-native-deps-cache'
import {
ensureRelayNativeDepsCacheCommand,
promoteRelayNativeDepsCacheCommand,
RELAY_NATIVE_CACHE_LINKED,
RELAY_NATIVE_CACHE_PROMOTED,
RELAY_NATIVE_CACHE_SEEDED,
type RelayNativeDepsCachePaths
} from './ssh-relay-native-deps-cache-commands'
import type { RemoteHostPlatform } from './ssh-remote-platform'
/**
* `linked` — the relay directory now points at a complete shared entry and needs no install.
* `private` — it owns (or is about to own) its own tree, which promotion may later publish.
*/
export type RelayNativeDepsCacheAttachment = {
mode: 'linked' | 'private'
key: string
}
export type RelayNativeDepsCacheContext = {
hostPlatform: RemoteHostPlatform
remoteHome: string
relayDir: string
platform: string
localRelayDir: string
deps: Readonly<Record<string, string>>
signal?: AbortSignal
}
function execHostCommand(
conn: SshConnection,
host: RemoteHostPlatform,
command: string,
signal?: AbortSignal
): Promise<string> {
return execCommand(conn, command, {
wrapCommand: host.commandDialect !== 'powershell',
// Why the native-deps budget and not the default 30s: a seeding copy moves a whole
// node_modules on the host's own disk, which is fast but not instant on a cold cache.
timeoutMs: NATIVE_DEPS_COMMAND_TIMEOUT_MS,
signal
})
}
/**
* Every shipped artifact that patches the installed native tree, read for hashing.
*
* Reading is best-effort by design: a patch this client cannot read must not silently drop out of
* the key, so an unreadable one disables the cache rather than producing a key that claims the
* patch was applied.
*/
export function readRelayNativeDepsPatchSources(
localRelayDir: string
): RelayNativeDepsCachePatchSource[] | null {
const sources: RelayNativeDepsCachePatchSource[] = []
for (const artifact of RELAY_ARTIFACTS) {
if (!RELAY_NATIVE_DEPS_PATCH_ARTIFACT_PATTERN.test(artifact.filename)) {
continue
}
const path = join(localRelayDir, artifact.filename)
try {
if (!existsSync(path)) {
continue
}
sources.push({ filename: artifact.filename, contents: readFileSync(path, 'utf-8') })
} catch {
return null
}
}
return sources
}
/** This build's cache key, or null when it cannot be computed and the cache must stay off. */
export function resolveRelayNativeDepsCacheKey(context: {
platform: string
localRelayDir: string
deps: Readonly<Record<string, string>>
}): string | null {
const patchSources = readRelayNativeDepsPatchSources(context.localRelayDir)
if (!patchSources) {
return null
}
try {
return computeRelayNativeDepsCacheKey({
platform: context.platform,
deps: context.deps,
patchSources
})
} catch (err) {
console.warn(
`[ssh-relay] Native-deps cache key unavailable for ${context.platform}: ${
err instanceof Error ? err.message : String(err)
}`
)
return null
}
}
/**
* Link a complete entry, or leave the relay directory to install privately.
*
* Returns null when the cache is off for this host, which keeps the caller on the exact command
* sequence it ran before the cache existed.
*/
export async function attachRelayNativeDepsCache(
conn: SshConnection,
context: RelayNativeDepsCacheContext
): Promise<RelayNativeDepsCacheAttachment | null> {
if (!supportsRelayNativeDepsCache(context.hostPlatform)) {
return null
}
const key = resolveRelayNativeDepsCacheKey(context)
if (!key) {
return null
}
const paths = cachePathsFor(context, key)
try {
const output = await execHostCommand(
conn,
context.hostPlatform,
ensureRelayNativeDepsCacheCommand(paths, context.deps),
context.signal
)
if (output.includes(RELAY_NATIVE_CACHE_LINKED)) {
console.log(`[ssh-relay] Native deps linked from shared cache entry ${key}`)
return { mode: 'linked', key }
}
if (output.includes(RELAY_NATIVE_CACHE_SEEDED)) {
console.log(`[ssh-relay] Seeded native deps for ${key} from an existing install on this host`)
}
return { mode: 'private', key }
} catch (err) {
context.signal?.throwIfAborted()
// Why still 'private' and not null: the relay directory owns nothing yet either way, and the
// install that follows is identical. Promotion afterwards is separately best-effort.
console.warn(
`[ssh-relay] Native-deps cache probe for ${key} failed; installing per-directory: ${
err instanceof Error ? err.message : String(err)
}`
)
return { mode: 'private', key }
}
}
/**
* Publish a private tree the probe just loaded, and link the relay directory to it.
*
* Called only after `probeInstalledNativeDeps` reported both addons loadable on this host, so an
* entry is never published on the strength of a successful `npm install` alone.
*/
export async function promoteRelayNativeDepsCache(
conn: SshConnection,
context: RelayNativeDepsCacheContext,
key: string
): Promise<void> {
try {
const output = await execHostCommand(
conn,
context.hostPlatform,
promoteRelayNativeDepsCacheCommand(cachePathsFor(context, key)),
context.signal
)
console.log(
output.includes(RELAY_NATIVE_CACHE_PROMOTED)
? `[ssh-relay] Published native deps as shared cache entry ${key}`
: `[ssh-relay] Native deps stay per-directory; shared cache entry ${key} was not published`
)
} catch (err) {
context.signal?.throwIfAborted()
console.warn(
`[ssh-relay] Could not publish native-deps cache entry ${key}: ${
err instanceof Error ? err.message : String(err)
}`
)
}
}
function cachePathsFor(
context: RelayNativeDepsCacheContext,
key: string
): RelayNativeDepsCachePaths {
return {
host: context.hostPlatform,
remoteHome: context.remoteHome,
relayDir: context.relayDir,
key
}
}
@@ -1,90 +1,42 @@
// The cache's safety argument is made of `sh`, not TypeScript: `mkdir` elects the publisher,
// `.deps-complete` gates linking, and a failed publish must put the tree back. Asserting on the
// command strings cannot show any of that, so these run the real scripts against a real tree.
import { execFileSync } from 'node:child_process'
import {
mkdirSync,
mkdtempSync,
readlinkSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
existsSync,
lstatSync
} from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import {
computeRelayNativeDepsCacheKey,
relayNativeDepsCacheEntryDir,
relayNativeDepsCacheNodeModulesPath
} from './ssh-relay-native-deps-cache'
import {
ensureRelayNativeDepsCacheCommand,
listRelayNativeDepsCacheEntriesCommand,
listRelayNativeDepsCacheReferencesCommand,
promoteRelayNativeDepsCacheCommand,
RELAY_NATIVE_CACHE_LINKED,
RELAY_NATIVE_CACHE_LIST_OK,
RELAY_NATIVE_CACHE_MISS,
RELAY_NATIVE_CACHE_NOT_PROMOTED,
RELAY_NATIVE_CACHE_PROMOTED,
RELAY_NATIVE_CACHE_REFS_OK,
RELAY_NATIVE_CACHE_SEEDED
RELAY_NATIVE_CACHE_REFS_OK
} from './ssh-relay-native-deps-cache-commands'
const HOST = getRemoteHostPlatform('linux-x64')
const DEPS = { 'node-pty': '1.1.0', '@parcel/watcher': '2.5.6' } as const
const KEY = computeRelayNativeDepsCacheKey({ platform: 'linux-x64', deps: DEPS })
const KEY = 'linux-x64-99355f00e9e557a3'
// Debian and Ubuntu point /bin/sh at dash, which is stricter than the bash-in-sh-mode that macOS
// ships; run against both when both exist so a bashism cannot pass here and fail on a host.
const SHELLS = ['/bin/sh', '/bin/dash'].filter((shell) => existsSync(shell))
describe.runIf(process.platform !== 'win32').each(SHELLS)(
'relay native-deps cache shell scripts (%s)',
'historical relay native cache scans (%s)',
(shell) => {
let home: string
const relayDir = (version: string): string => join(home, '.orca-remote', `relay-${version}`)
function sh(command: string): string {
return execFileSync(shell, ['-c', command], { encoding: 'utf-8' })
}
/** A relay directory holding its own installed tree, exactly as `npm install` leaves it. */
function makePrivateInstall(version: string, deps: Record<string, string> = DEPS): string {
const dir = relayDir(version)
mkdirSync(join(dir, 'node_modules', 'node-pty', 'build'), { recursive: true })
mkdirSync(join(dir, 'node_modules', '@parcel', 'watcher'), { recursive: true })
writeFileSync(join(dir, 'node_modules', 'node-pty', 'build', 'pty.node'), 'binary')
writeFileSync(join(dir, 'package.json'), JSON.stringify({ dependencies: deps }))
return dir
}
function ensure(version: string): string {
return sh(
ensureRelayNativeDepsCacheCommand(
{ host: HOST, remoteHome: home, relayDir: relayDir(version), key: KEY },
DEPS
)
).trim()
}
function promote(version: string): string {
return sh(
promoteRelayNativeDepsCacheCommand({
host: HOST,
remoteHome: home,
relayDir: relayDir(version),
key: KEY
})
).trim()
function makeLegacyCache(version: string): void {
const entry = relayNativeDepsCacheEntryDir(HOST, home, KEY)
const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY)
mkdirSync(target, { recursive: true })
writeFileSync(join(entry, '.deps-complete'), '')
mkdirSync(relayDir(version), { recursive: true })
symlinkSync(target, join(relayDir(version), 'node_modules'))
}
beforeEach(() => {
@@ -96,119 +48,8 @@ describe.runIf(process.platform !== 'win32').each(SHELLS)(
rmSync(home, { recursive: true, force: true })
})
it('publishes a probe-verified tree and links the relay directory to it', () => {
makePrivateInstall('0.1.0+aaa')
expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_PROMOTED)
const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY)
expect(readlinkSync(join(relayDir('0.1.0+aaa'), 'node_modules'))).toBe(target)
expect(statSync(join(target, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
expect(
existsSync(join(relayNativeDepsCacheEntryDir(HOST, home, KEY), '.deps-complete'))
).toBe(true)
})
it('links a second bundle to the published tree with no install of its own', () => {
makePrivateInstall('0.1.0+aaa')
promote('0.1.0+aaa')
// A different bundle: fresh directory, no node_modules, nothing installed.
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_LINKED)
const linked = join(relayDir('0.1.0+bbb'), 'node_modules')
expect(readlinkSync(linked)).toBe(relayNativeDepsCacheNodeModulesPath(HOST, home, KEY))
expect(statSync(join(linked, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
})
it('will not link an entry whose completion sentinel is absent', () => {
makePrivateInstall('0.1.0+aaa')
promote('0.1.0+aaa')
rmSync(join(relayNativeDepsCacheEntryDir(HOST, home, KEY), '.deps-complete'))
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
expect(existsSync(join(relayDir('0.1.0+bbb'), 'node_modules'))).toBe(false)
})
it('seeds from a sibling install rather than recompiling once more', () => {
makePrivateInstall('0.1.0+aaa')
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_SEEDED)
const seeded = join(relayDir('0.1.0+bbb'), 'node_modules')
expect(lstatSync(seeded).isSymbolicLink()).toBe(false)
expect(statSync(join(seeded, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
// The source is untouched, so a relay running out of it is unaffected.
expect(existsSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty'))).toBe(true)
})
it('refuses to seed from a sibling pinned to different versions', () => {
makePrivateInstall('0.1.0+aaa', { 'node-pty': '1.0.0', '@parcel/watcher': '2.5.6' })
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
expect(existsSync(join(relayDir('0.1.0+bbb'), 'node_modules'))).toBe(false)
})
it('refuses to seed from a sibling that had node-pty skipped', () => {
makePrivateInstall('0.1.0+aaa')
rmSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty'), { recursive: true })
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
})
it('leaves a directory that installed for itself alone', () => {
makePrivateInstall('0.1.0+aaa')
promote('0.1.0+aaa')
const own = makePrivateInstall('0.1.0+bbb')
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
expect(lstatSync(join(own, 'node_modules')).isSymbolicLink()).toBe(false)
})
it('elects exactly one publisher and leaves the loser its own tree', () => {
makePrivateInstall('0.1.0+aaa')
makePrivateInstall('0.1.0+bbb')
expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_PROMOTED)
expect(promote('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_NOT_PROMOTED)
// The loser must not have handed its tree to an entry it lost the race for.
const loser = join(relayDir('0.1.0+bbb'), 'node_modules')
expect(lstatSync(loser).isSymbolicLink()).toBe(false)
expect(statSync(join(loser, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
})
it('never republishes through a symlink it already holds', () => {
makePrivateInstall('0.1.0+aaa')
promote('0.1.0+aaa')
expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_NOT_PROMOTED)
expect(statSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty')).isDirectory()).toBe(
true
)
})
it('survives removing a linked relay directory without touching the shared tree', () => {
makePrivateInstall('0.1.0+aaa')
promote('0.1.0+aaa')
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
ensure('0.1.0+bbb')
// Exactly what version GC does to an idle directory.
sh(`rm -rf ${JSON.stringify(relayDir('0.1.0+bbb'))}`)
const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY)
expect(statSync(join(target, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
})
it('reports the published entry and every symlink that references it', () => {
makePrivateInstall('0.1.0+aaa')
promote('0.1.0+aaa')
makeLegacyCache('0.1.0+aaa')
const entries = sh(listRelayNativeDepsCacheEntriesCommand(HOST, home)).trim().split('\n')
expect(entries).toEqual([`ENTRY ${KEY}`, RELAY_NATIVE_CACHE_LIST_OK])
@@ -221,8 +62,7 @@ describe.runIf(process.platform !== 'win32').each(SHELLS)(
})
it('reports a symlink no Orca version wrote, so GC can refuse the pass', () => {
makePrivateInstall('0.1.0+aaa')
promote('0.1.0+aaa')
makeLegacyCache('0.1.0+aaa')
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
symlinkSync('../relay-0.1.0+aaa/node_modules', join(relayDir('0.1.0+bbb'), 'node_modules'))
@@ -1,7 +1,3 @@
// The cache is shared across every relay directory on a host, so these cover the two things that
// make sharing safe: the key changes when the tree's inputs change, and GC refuses to delete on
// anything short of a complete, attributable reference listing.
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('./ssh-relay-deploy-helpers', () => ({
@@ -12,15 +8,12 @@ import { execCommand } from './ssh-relay-deploy-helpers'
import type { SshConnection } from './ssh-connection'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import {
computeRelayNativeDepsCacheKey,
isRelayNativeDepsCacheEntryName,
relayNativeDepsCacheEntryDir,
relayNativeDepsCacheNodeModulesPath,
supportsRelayNativeDepsCache
} from './ssh-relay-native-deps-cache'
import {
ensureRelayNativeDepsCacheCommand,
promoteRelayNativeDepsCacheCommand,
RELAY_NATIVE_CACHE_LIST_OK,
RELAY_NATIVE_CACHE_REFS_ERR,
RELAY_NATIVE_CACHE_REFS_OK
@@ -30,9 +23,7 @@ import { gcRelayNativeDepsCache } from './ssh-relay-native-deps-cache-gc'
const POSIX = getRemoteHostPlatform('linux-x64')
const WINDOWS = getRemoteHostPlatform('win32-x64')
const HOME = '/home/u'
const DEPS = { 'node-pty': '1.1.0', '@parcel/watcher': '2.5.6' } as const
const KEY = computeRelayNativeDepsCacheKey({ platform: 'linux-x64', deps: DEPS })
const RELAY_DIR = `${HOME}/.orca-remote/relay-0.1.0+aaa`
const KEY = 'linux-x64-99355f00e9e557a3'
const conn = {} as SshConnection
const mockExec = vi.mocked(execCommand)
@@ -45,112 +36,21 @@ function listing(...keys: string[]): string {
return [...keys.map((k) => `ENTRY ${k}`), RELAY_NATIVE_CACHE_LIST_OK].join('\n')
}
describe('computeRelayNativeDepsCacheKey', () => {
it('keys on the dependency set, not on the relay bundle', () => {
expect(computeRelayNativeDepsCacheKey({ platform: 'linux-x64', deps: DEPS })).toBe(KEY)
expect(
computeRelayNativeDepsCacheKey({
platform: 'linux-x64',
deps: { '@parcel/watcher': '2.5.6', 'node-pty': '1.1.0' }
})
).toBe(KEY)
})
it('mints a new entry when a dependency version moves', () => {
expect(
computeRelayNativeDepsCacheKey({
platform: 'linux-x64',
deps: { ...DEPS, 'node-pty': '1.2.0' }
})
).not.toBe(KEY)
})
it('mints a new entry when a patch applied to the tree changes', () => {
const withPatch = computeRelayNativeDepsCacheKey({
platform: 'linux-x64',
deps: DEPS,
patchSources: [{ filename: 'node-pty-1.1.0-patch.cjs', contents: 'a' }]
})
const withChangedPatch = computeRelayNativeDepsCacheKey({
platform: 'linux-x64',
deps: DEPS,
patchSources: [{ filename: 'node-pty-1.1.0-patch.cjs', contents: 'b' }]
})
expect(withPatch).not.toBe(KEY)
expect(withChangedPatch).not.toBe(withPatch)
})
it('separates platforms so one host never links another architecture', () => {
expect(computeRelayNativeDepsCacheKey({ platform: 'linux-arm64', deps: DEPS })).not.toBe(KEY)
expect(KEY.startsWith('linux-x64-')).toBe(true)
})
it('refuses a platform it cannot recognise rather than building a path from it', () => {
expect(() => computeRelayNativeDepsCacheKey({ platform: '../../etc', deps: DEPS })).toThrow(
describe('historical native cache identities', () => {
it('recognizes old keys and rejects unsafe directory names', () => {
expect(isRelayNativeDepsCacheEntryName(KEY)).toBe(true)
expect(isRelayNativeDepsCacheEntryName('../../etc')).toBe(false)
expect(() => relayNativeDepsCacheEntryDir(POSIX, HOME, '../../etc')).toThrow(
/Unsafe relay native-deps cache key/
)
expect(isRelayNativeDepsCacheEntryName('../../etc')).toBe(false)
expect(isRelayNativeDepsCacheEntryName(KEY)).toBe(true)
})
it('leaves Windows on the per-directory install', () => {
it('preserves the POSIX-only cache boundary', () => {
expect(supportsRelayNativeDepsCache(POSIX)).toBe(true)
expect(supportsRelayNativeDepsCache(WINDOWS)).toBe(false)
})
})
describe('ensureRelayNativeDepsCacheCommand', () => {
const command = ensureRelayNativeDepsCacheCommand(
{ host: POSIX, remoteHome: HOME, relayDir: RELAY_DIR, key: KEY },
DEPS
)
it('links only an entry that carries the completion sentinel', () => {
expect(command).toContain(`[ -f "$cache/.deps-complete" ]`)
expect(command).toContain('ln -s "$target" "$nm"')
expect(command).toContain(relayNativeDepsCacheNodeModulesPath(POSIX, HOME, KEY))
})
it('never overwrites a directory the relay installed for itself', () => {
// The link is only created on a path that does not exist; a real node_modules reads as a miss.
expect(command).toContain('if [ ! -e "$nm" ] && ln -s "$target" "$nm"')
})
it('seeds only from a sibling whose manifest pins the same versions', () => {
for (const [name, version] of Object.entries(DEPS)) {
expect(command).toContain(`grep -F -q '"${name}":"${version}"' "$pj"`)
}
expect(command).toContain('[ -d "$cand/node-pty" ] || continue')
})
})
describe('promoteRelayNativeDepsCacheCommand', () => {
const command = promoteRelayNativeDepsCacheCommand({
host: POSIX,
remoteHome: HOME,
relayDir: RELAY_DIR,
key: KEY
})
it('elects one publisher with mkdir rather than a lock', () => {
expect(command).toContain('mkdir "$cache" 2>/dev/null')
})
it('writes the completion sentinel after the symlink exists', () => {
expect(command.indexOf('ln -s "$target" "$nm"')).toBeLessThan(
command.indexOf(': > "$cache/.deps-complete"')
)
})
it('never deletes the entry unless the tree made it back to the relay directory', () => {
expect(command).toContain('if mv "$target" "$nm" 2>/dev/null; then rm -rf "$cache"')
})
it('refuses to publish a directory that is already a shared symlink', () => {
expect(command).toContain('if [ -L "$nm" ]; then')
})
})
describe('gcRelayNativeDepsCache', () => {
beforeEach(() => {
mockExec.mockReset().mockResolvedValue('')
+2 -86
View File
@@ -1,36 +1,4 @@
/**
* Where the relay's compiled native dependencies live, and what their identity is keyed on.
*
* A relay install directory is keyed on the JS bundle hash, which moves on every commit to
* `src/relay/` or the `src/shared/` it pulls in. `node_modules` used to live inside it, so a
* dependency set that is a pinned constant (`RELAY_NATIVE_DEPS`) was reinstalled — and on Linux,
* where node-pty ships no prebuild, recompiled from source — on every new bundle (#18009). The
* directory key was coupled to the wrong quantity.
*
* The tree now lives at `~/.orca-remote/native/<relayPlatform>-<depsHash>/node_modules` and each
* relay directory holds a symlink to it. Three rules make one tree safe to share:
*
* 1. **A published entry is immutable.** `.deps-complete` is written last, only after a probe on
* this host loaded both addons. Nothing installs, rebuilds or resets into a published entry: a
* repair detaches the symlink and installs privately, so a host with a broken toolchain can
* never `rm -rf node_modules/node-pty` out from under a live relay that shares the tree.
* 2. **Publication elects one winner with `mkdir`.** The entry either does not exist (this deploy
* builds it privately and promotes it) or is already complete (this deploy links it). There is
* no window in which two deploys write one tree, so no client-side lock is needed.
* 3. **Every failure degrades to today's per-directory install.** A host that cannot symlink,
* cannot create the directory, or answers nothing still deploys, one bundle at a time.
*
* Windows is deliberately excluded. node-pty's npm tarball ships win32 prebuilts, so there is no
* compile to avoid there, and `node-pty-1.1.0-console-list-agent-patch.cjs` mutates the installed
* tree in place — which rule 1 forbids for a shared one.
*
* Linux's `node-pty-1.1.0-master-cloexec-patch.cjs` also mutates in place, but it stays inside rule
* 1: the deploy path runs it before promotion, and returns early on a linked entry, so it only ever
* touches a private tree. Its bytes are in the key, so a patched build never links a pre-patch
* entry -- and a tree whose patch was refused or rolled back is not promoted at all, because under
* that same key it would publish the leak to every later host on the machine.
*/
import { createHash } from 'node:crypto'
/** Historical cache identities remain recognizable while older relay owners may use them. */
import { RELAY_REMOTE_DIR } from './relay-protocol'
import { RELAY_BUILD_PLATFORMS } from '../../shared/relay-artifacts'
import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
@@ -45,61 +13,12 @@ export const RELAY_NATIVE_DEPS_CACHE_COMPLETE_NAME = '.deps-complete'
export const RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX = '.native-gc-'
export const LEGACY_RELAY_NATIVE_DEPS_CACHE_TOMBSTONE_PREFIX = '.gc-tombstone.'
/**
* Bump when the remote install starts mutating the installed tree in a way the hashed inputs
* below cannot see — a new `npm rebuild` flag, a new post-install step, a patch applied by
* something other than a shipped `node-pty-*` artifact. A published entry is never repaired in
* place; only a new key retires it.
*/
export const RELAY_NATIVE_DEPS_CACHE_EPOCH = 1
/**
* Shipped relay artifacts that patch the installed native tree. Their bytes go into the key, so
* changing a patch mints a new entry instead of leaving hosts on a tree built from the old one.
*/
export const RELAY_NATIVE_DEPS_PATCH_ARTIFACT_PATTERN = /^node-pty-.*\.(cjs|js|patch)$/
const CACHE_KEY_HASH_LENGTH = 16
const CACHE_ENTRY_NAME_REGEX = new RegExp(
`^(${RELAY_BUILD_PLATFORMS.join('|')})-[0-9a-f]{${CACHE_KEY_HASH_LENGTH}}$`
)
export type RelayNativeDepsCachePatchSource = {
filename: string
contents: string
}
/**
* `<relayPlatform>-<sha256 prefix>` over the dependency set, the epoch, and every patch the
* remote install applies. Platform and arch stay in the name rather than the hash so an operator
* reading `~/.orca-remote/native/` can tell what an entry is for.
*/
export function computeRelayNativeDepsCacheKey(input: {
platform: string
deps: Readonly<Record<string, string>>
patchSources?: readonly RelayNativeDepsCachePatchSource[]
}): string {
const hash = createHash('sha256')
hash.update(`epoch ${RELAY_NATIVE_DEPS_CACHE_EPOCH}\n`)
for (const [name, version] of Object.entries(input.deps).sort(([a], [b]) => (a < b ? -1 : 1))) {
hash.update(`dep ${name} ${version}\n`)
}
const patches = [...(input.patchSources ?? [])].sort((a, b) => (a.filename < b.filename ? -1 : 1))
for (const patch of patches) {
hash.update(
`patch ${patch.filename} ${createHash('sha256').update(patch.contents).digest('hex')}\n`
)
}
const key = `${input.platform}-${hash.digest('hex').slice(0, CACHE_KEY_HASH_LENGTH)}`
if (!isRelayNativeDepsCacheEntryName(key)) {
// Why: the key reaches the host inside `mv` and `rm -rf`; an unrecognized platform must
// disable the cache rather than arrive as a path fragment nobody validated.
throw new Error(`Unsafe relay native-deps cache key: ${JSON.stringify(key)}`)
}
return key
}
/**
* Whether a name the host listed is one this client may move or delete. Every GC candidate goes
* through here before it reaches a shell.
@@ -143,10 +62,7 @@ export function relayNativeDepsCacheNodeModulesPath(
return joinRemotePath(host, relayNativeDepsCacheEntryDir(host, remoteHome, key), 'node_modules')
}
/**
* Windows hosts install node-pty from an npm prebuilt and then patch the tree in place, so they
* keep the per-directory install. Nothing else about their deploy changes.
*/
/** Older Windows relays never used the shared cache. */
export function supportsRelayNativeDepsCache(host: RemoteHostPlatform): boolean {
return !isWindowsRemoteHost(host)
}