mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
Remove unused legacy relay native-cache creation
This commit is contained in:
@@ -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('')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user