bdy 1.24.4 → 1.24.5-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bdy",
3
3
  "preferGlobal": false,
4
- "version": "1.24.4",
4
+ "version": "1.24.5-dev",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
7
7
  "homepage": "https://buddy.works/docs/cli",
@@ -5,17 +5,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const output_1 = __importDefault(require("../../output"));
7
7
  const utils_1 = require("../../utils");
8
+ const sshChannelWindow_1 = require("../../sshChannelWindow");
8
9
  // Machine-invoked and hidden, like `agent run`. A freshly downloaded binary is executed
9
10
  // with this before it is allowed to replace the installed one (AgentSystem.verifyBinary),
10
11
  // so it has to stay hermetic: skipPre keeps out the latest-version fetch and the probe of
11
12
  // the running agent, leaving exit 0 to mean exactly one thing - this binary runs here.
12
13
  // The version it prints is what tells the caller the archive held the release it asked
13
- // for; nothing else may be written to stdout.
14
+ // for. Machine-read on both lines, so nothing else may go to stdout.
14
15
  const commandAgentSelftest = (0, utils_1.newCommand)('selftest');
15
16
  commandAgentSelftest.skipPre = true;
16
17
  commandAgentSelftest.action(async () => {
18
+ // Output.data, not normal: both lines are payload read by a machine, and that is what it is
19
+ // for - terminal-kit's printf-style formatting would treat a % or ^ in either as markup. It
20
+ // also keeps the two lines on one writer, so the version cannot end up second.
21
+ //
17
22
  // Not exitNormal(): that exits the moment the write is queued, and stdout is a pipe
18
23
  // here, so the version could be lost before it is flushed.
19
- output_1.default.normal((0, utils_1.getVersion)());
24
+ output_1.default.data((0, utils_1.getVersion)());
25
+ // The window ssh2 will really advertise, measured inside the artifact that ships - see
26
+ // effectiveChannelWindow(). build.sh fails a release whose binary does not say 8388608,
27
+ // which is the only check that survives bundling: a rollup bundle has no require.cache,
28
+ // so the ordering guard in sshChannelWindow.ts cannot see a violation there.
29
+ //
30
+ // Printed, not enforced: an agent that would run at ssh2's 2 MB is slower, not broken,
31
+ // and refusing the update over it would keep a fleet on an older binary for a knob.
32
+ output_1.default.data(`sshWindow=${(0, sshChannelWindow_1.effectiveChannelWindow)()}`);
20
33
  });
21
34
  exports.default = commandAgentSelftest;
@@ -119,6 +119,12 @@ class Logger {
119
119
  this.ts = setInterval(() => {
120
120
  this.checkLogSize();
121
121
  }, 30000);
122
+ // A rotation check may never be the reason a process stays up. Without this, every
123
+ // short-lived command that logs a single line keeps its event loop alive forever -
124
+ // the ones that end in Output.exitNormal() get away with it, but `agent selftest`
125
+ // deliberately returns instead of exiting, so the update that runs it read the hang
126
+ // as 'the downloaded binary does not run on this machine' and gave up.
127
+ this.ts.unref?.();
122
128
  }
123
129
  return this.p;
124
130
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sshChannelWindow = void 0;
3
+ exports.effectiveChannelWindow = exports.requireSsh2 = exports.sshChannelWindow = void 0;
4
4
  // The SSH channel window, raised before anything in this process loads ssh2.
5
5
  //
6
6
  // Throughput over one SSH channel is window/RTT, and ssh2 ships a 2 MB window (lib/Channel.js) that is
@@ -13,10 +13,17 @@ exports.sshChannelWindow = void 0;
13
13
  // all (measured: still 11.7 MB/s), which is why this half exists. Past 8 MB the inner SSH becomes the
14
14
  // ceiling and there is nothing left to win: 32 MB measured 18.7 MB/s.
15
15
  //
