Public Access
The Android head skipped a host's forwards because it had nowhere to show what was listening. Its terminal bar now draws a ⇄ for a tab that has forwards, carrying the listening count in green and turning the danger colour when one failed, and it raises a sheet over the same rows the desktop sidebar lists: notation, route, state, failure reason and a tap to switch each off or on. MainWindowViewModel.StartForwards no longer returns early on Android. The sheet is shell state like the connect sheet: it collapses the renderer while up, is lowered by leaving the terminal, and the back gesture closes it first. Forwards ride the shell's keep-alive rather than holding the foreground service themselves, since a forward cannot outlive its shell. They only change the notification, which now counts ports forwarded beside shells connected, fed by a new ForwardsChanged event and ListeningForwardCount. The phone still has no editor for forwards; the More screen card, README and design-import-gaps now say they run here and are edited on the desktop.
98 lines
5.2 KiB
C#
98 lines
5.2 KiB
C#
using DodoSSH.Client.Terminal;
|
|
|
|
namespace DodoSSH.Client.Android.Platform;
|
|
|
|
/// <summary>
|
|
/// Keeps <see cref="SessionForegroundService"/> in step with what is actually running.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The service is started and stopped from one place, and that place is a count rather than a lifecycle.
|
|
/// Anything else drifts: a service started when a shell opens and stopped when a tab closes would leave
|
|
/// the notification up after the last shell died on its own, and a phone showing "1 shell connected" over
|
|
/// nothing is the same dishonesty the unlock screen's shell count exists to avoid.
|
|
/// </para>
|
|
/// <para>
|
|
/// <see cref="TerminalWorkspace.LiveSessionCount"/> is deliberately the source of truth rather than a
|
|
/// tally kept here. It already knows that a session whose shell exited half an hour ago is not live, which
|
|
/// a counter incremented on open and decremented on close would not.
|
|
/// </para>
|
|
/// <para>
|
|
/// Three facts feed <see cref="SessionForegroundService.Reconcile"/>, not one: live shells, moving
|
|
/// transfers, and an idle-but-connected Files session. The last of those used to be missing entirely —
|
|
/// a shell survives backgrounding because somebody opened it, but a Files connection with nothing moving
|
|
/// looked, to this class, exactly like nothing being open at all. <c>holdsFileSession</c> below is that
|
|
/// gap closed, read the same way the other two facts are: asked, not cached.
|
|
/// </para>
|
|
/// <para>
|
|
/// Listening port forwards are a fourth fact, and a different kind: they only change what the notification
|
|
/// says. A forward runs over a shell's connection and stops with it, so there is never a listening forward
|
|
/// without a live shell already holding the service up — and a count that could start the service on its own
|
|
/// would only ever be wrong, in the moment a forward's Stopped had not been heard yet. What they add is the
|
|
/// receipt: the user is entitled to know the phone is holding ports open, as well as shells.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal sealed class SessionKeepAlive : IDisposable
|
|
{
|
|
private readonly TerminalWorkspace workspace;
|
|
private readonly Func<int> activeTransfers;
|
|
private readonly Func<bool> holdsFileSession;
|
|
private readonly Func<int> listeningForwards;
|
|
|
|
/// <param name="workspace">The live shells.</param>
|
|
/// <param name="activeTransfers">
|
|
/// How many transfers are moving bytes. A delegate rather than a queue, because ownership of the
|
|
/// transfer queue stays with <c>TransfersViewModel</c> — this class only ever asks it a question.
|
|
/// </param>
|
|
/// <param name="holdsFileSession">
|
|
/// Whether the Files screen holds a live SFTP connection with nothing moving on it — the idle-but-
|
|
/// connected case a transfer count alone would miss. See <c>TransfersViewModel.HasLiveFileSession</c>.
|
|
/// </param>
|
|
/// <param name="listeningForwards">
|
|
/// How many port forwards are listening, across every tab. For the notification's wording only; see the
|
|
/// class remarks. See <c>MainWindowViewModel.ListeningForwardCount</c>.
|
|
/// </param>
|
|
public SessionKeepAlive(
|
|
TerminalWorkspace workspace,
|
|
Func<int> activeTransfers,
|
|
Func<bool> holdsFileSession,
|
|
Func<int> listeningForwards)
|
|
{
|
|
this.workspace = workspace;
|
|
this.activeTransfers = activeTransfers;
|
|
this.holdsFileSession = holdsFileSession;
|
|
this.listeningForwards = listeningForwards;
|
|
|
|
// Raised on whatever thread the workspace announced from — a continuation of the ended run, or the
|
|
// closer's own — which is fine: starting and stopping a service is a binder call and needs no
|
|
// particular thread. Nothing here touches the interface. That the announcement waits for the run to
|
|
// actually complete, and comes for deliberate closes too, is what makes reading LiveSessionCount
|
|
// from it honest — the event's own remark carries the stuck notification that taught us both.
|
|
workspace.SessionEnded += OnSessionEnded;
|
|
}
|
|
|
|
/// <summary>Re-reads the counts and starts or stops the service to match.</summary>
|
|
/// <remarks>
|
|
/// Called after anything that could change any of the facts — opening a shell, closing a tab, a transfer
|
|
/// finishing, a Files connection opening or closing, a forward starting or stopping. Calling it when nothing changed is free:
|
|
/// reconciling to the state it is already in is either a redundant <c>startForegroundService</c> — or,
|
|
/// now, a redundant notification post — on a running service, or a <c>stopService</c> on a stopped one,
|
|
/// and Android treats all of those as no-ops.
|
|
/// </remarks>
|
|
public void Refresh() =>
|
|
SessionForegroundService.Reconcile(
|
|
workspace.LiveSessionCount, activeTransfers(), holdsFileSession(), listeningForwards());
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
workspace.SessionEnded -= OnSessionEnded;
|
|
|
|
// The notification goes with the composition root. Leaving it up over a process that is shutting
|
|
// down is how an SSH client acquires a reputation for a notification you cannot get rid of.
|
|
SessionForegroundService.Reconcile(0, 0, false, 0);
|
|
}
|
|
|
|
private void OnSessionEnded(object? sender, TerminalSessionEndedEventArgs e) => Refresh();
|
|
}
|