Skip to content

Commit 483f01d

Browse files
authored
fix(ssh): replace connection pool poller with lazy acquire and bounded backoff (#2316)
* FIX: Prevent the SSH reconnect storm on connection loss and auto-dismiss the reconnect modal * ADD: Show the retry count and a countdown to the next attempt in the reconnect modal * CHORE: Bump the launcher to 2.5.3 and update dependencies to clear all audit vulnerabilities * FIX: Detect connection loss faster, close tunnels on loss and harden the install check
1 parent 6bd7cbc commit 483f01d

13 files changed

Lines changed: 2155 additions & 1290 deletions

File tree

‎launcher/package-lock.json‎

Lines changed: 1435 additions & 1207 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎launcher/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "stereum-launcher",
3-
"version": "2.5.2",
3+
"version": "2.5.3",
44
"private": true,
55
"description": "Stereum Ethereum Node Setup Launcher",
66
"author": {

‎launcher/src/backend/HetznerServer.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export class HetznerServer {
216216
}
217217

218218
async finishTestGracefully(nodeConnection) {
219-
clearInterval(nodeConnection.sshService.checkPoolPolling);
219+
nodeConnection.sshService.reconnectAbort?.abort();
220220
await this.Sleep(10000);
221221
await nodeConnection.sshService.disconnect();
222222
await this.deleteSSHKey();

‎launcher/src/backend/Monitoring.js‎

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const globalMonitoringCache = {
2323
};
2424

2525
export class Monitoring {
26+
// A transient exec failure used to read as "not installed", which is the dangerous direction:
27+
// it offers to install over a working node. Retry before believing an inconclusive answer.
28+
static INSTALL_CHECK_ATTEMPTS = 3;
29+
static INSTALL_CHECK_RETRY_MS = 500;
30+
2631
constructor(nodeConnection) {
2732
this.nodeConnection = nodeConnection;
2833
this.nodeConnectionProm = nodeConnection;
@@ -87,10 +92,12 @@ export class Monitoring {
8792
// Start global monitoring cache refreshing periodically each X seconds async in background
8893
// Therefore no "await" is used for implemented calls (otherwise the login is blocked)!
8994
async startGlobalMonitoringCacheBackgroundWorker() {
90-
this.refreshGlobalMonitoringCache();
95+
// Deliberately not awaited (it would block login), so it needs its own catch: during a
96+
// disconnect these reject with "Logging Out!" and surfaced as UnhandledPromiseRejection.
97+
this.refreshGlobalMonitoringCache().catch((err) => log.debug("global monitoring refresh skipped: ", err?.message || err));
9198
this.globalMonitoringCache.intervalHandler = setInterval(
9299
function (me) {
93-
me.refreshGlobalMonitoringCache();
100+
me.refreshGlobalMonitoringCache().catch((err) => log.debug("global monitoring refresh skipped: ", err?.message || err));
94101
},
95102
this.globalMonitoringCache.refreshIntervalSeconds * 1000,
96103
this
@@ -104,29 +111,42 @@ export class Monitoring {
104111
}
105112
}
106113

114+
/**
115+
* Whether /etc/stereum/stereum.yaml exists on the node.
116+
*
117+
* A wrong "false" here sends the user to the install screen for a node that is already set up,
118+
* so the check must not conflate "absent" with "could not tell". The probe echoes an explicit
119+
* marker instead of parsing `ls` output, which was both locale dependent (it matched the
120+
* English "No such file or directory") and unable to distinguish a failed command from a
121+
* missing file. Anything other than a marker is retried.
122+
*/
107123
async checkStereumInstallation(nodeConnection) {
108124
if (!nodeConnection) {
109125
nodeConnection = this.nodeConnection;
110126
}
111-
if (nodeConnection.sshService.connected) {
112-
try {
113-
const settings = await nodeConnection.sshService.exec("ls /etc/stereum");
114-
115-
// Return true if stdout includes "stereum.yaml"
116-
if (settings?.stdout && settings.stdout.includes("stereum.yaml")) {
117-
return true;
118-
}
127+
const ssh = nodeConnection.sshService;
128+
const probe = "test -e /etc/stereum/stereum.yaml && echo STEREUM_PRESENT || echo STEREUM_ABSENT";
119129

120-
// Return false if stderr includes "No such file or directory"
121-
if (settings?.stderr && settings.stderr.includes("No such file or directory")) {
122-
return false;
123-
}
130+
for (let attempt = 1; attempt <= Monitoring.INSTALL_CHECK_ATTEMPTS; attempt++) {
131+
if (attempt > 1) await new Promise((r) => setTimeout(r, Monitoring.INSTALL_CHECK_RETRY_MS));
132+
if (!ssh.connected) {
133+
log.warn(`Stereum installation check: not connected (attempt ${attempt})`);
134+
continue;
135+
}
136+
let result;
137+
try {
138+
result = await ssh.exec(probe);
124139
} catch (err) {
125-
log.debug("checking stereum installation failed:", err);
140+
log.warn(`Stereum installation check failed (attempt ${attempt}): `, err?.message || err);
141+
continue;
126142
}
143+
// A marker means the command actually ran, so its answer is trustworthy
144+
if (result?.stdout?.includes("STEREUM_PRESENT")) return true;
145+
if (result?.stdout?.includes("STEREUM_ABSENT")) return false;
146+
log.warn(`Stereum installation check inconclusive (attempt ${attempt}): rc=${result?.rc} stderr=${result?.stderr}`);
127147
}
128148

129-
// Default return false if none of the above conditions are met
149+
log.error("Could not determine whether Stereum is installed; treating the node as not installed");
130150
return false;
131151
}
132152

‎launcher/src/backend/NodeConnection.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export class NodeConnection {
4040
await this.sshService.disconnect(true);
4141
}
4242
await this.sshService.connect(this.nodeConnectionParams, currentWindow);
43-
this.sshService.addingConnection = true;
4443
await this.findStereumSettings();
4544
this.taskManager = taskManager;
4645
} catch (error) {

0 commit comments

Comments
 (0)