fix SSH ping CPU spin and Linux releases
This commit is contained in:
@@ -222,11 +222,82 @@ async function testCompleteAppTeardownIsImmediateAndIdempotent() {
|
|||||||
assert.strictEqual(app._closingSessions.size, 0);
|
assert.strictEqual(app._closingSessions.size, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testLatencyPingIsSingleFlightAndIgnoresStaleResults() {
|
||||||
|
const flushAsync = () => new Promise((resolve) => setImmediate(resolve));
|
||||||
|
let nextTimerID = 1;
|
||||||
|
const timers = new Map();
|
||||||
|
const latencyEl = { textContent: "", className: "" };
|
||||||
|
const context = loadScript("src/js/app.js", {
|
||||||
|
setTimeout: (callback) => {
|
||||||
|
const id = nextTimerID++;
|
||||||
|
timers.set(id, callback);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
clearTimeout: (id) => timers.delete(id),
|
||||||
|
document: {
|
||||||
|
querySelector: () => null,
|
||||||
|
getElementById: (id) => (id === "status-latency" ? latencyEl : null),
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const App = context.window.NexusTerminal.App;
|
||||||
|
const app = Object.create(App.prototype);
|
||||||
|
app._latencySessionID = null;
|
||||||
|
app._latencyTimer = null;
|
||||||
|
app._latencyGeneration = 0;
|
||||||
|
app._latencyPingInFlight = false;
|
||||||
|
|
||||||
|
const pending = [];
|
||||||
|
context.window.NexusTerminal.wailsService = {
|
||||||
|
PingSession: (sessionID) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
pending.push({ sessionID, resolve, reject });
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
app._startLatencyPing("session-a");
|
||||||
|
app._startLatencyPing("session-a");
|
||||||
|
assert.strictEqual(pending.length, 1, "an in-flight ping must not overlap");
|
||||||
|
assert.strictEqual(timers.size, 0, "the next ping waits for completion");
|
||||||
|
|
||||||
|
app._startLatencyPing("session-b");
|
||||||
|
assert.strictEqual(pending.length, 2);
|
||||||
|
pending[0].reject(new Error("stale session closed"));
|
||||||
|
await flushAsync();
|
||||||
|
assert.strictEqual(app._latencySessionID, "session-b");
|
||||||
|
assert.strictEqual(
|
||||||
|
app._latencyPingInFlight,
|
||||||
|
true,
|
||||||
|
"a stale rejection must not stop the current session ping",
|
||||||
|
);
|
||||||
|
|
||||||
|
pending[1].resolve(42);
|
||||||
|
await flushAsync();
|
||||||
|
assert.strictEqual(latencyEl.textContent, "42 ms");
|
||||||
|
assert.strictEqual(latencyEl.className, "latency-good");
|
||||||
|
assert.strictEqual(timers.size, 1, "one follow-up ping should be scheduled");
|
||||||
|
|
||||||
|
const followUp = timers.values().next().value;
|
||||||
|
timers.clear();
|
||||||
|
followUp();
|
||||||
|
assert.strictEqual(pending.length, 3);
|
||||||
|
assert.strictEqual(pending[2].sessionID, "session-b");
|
||||||
|
assert.strictEqual(timers.size, 0, "follow-up pings must also be single-flight");
|
||||||
|
|
||||||
|
pending[2].resolve(250);
|
||||||
|
await flushAsync();
|
||||||
|
assert.strictEqual(latencyEl.className, "latency-bad");
|
||||||
|
assert.strictEqual(timers.size, 1);
|
||||||
|
app._stopLatencyPing();
|
||||||
|
assert.strictEqual(timers.size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await testRepeatedSplitCleanup();
|
await testRepeatedSplitCleanup();
|
||||||
testClosedPaneRejectsLateConnection();
|
testClosedPaneRejectsLateConnection();
|
||||||
testPerSessionStoresReleaseIDs();
|
testPerSessionStoresReleaseIDs();
|
||||||
await testCompleteAppTeardownIsImmediateAndIdempotent();
|
await testCompleteAppTeardownIsImmediateAndIdempotent();
|
||||||
|
await testLatencyPingIsSingleFlightAndIgnoresStaleResults();
|
||||||
console.log("Session lifecycle regression tests passed.");
|
console.log("Session lifecycle regression tests passed.");
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
+45
-7
@@ -49,6 +49,10 @@
|
|||||||
this.leviathan = new LeviathanWorkbench(this);
|
this.leviathan = new LeviathanWorkbench(this);
|
||||||
/** @type {Map<string, Promise<void>>} Idempotent in-flight session closes. */
|
/** @type {Map<string, Promise<void>>} Idempotent in-flight session closes. */
|
||||||
this._closingSessions = new Map();
|
this._closingSessions = new Map();
|
||||||
|
this._latencySessionID = null;
|
||||||
|
this._latencyTimer = null;
|
||||||
|
this._latencyGeneration = 0;
|
||||||
|
this._latencyPingInFlight = false;
|
||||||
this.updateInfo = null;
|
this.updateInfo = null;
|
||||||
this.mode = "nexus";
|
this.mode = "nexus";
|
||||||
}
|
}
|
||||||
@@ -1362,17 +1366,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_startLatencyPing(sessionID) {
|
_startLatencyPing(sessionID) {
|
||||||
// Don't restart if already pinging the same session.
|
// Do not queue overlapping backend requests for the active session.
|
||||||
if (this._latencySessionID === sessionID && this._latencyTimer) return;
|
if (
|
||||||
|
this._latencySessionID === sessionID &&
|
||||||
|
(this._latencyTimer || this._latencyPingInFlight)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._stopLatencyPing();
|
this._stopLatencyPing();
|
||||||
this._latencySessionID = sessionID;
|
this._latencySessionID = sessionID;
|
||||||
|
const generation = this._latencyGeneration;
|
||||||
|
|
||||||
|
const isCurrent = () =>
|
||||||
|
this._latencyGeneration === generation &&
|
||||||
|
this._latencySessionID === sessionID;
|
||||||
|
|
||||||
|
const scheduleNext = () => {
|
||||||
|
if (!isCurrent() || this._latencyTimer) return;
|
||||||
|
this._latencyTimer = setTimeout(() => {
|
||||||
|
this._latencyTimer = null;
|
||||||
|
void doPing();
|
||||||
|
}, 10000);
|
||||||
|
};
|
||||||
|
|
||||||
const doPing = async () => {
|
const doPing = async () => {
|
||||||
if (!window.NexusTerminal?.wailsService) return;
|
if (!isCurrent()) return;
|
||||||
|
if (!window.NexusTerminal?.wailsService) {
|
||||||
|
scheduleNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._latencyPingInFlight = true;
|
||||||
|
let succeeded = false;
|
||||||
try {
|
try {
|
||||||
const ms = await window.NexusTerminal.wailsService.PingSession(sessionID);
|
const ms = await window.NexusTerminal.wailsService.PingSession(sessionID);
|
||||||
|
if (!isCurrent()) return;
|
||||||
|
succeeded = true;
|
||||||
const el = document.getElementById("status-latency");
|
const el = document.getElementById("status-latency");
|
||||||
if (el && this._latencySessionID === sessionID) {
|
if (el) {
|
||||||
el.textContent = ms + " ms";
|
el.textContent = ms + " ms";
|
||||||
el.className =
|
el.className =
|
||||||
ms < 100
|
ms < 100
|
||||||
@@ -1382,6 +1413,7 @@
|
|||||||
: "latency-bad";
|
: "latency-bad";
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
if (!isCurrent()) return;
|
||||||
// Session may have disconnected — clear indicator.
|
// Session may have disconnected — clear indicator.
|
||||||
const el = document.getElementById("status-latency");
|
const el = document.getElementById("status-latency");
|
||||||
if (el) {
|
if (el) {
|
||||||
@@ -1389,18 +1421,24 @@
|
|||||||
el.className = "";
|
el.className = "";
|
||||||
}
|
}
|
||||||
this._stopLatencyPing();
|
this._stopLatencyPing();
|
||||||
|
} finally {
|
||||||
|
if (isCurrent()) {
|
||||||
|
this._latencyPingInFlight = false;
|
||||||
|
if (succeeded) scheduleNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
doPing();
|
void doPing();
|
||||||
this._latencyTimer = setInterval(doPing, 10000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_stopLatencyPing() {
|
_stopLatencyPing() {
|
||||||
|
this._latencyGeneration = (this._latencyGeneration || 0) + 1;
|
||||||
if (this._latencyTimer) {
|
if (this._latencyTimer) {
|
||||||
clearInterval(this._latencyTimer);
|
clearTimeout(this._latencyTimer);
|
||||||
this._latencyTimer = null;
|
this._latencyTimer = null;
|
||||||
}
|
}
|
||||||
|
this._latencyPingInFlight = false;
|
||||||
this._latencySessionID = null;
|
this._latencySessionID = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/wailsapp/wails/v2 v2.12.0
|
github.com/wailsapp/wails/v2 v2.12.0
|
||||||
github.com/zalando/go-keyring v0.2.8
|
github.com/zalando/go-keyring v0.2.8
|
||||||
golang.org/x/crypto v0.52.0
|
golang.org/x/crypto v0.55.0
|
||||||
golang.org/x/net v0.55.0
|
golang.org/x/net v0.57.0
|
||||||
golang.org/x/text v0.37.0
|
golang.org/x/text v0.41.0
|
||||||
modernc.org/sqlite v1.53.0
|
modernc.org/sqlite v1.53.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ require (
|
|||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
golang.org/x/sys v0.45.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
modernc.org/libc v1.73.4 // indirect
|
modernc.org/libc v1.73.4 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
@@ -99,31 +99,31 @@ github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozH
|
|||||||
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
||||||
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
|
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
|
||||||
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
|
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
|
||||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package sshclient
|
package sshclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -57,3 +59,94 @@ func TestJumpHostClientConfigUsesFinalHostKeyCallback(t *testing.T) {
|
|||||||
t.Fatal("final host key callback was not used")
|
t.Fatal("final host key callback was not used")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPingReturnsAfterSSHTransportCloses(t *testing.T) {
|
||||||
|
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateKey() error = %v", err)
|
||||||
|
}
|
||||||
|
signer, err := ssh.NewSignerFromKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSignerFromKey() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConfig := &ssh.ServerConfig{NoClientAuth: true}
|
||||||
|
serverConfig.AddHostKey(signer)
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Listen() error = %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = listener.Close() })
|
||||||
|
|
||||||
|
serverReady := make(chan *ssh.ServerConn, 1)
|
||||||
|
serverErrors := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
conn, acceptErr := listener.Accept()
|
||||||
|
if acceptErr != nil {
|
||||||
|
serverErrors <- acceptErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverConn, _, _, handshakeErr := ssh.NewServerConn(conn, serverConfig)
|
||||||
|
if handshakeErr != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
serverErrors <- handshakeErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverReady <- serverConn
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", listener.Addr().String(), 2*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DialTimeout() error = %v", err)
|
||||||
|
}
|
||||||
|
clientConn, channels, requests, err := ssh.NewClientConn(conn, listener.Addr().String(), &ssh.ClientConfig{
|
||||||
|
User: "test",
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Test server uses an ephemeral key.
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("NewClientConn() error = %v", err)
|
||||||
|
}
|
||||||
|
rawClient := ssh.NewClient(clientConn, channels, requests)
|
||||||
|
|
||||||
|
var serverConn *ssh.ServerConn
|
||||||
|
select {
|
||||||
|
case serverConn = <-serverReady:
|
||||||
|
case serverErr := <-serverErrors:
|
||||||
|
t.Fatalf("SSH server handshake error = %v", serverErr)
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for SSH server handshake")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(&models.Profile{})
|
||||||
|
client.mu.Lock()
|
||||||
|
client.sshClient = rawClient
|
||||||
|
client.connected = true
|
||||||
|
client.mu.Unlock()
|
||||||
|
t.Cleanup(func() { _ = client.Close() })
|
||||||
|
|
||||||
|
if err := serverConn.Close(); err != nil {
|
||||||
|
t.Fatalf("server Close() error = %v", err)
|
||||||
|
}
|
||||||
|
waitDone := make(chan error, 1)
|
||||||
|
go func() { waitDone <- rawClient.Wait() }()
|
||||||
|
select {
|
||||||
|
case <-waitDone:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for SSH transport to close")
|
||||||
|
}
|
||||||
|
|
||||||
|
pingDone := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, pingErr := client.Ping()
|
||||||
|
pingDone <- pingErr
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case pingErr := <-pingDone:
|
||||||
|
if pingErr == nil {
|
||||||
|
t.Fatal("Ping() succeeded after the SSH transport closed")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Ping() did not return after the SSH transport closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
ARG GO_IMAGE
|
||||||
|
FROM ${GO_IMAGE}
|
||||||
|
|
||||||
|
ARG NODE_ARCH
|
||||||
|
ARG NODE_VERSION
|
||||||
|
ARG WAILS_VERSION
|
||||||
|
|
||||||
|
ENV PATH="/opt/node/bin:/go/bin:/usr/local/go/bin:${PATH}"
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
libdigest-sha-perl \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
pkg-config \
|
||||||
|
xz-utils \
|
||||||
|
zip \
|
||||||
|
&& node_archive="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" \
|
||||||
|
&& curl -fsSLo "/tmp/${node_archive}" \
|
||||||
|
"https://nodejs.org/dist/v${NODE_VERSION}/${node_archive}" \
|
||||||
|
&& mkdir -p /opt/node \
|
||||||
|
&& tar -xJf "/tmp/${node_archive}" -C /opt/node --strip-components=1 \
|
||||||
|
&& rm -f "/tmp/${node_archive}" \
|
||||||
|
&& go install "github.com/wailsapp/wails/v2/cmd/wails@${WAILS_VERSION}" \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
Reference in New Issue
Block a user