From 98d8cfa993b1ea85c4d67e3c23139f892232a683 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 25 Sep 2026 23:22:16 +0200 Subject: [PATCH] Network: confirm a change only after it has been fully applied, keep the session when the old connection times out, and retry reloading so changes show up without reopening the tab Co-Authored-By: Claude Opus 5.5 --- src/main/network.js | 23 +++++++++++++---------- src/renderer/app.js | 11 ++++++++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/main/network.js b/src/main/network.js index f86682a..dbcc75e 100644 --- a/src/main/network.js +++ b/src/main/network.js @@ -149,8 +149,10 @@ class NetworkConfigManager { } watch(id, s) { - s.conn.on('close', () => { if (this.sessions.get(id) === s && !s.replacing) { this.close(id); s.onClose(); } }); - s.conn.on('error', () => {}); + const conn = s.conn; + // Nur die aktuelle Verbindung darf die Sitzung beenden (nach apply wird sie ersetzt) + conn.on('close', () => { if (this.sessions.get(id) === s && s.conn === conn) { this.close(id); s.onClose(); } }); + conn.on('error', () => {}); } async read(id) { @@ -226,7 +228,7 @@ class NetworkConfigManager { const be = BACKENDS[backendName || s.backend]; if (!be) throw new Error(i18n.t('Editing is not supported for this network configuration ({backend}).', { backend: s.backend })); const script = `export PATH=$PATH:/usr/sbin:/sbin -mkdir -p ${STATE} || exit 1; rm -f ${STATE}/pending ${STATE}/rolled-back +mkdir -p ${STATE} || exit 1; rm -f ${STATE}/pending ${STATE}/applied ${STATE}/rolled-back cd / && tar czf ${STATE}/backup.tgz $(for p in ${be.paths.join(' ')}; do [ -e "$p" ] && echo "$p"; done) || exit 1 # Schreiben in einer Subshell: bei einem Fehler sofort den alten Stand wiederherstellen if ! ( set -e @@ -234,7 +236,7 @@ ${writes.join('\n')} ) >${STATE}/write.log 2>&1; then ${be.restore}; cat ${STATE}/write.log >&2; exit 5; fi if ! (${be.validate}) >${STATE}/validate.log 2>&1; then ${be.restore}; cat ${STATE}/validate.log >&2; exit 4; fi touch ${STATE}/pending -nohup setsid sh -c 'export PATH=$PATH:/usr/sbin:/sbin; sleep 2; (${be.apply}) >${STATE}/apply.log 2>&1; sleep ${ROLLBACK_SECONDS}; if [ -f ${STATE}/pending ]; then ${be.restore}; (${be.apply}) >>${STATE}/apply.log 2>&1; rm -f ${STATE}/pending; date "+%Y-%m-%d %H:%M:%S" > ${STATE}/rolled-back; fi' >/dev/null 2>&1 & +nohup setsid sh -c 'export PATH=$PATH:/usr/sbin:/sbin; sleep 2; (${be.apply}) >${STATE}/apply.log 2>&1; touch ${STATE}/applied; sleep ${ROLLBACK_SECONDS}; if [ -f ${STATE}/pending ]; then ${be.restore}; (${be.apply}) >>${STATE}/apply.log 2>&1; rm -f ${STATE}/pending; date "+%Y-%m-%d %H:%M:%S" > ${STATE}/rolled-back; fi' >/dev/null 2>&1 & echo started`; const r = await this.root(s, script); if (r.code === 4) throw new Error(i18n.t('The new configuration is invalid and was not applied: {err}', { err: lastLine(r.err) })); @@ -245,13 +247,14 @@ echo started`; while (Date.now() < deadline) { try { const { conn, jumps } = await this.ssh.connect(target, id); - const c = await this.root(s, `rm -f ${STATE}/pending`, conn); - if (c.code) { conn.end(); throw new Error(lastLine(c.err)); } + // Erst bestätigen, wenn das Anwenden fertig ist: sonst reißt z. B. ein Bond-Neustart die neue Verbindung wieder ab + const c = await this.root(s, `[ -f ${STATE}/applied ] || exit 7; rm -f ${STATE}/pending`, conn); + if (c.code) { conn.on('error', () => {}); conn.end(); jumps?.forEach((x) => { try { x.end(); } catch {} }); throw new Error(lastLine(c.err) || 'not applied yet'); } // Neue Verbindung übernimmt die Sitzung - s.replacing = true; - try { s.conn.end(); } catch {} - s.jumps?.forEach((x) => { try { x.end(); } catch {} }); - Object.assign(s, { conn, jumps, replacing: false }); + const old = { conn: s.conn, jumps: s.jumps }; + Object.assign(s, { conn, jumps }); + // Alte Verbindung ist nach dem IP-Wechsel meist tot: Fehler (z. B. Keepalive-Timeout) still verwerfen + for (const c of [old.conn, ...(old.jumps || [])]) { c.on('error', () => {}); try { c.end(); } catch {} } if (verifyAddress) s.host = target; this.watch(id, s); return { confirmed: true }; diff --git a/src/renderer/app.js b/src/renderer/app.js index 94d7f84..cf700ee 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -2185,8 +2185,13 @@ class NetworkSession { this.draw(); } - async refresh() { - try { this.apply(await api.call('network:read', this.id)); } catch (e) { toast(e.message, 'error'); } + async refresh(tries = 1) { + for (let i = 1; ; i++) { + try { return this.apply(await api.call('network:read', this.id)); } catch (e) { + if (i >= tries) return toast(e.message, 'error'); + await new Promise((r) => setTimeout(r, 3000)); + } + } } cfg(name) { return this.data.config.find((c) => c.name === name); } @@ -2258,7 +2263,7 @@ class NetworkSession { try { const r = await fn(); ov.remove(); - if (r?.confirmed) { toast(r.direct ? T('Saved') : T('Network change applied and confirmed'), 'ok'); await this.refresh(); } + if (r?.confirmed) { toast(r.direct ? T('Saved') : T('Network change applied and confirmed'), 'ok'); await this.refresh(4); } else { setTabState(this, 'err'); this.body.innerHTML = `
${T('MrTerm could not reconnect after the change. The server restores the previous configuration automatically within 90 seconds.')}
`;