16
+ // Those numbers predate the refill below being fixed, when the raise only applied to the first burst
17
+ // and the steady-state window was ssh2's 2 MB whatever this said. With the refill honoured, one channel
18
+ // through raw ssh2 does 15.8 MB/s on 2 MB, 30.0 on 4 MB and 59.0 on 8 MB at 60 ms RTT (7.5, 15.2 and
19
+ // 30.1 at 200 ms) - linear in window/RTT, and 8 MB is where a 200 ms link clears the ~19 MB/s an
20
+ // untunneled sftp does. See worker/src/ssh2.js, which carries the same constant and the full set.
21
+ //
16
22
  // Memory: a receive window only becomes resident when the consumer is slower than the sender, bounded
17
- // by the window per busy channel. On an agent that is a handful of channels on one machine, so there
18
- // is no knob here - unlike the worker, which multiplies it by every concurrent transfer on an edge and
19
- // reads SSH_CHANNEL_WINDOW for exactly that reason.
23
+ // by the window per busy channel - measured at 15.6 MB of RSS per SATURATED channel on 8 MB against
24
+ // 9.8 MB on 2 MB. On an agent that is a handful of channels on one machine, so there is no knob here -
25
+ // unlike the worker, which multiplies it by every concurrent transfer on an edge and reads
26
+ // SSH_CHANNEL_WINDOW for exactly that reason.
20
27
  const SSH_CHANNEL_WINDOW = 8 * 1024 * 1024;
21
28
  // Why a mutation rather than a patched dependency: client.js and server.js read these two constants
22
29
  // ONCE, by destructuring lib/Channel.js at module load, so replacing them before ssh2 is first
@@ -27,22 +34,39 @@ const SSH_CHANNEL_WINDOW = 8 * 1024 * 1024;
27
34
  // Imported FIRST in src/index.ts, which is the only entry point (bin/cli.js requires it) and already
28
35
  // the home of process-wide setup like stream.setDefaultHighWaterMark. Requiring only Channel.js here
29
36
  // does not pull in client.js or server.js, so the constants are still unread at this point.
30
- // What happened, for src/index.ts to log once the logger exists. This module deliberately imports
31
- // NOTHING - logger pulls in ./utils, and the first import of the process reaching that far could drag
32
- // ssh2 in with it and silence the very thing being set here.
37
+ //
38
+ // That ordering is not left to whoever edits an import list, though: `requireSsh2` below is the only
39
+ // route to ssh2 in src/, enforced by eslint, so client.js cannot be reached without this module
40
+ // having run first. The check for 'ssh2 is already loaded' is kept for the tsc path, but it is a
41
+ // backstop - in the released bundle there is no require.cache to ask, so it cannot be the thing the
42
+ // guarantee rests on.
43
+ //
44
+ // This module deliberately imports NOTHING - logger pulls in ./utils, and the first import of the
45
+ // process reaching that far could drag ssh2 in with it and silence the very thing being set here.
46
+ // What it found is left on `sshChannelWindow` for src/index.ts to log once the logger exists.
33
47
  exports.sshChannelWindow = { bytes: SSH_CHANNEL_WINDOW, applied: false, reason: '' };
