Public Access
Announce a session's end when it is actually over, and for closes too
The phone's notification kept saying '1 shell connected' after the shell was gone, and both close routes were at fault. A shell exiting on its own raised SessionEnded from inside its run's finally block — where the run task is by definition not yet complete, so the LiveSessionCount the keep-alive reads still counted the dead shell, and nothing fired later to correct it. A tab closed by hand announced nothing at all, by a recorded decision that assumed every subscriber was the closer; the keep-alive is not, and a close it never heard about left the notification claiming a shell over nothing. The end is now announced from a continuation after the run completes, and CloseSessionAsync announces after its own drain — every subscriber was already a reconcile-to-reality handler, so the echo the old remark feared costs nothing. Shutdown stays silent: it is dismantling the subscribers along with the sessions.
This commit is contained in:
@@ -217,12 +217,15 @@ public sealed class TerminalWorkspaceTests
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The event the tab strip listens to, so a dot can go out the moment a shell exits rather than at the
|
||||
/// next thing that happens to repaint. Raised only when the session ended on its own: a tab the user
|
||||
/// closed has a caller who already knows, and telling it would turn one close into two.
|
||||
/// The event the tab strip and the phone's keep-alive listen to, so a dot can go out — and a foreground
|
||||
/// notification can come down — the moment a shell exits rather than at the next thing that happens to
|
||||
/// repaint. The count captured inside the handler is the sharper half of this test: the announcement
|
||||
/// used to fire from inside the run's own finally block, where the run task is not yet complete, so
|
||||
/// <c>LiveSessionCount</c> read from the handler still said 1 — and the phone's notification went on
|
||||
/// claiming a shell that was gone, with nothing left to fire and correct it.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ASessionEndingOnItsOwnIsAnnounced()
|
||||
public async Task ASessionEndingOnItsOwnIsAnnounced_AfterTheCountStoppedIncludingIt()
|
||||
{
|
||||
// A shell with no output to give: its first read returns 0, which is a remote closing the channel,
|
||||
// so the pump finishes with nobody asking it to.
|
||||
@@ -231,10 +234,12 @@ public sealed class TerminalWorkspaceTests
|
||||
await using var workspace = CreateWorkspace(connections);
|
||||
|
||||
var ended = new List<uint>();
|
||||
var liveAtAnnouncement = -1;
|
||||
workspace.SessionEnded += (_, e) =>
|
||||
{
|
||||
lock (ended)
|
||||
{
|
||||
liveAtAnnouncement = workspace.LiveSessionCount;
|
||||
ended.Add(e.SessionId);
|
||||
}
|
||||
};
|
||||
@@ -249,18 +254,33 @@ public sealed class TerminalWorkspaceTests
|
||||
return ended.Contains(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
lock (ended)
|
||||
{
|
||||
liveAtAnnouncement.ShouldBe(0, "the announcement must wait for the run to actually complete");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ASessionEndingOnItsOwnIsAnnounced" />
|
||||
/// <remarks>
|
||||
/// The reversal of a recorded decision, and the event's own remark carries why: a close used to be
|
||||
/// announced to nobody, on the theory that the caller already knew — but the phone's keep-alive is not
|
||||
/// the caller, and a close it never heard about left the foreground notification claiming a shell that
|
||||
/// was gone. Announced once, after the drain, so the count a handler reads is already honest.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ClosingASessionIsNotAnnouncedBack()
|
||||
public async Task ClosingASessionIsAnnounced_OnceItHasDrained()
|
||||
{
|
||||
var connections = new FakeConnectionFactory();
|
||||
|
||||
await using var workspace = CreateWorkspace(connections);
|
||||
|
||||
var announcements = 0;
|
||||
workspace.SessionEnded += (_, _) => Interlocked.Increment(ref announcements);
|
||||
var liveAtAnnouncement = -1;
|
||||
workspace.SessionEnded += (_, _) =>
|
||||
{
|
||||
liveAtAnnouncement = workspace.LiveSessionCount;
|
||||
Interlocked.Increment(ref announcements);
|
||||
};
|
||||
|
||||
var sessionId = await workspace.OpenSessionAsync(
|
||||
Request(), TerminalSize.Default, TestContext.Current.CancellationToken);
|
||||
@@ -268,12 +288,15 @@ public sealed class TerminalWorkspaceTests
|
||||
await workspace.CloseSessionAsync(sessionId);
|
||||
|
||||
Volatile.Read(ref announcements)
|
||||
.ShouldBe(0, "a close the caller asked for is not news to report back to it");
|
||||
.ShouldBe(1, "a close is news to the keep-alive even though it is an echo to the closer");
|
||||
liveAtAnnouncement.ShouldBe(0, "announced after the drain, so the count already excludes it");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Disposal is the other path that closes sessions, because it is process shutdown. Asserted so
|
||||
/// that the SSH connections are known to be released rather than assumed to be.
|
||||
/// that the SSH connections are known to be released rather than assumed to be — and that these closes,
|
||||
/// unlike a deliberate one, are announced to nobody: shutdown is dismantling every subscriber along
|
||||
/// with the sessions, and news nobody is left to hear is not news.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task DisposingTheWorkspaceClosesEverySession()
|
||||
@@ -282,6 +305,9 @@ public sealed class TerminalWorkspaceTests
|
||||
|
||||
var workspace = CreateWorkspace(connections);
|
||||
|
||||
var announcements = 0;
|
||||
workspace.SessionEnded += (_, _) => Interlocked.Increment(ref announcements);
|
||||
|
||||
await workspace.OpenSessionAsync(
|
||||
Request(), TerminalSize.Default, TestContext.Current.CancellationToken);
|
||||
await workspace.OpenSessionAsync(
|
||||
@@ -292,6 +318,7 @@ public sealed class TerminalWorkspaceTests
|
||||
workspace.LiveSessionCount.ShouldBe(0);
|
||||
connections.Connections.Count.ShouldBe(2);
|
||||
connections.Connections.ShouldAllBe(connection => connection.IsDisposed);
|
||||
Volatile.Read(ref announcements).ShouldBe(0, "shutdown closes are not announced");
|
||||
}
|
||||
|
||||
// ---- Reattach ----
|
||||
|
||||
Reference in New Issue
Block a user