34
48
  const applyChannelWindow = () => {
35
49
  try {
36
50
  // If ssh2 is already loaded the constants have been read and this can no longer take effect. It
37
51
  // means a new module started importing ssh2 above this line - the symptom would otherwise be
38
52
  // nothing but slower uploads.
39
- if (Object.keys(require.cache).some((f) => f.includes(`ssh2${'/'}lib${'/'}client.js`))) {
53
+ //
54
+ // Only asked where there is a cache to ask: the released binary is a rollup bundle with its own
55
+ // require shim, where `require.cache` is undefined and reading it threw - which took the window
56
+ // raise with it in every build we ship (1.24.3 and 1.24.4 both run on ssh2's 2 MB) and, worse,
57
+ // made src/index.ts log at startup. In a bundle the load order is the one this file sets, so
58
+ // there is nothing to detect: go straight to the mutation.
59
+ const cache = require.cache;
60
+ if (cache &&
61
+ Object.keys(cache).some((f) => f.includes(`ssh2${'/'}lib${'/'}client.js`))) {
40
62
  exports.sshChannelWindow.reason = 'ssh2 was already loaded, so its constants had been read';
41
63
  return;
42
64
  }
43
65
  const channel = require('ssh2/lib/Channel.js');
44
- if (typeof channel.MAX_WINDOW !== 'number' || typeof channel.WINDOW_THRESHOLD !== 'number') {
45
- exports.sshChannelWindow.reason = 'ssh2 no longer exposes MAX_WINDOW/WINDOW_THRESHOLD';
66
+ if (typeof channel.MAX_WINDOW !== 'number' ||
67
+ typeof channel.WINDOW_THRESHOLD !== 'number' ||
68
+ typeof channel.windowAdjust !== 'function') {
69
+ exports.sshChannelWindow.reason = 'ssh2 no longer exposes MAX_WINDOW/WINDOW_THRESHOLD/windowAdjust';
46
70
  return;
47
71
  }
48
72
  if (SSH_CHANNEL_WINDOW < channel.MAX_WINDOW) {
@@ -53,6 +77,39 @@ const applyChannelWindow = () => {
53
77
  // ssh2 tops the window up once half of it is spent. Left at its own 1 MB it would top up only in
54
78
  // the last eighth of an 8 MB window, which is exactly when a fast sender runs dry.
55
79
  channel.WINDOW_THRESHOLD = Math.floor(SSH_CHANNEL_WINDOW / 2);
80
+ // And the refill itself, which is where the two constants above stopped being worth anything.
81
+ // client.js and server.js destructure `windowAdjust` from here, but the copy inside Channel.js
82
+ // closes over ITS OWN 2 MB const, so it refilled to 2 MB whatever these two say: measured on a
83
+ // 60 MB transfer, the window opened at 8 MB and every refill after it was 32 KB back up to 2 MB
84
+ // - 1665 window-adjust packets where 14 would do, and a steady-state window of 2 MB. So the raise
85
+ // only ever bought the first burst. Three lines, ssh2's own (Channel.js windowAdjust), with the
86
+ // window read from here - and, like the constants, it has to be in place before client.js
87
+ // destructures it. What still tops up to 2 MB is Channel.js's internal call on the drain path;
88
+ // the next arriving packet brings the window back to full through this one.
89
+ //
90
+ // worker/src/ssh2.js holds the same three lines for the edge, and has the same tests: a window
91
+ // raised on one end alone changes nothing measurable, so an ssh2 bump has to move both.
92
+ const refill = (self) => {
93
+ if (self.outgoing.state === 'closed')
94
+ return;
95
+ const amt = SSH_CHANNEL_WINDOW - self.incoming.window;
96
+ if (amt <= 0)
97
+ return;
98
+ self.incoming.window += amt;
99
+ self._client._protocol.channelWindowAdjust(self.outgoing.id, amt);
100
+ };
101
+ channel.windowAdjust = refill;
102
+ // Read back rather than assume. A bundler that hands consumers a copy of these exports, a frozen
103
+ // module, an ssh2 that redefines them as getters - all of that would leave the three assignments
104
+ // above doing nothing, and `applied: true` would then be a claim nobody checked. What this still
105
+ // cannot see is whether client.js had already destructured them; that is what
106
+ // effectiveChannelWindow() is for, and what the build gate runs on the shipped binary.
107
+ if (channel.MAX_WINDOW !== SSH_CHANNEL_WINDOW ||
108
+ channel.WINDOW_THRESHOLD !== Math.floor(SSH_CHANNEL_WINDOW / 2) ||
109
+ channel.windowAdjust !== refill) {
110
+ exports.sshChannelWindow.reason = 'ssh2 kept its own constants, the assignment did not take';
111
+ return;
112
+ }
56
113
  exports.sshChannelWindow.applied = true;
57
114
  }
58
115
  catch (err) {
@@ -63,4 +120,42 @@ const applyChannelWindow = () => {
63
120
  }
64
121
  };
65
122
  applyChannelWindow();
123
+ // The only route to ssh2 in src/, so the window above is always in place before client.js and
124
+ // server.js destructure the constants - eslint refuses `import ... from 'ssh2'` and `require('ssh2')`
125
+ // everywhere else (type-only imports are fine, they carry no runtime load). Kept lazy on purpose:
126
+ // requiring ssh2 costs ~70 ms of native binding and protocol module loading, and most commands never
127
+ // open a channel - `bdy --version` should not pay for one.
128
+ const requireSsh2 = () => require('ssh2');
129
+ exports.requireSsh2 = requireSsh2;
130
+ // What client.js ACTUALLY captured, which is the only honest answer to "is the window raised". It
131
+ // destructures MAX_WINDOW at module load, so a mutation that arrived late leaves this at ssh2's 2 MB
132
+ // while Channel.js's exports read 8 MB - the shape that shipped in 1.24.3 and 1.24.4 and that no
133
+ // negative check can see in a bundle, where there is no require.cache to consult.
134
+ //
135
+ // Measured, not asserted: openChannel() passes the window it captured to _protocol.session(), so a
136
+ // Client with a stubbed protocol reports it without a socket, a server or any I/O. `agent selftest`
137
+ // prints it and build.sh fails the release if the binary it just built does not say 8388608.
138
+ const effectiveChannelWindow = () => {
139
+ try {
140
+ const { Client } = (0, exports.requireSsh2)();
141
+ const client = new Client();
142
+ let captured = 0;
143
+ // The shape ssh2's isWritable() asks for (lib/utils.js), and nothing more - no socket is opened
144
+ client._sock = { writable: true, _readableState: { ended: false } };
145
+ client._chanMgr = { add: () => 0 };
146
+ client._protocol = {
147
+ session: (_id, window) => {
148
+ captured = window;
149
+ },
150
+ };
151
+ client.exec('window-probe', () => { });
152
+ return captured;
153
+ }
154
+ catch {
155
+ // ssh2's internals moved. Nothing here is worth failing a command over - the build gate reads
156
+ // the number, and 0 is not the number it wants.
157
+ return 0;
158
+ }
159
+ };
160
+ exports.effectiveChannelWindow = effectiveChannelWindow;
66
161
  exports.default = SSH_CHANNEL_WINDOW;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSshHostKey = void 0;
4
+ const sshChannelWindow_1 = require("./sshChannelWindow");
4
5
  const createSshHostKey = () => {
5
- const Ssh2 = require('ssh2');
6
+ const Ssh2 = (0, sshChannelWindow_1.requireSsh2)();
6
7
  const keys = Ssh2.utils.generateKeyPairSync('ed25519', {});
7
8
  return keys.private;
8
9
  };
@@ -4,13 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const logger_1 = __importDefault(require("../../logger"));
7
- const ssh2_1 = __importDefault(require("ssh2"));
8
7
  const promises_1 = __importDefault(require("fs/promises"));
9
8
  const fs_1 = require("fs");
10
9
  const path_1 = require("path");
11
10
  const node_os_1 = require("node:os");
12
11
  const utils_1 = require("../../utils");
13
- const { STATUS_CODE } = ssh2_1.default.utils.sftp;
12
+ const sshChannelWindow_1 = require("../../sshChannelWindow");
13
+ const ssh2 = (0, sshChannelWindow_1.requireSsh2)();
14
+ const { STATUS_CODE } = ssh2.utils.sftp;
14
15
  var SftpOpenFlag;
15
16
  (function (SftpOpenFlag) {
16
17
  SftpOpenFlag[SftpOpenFlag["READ"] = 1] = "READ";
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const events_1 = __importDefault(require("events"));
7
- const ssh2_1 = __importDefault(require("ssh2"));
8
7
  const logger_1 = __importDefault(require("../../logger"));
9
8
  const crypto_1 = require("crypto");
10
9
  const child_process_1 = require("child_process");
@@ -12,6 +11,9 @@ const node_net_1 = __importDefault(require("node:net"));
12
11
  const sftp_1 = __importDefault(require("./sftp"));
13
12
  const buddy_1 = __importDefault(require("../api/buddy"));
14
13
  const utils_1 = require("../../utils");
14
+ const sshChannelWindow_1 = require("../../sshChannelWindow");
15
+ // The window is applied by the module this comes from, so ssh2 cannot be reached before it ran
16
+ const ssh2 = (0, sshChannelWindow_1.requireSsh2)();
15
17
  const pipeStreamToChannel = (stream, channel) => {
16
18
  channel.once('exit', (code, signal) => {
17
19
  logger_1.default.debug(`shell channel exit: ${code}, signal: ${signal}`);
@@ -38,7 +40,7 @@ class ServerSsh extends events_1.default {
38
40
  this.agent = agent;
39
41
  this.login = login;
40
42
  this.password = password;
41
- this.server = new ssh2_1.default.Server({
43
+ this.server = new ssh2.Server({
42
44
  hostKeys: [hostKey],
43
45
  highWaterMark: 65536,
44
46
  ident: 'SSH-2.0-Buddy',
@@ -97,7 +99,7 @@ class ServerSsh extends events_1.default {
97
99
  for (let i = 0; i < keys.length; i += 1) {
98
100
  const key = keys[i];
99
101
  const content = key.content ? key.content : key;
100
- const publicKey = ssh2_1.default.utils.parseKey(content);
102
+ const publicKey = ssh2.utils.parseKey(content);
101
103
  if (publicKey instanceof Error) {
102
104
  continue;
103
105
  }
@@ -127,7 +129,7 @@ class ServerSsh extends events_1.default {
127
129
  }
128
130
  createProxyConnection(privateKey, user) {
129
131
  return new Promise((resolve, reject) => {
130
- const client = new ssh2_1.default.Client();
132
+ const client = new ssh2.Client();
131
133
  client.once('error', (err) => {
132
134
  client.removeAllListeners();
133
135
  reject(err);
@@ -3,12 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const ssh2_1 = require("ssh2");
7
6
  const events_1 = __importDefault(require("events"));
8
7
  const logger_1 = __importDefault(require("../../logger"));
9
8
  const texts_1 = require("../../texts");
10
9
  const utils_1 = require("../../utils");
10
+ const sshChannelWindow_1 = require("../../sshChannelWindow");
11
11
  const tunnel_1 = require("../../types/tunnel");
12
+ // Through sshChannelWindow, never straight from 'ssh2': the window is applied when that module is
13
+ // imported, so client.js cannot read the constants before it - the placement of this line has
14
+ // nothing to do with it, imports hoist above it either way. eslint enforces the route.
15
+ const ssh2 = (0, sshChannelWindow_1.requireSsh2)();
12
16
  class SshClient extends events_1.default {
13
17
  ip;
14
18
  port;
@@ -47,7 +51,7 @@ class SshClient extends events_1.default {
47
51
  async open() {
48
52
  return new Promise((resolve, reject) => {
49
53
  this.close();
50
- this.client = new ssh2_1.Client();
54
+ this.client = new ssh2.Client();
51
55
  this.client.setNoDelay();
52
56
  this.client.on('ready', () => {
53
57
  if (this.client) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bdy",
3
3
  "preferGlobal": false,
4
- "version": "1.24.4",
4
+ "version": "1.24.5-dev",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
7
7
  "homepage": "https://buddy.works/docs/cli",