Public Access
Give DodoSSH a phone, and a shared shell for both heads to drive
The Android head from docs/android-port.md, taken as far as its step 6. Step 3, the spike, is answered and its throwaway screen is gone: libsodium.so and libe_sqlite3.so are both in the arm64 APK, so NSec resolves its native half on Android despite shipping no Android build, and the local cache opens. Two findings the audit could not have had: Avalonia.Controls.WebView only ships net10.0-android36.0, which settles the open "which Android versions" question at targetSdk 36; and Android has blocked cleartext HTTP since API 28, so the terminal renderer needs a network security config scoped to 127.0.0.1 or the WebView loads nothing. DodoSSH.Client.Shell is new and is why the phone can exist: the view models, the terminal renderer files and the palette moved there so both heads drive one state machine and draw from one set of tokens. The desktop head is otherwise untouched and its 144 tests still pass. The platform pieces behind interfaces that already existed: the profile directory from filesDir, a device key wrapped by a StrongBox-backed key that a fingerprint releases, and a foreground service so a shell outliving a vault lock stays true on a platform that stops backgrounded processes. Sign-in is deliberately absent rather than approximated. It needs an app link, because reusing the desktop loopback listener is the attack RFC 8252 section 8.3 names.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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>
|
||||
/// </remarks>
|
||||
internal sealed class SessionKeepAlive : IDisposable
|
||||
{
|
||||
private readonly TerminalWorkspace workspace;
|
||||
private readonly Func<int> activeTransfers;
|
||||
|
||||
/// <param name="workspace">The live shells.</param>
|
||||
/// <param name="activeTransfers">
|
||||
/// How many transfers are moving bytes. A delegate rather than a queue, because file transfer is out
|
||||
/// of this head's first scope — see the decision in docs/android-port.md — and this is the seam it
|
||||
/// will arrive through rather than a dependency taken before there is anything to depend on.
|
||||
/// </param>
|
||||
public SessionKeepAlive(TerminalWorkspace workspace, Func<int> activeTransfers)
|
||||
{
|
||||
this.workspace = workspace;
|
||||
this.activeTransfers = activeTransfers;
|
||||
|
||||
// Raised on whatever thread the pump unwound on, which is fine: starting and stopping a service is
|
||||
// a binder call and needs no particular thread. Nothing here touches the interface.
|
||||
workspace.SessionEnded += OnSessionEnded;
|
||||
}
|
||||
|
||||
/// <summary>Re-reads the counts and starts or stops the service to match.</summary>
|
||||
/// <remarks>
|
||||
/// Called after anything that could change either count — opening a shell, closing a tab, a transfer
|
||||
/// finishing. Calling it when nothing changed is free: reconciling to the state it is already in is
|
||||
/// either a redundant <c>startForegroundService</c> on a running service or a <c>stopService</c> on a
|
||||
/// stopped one, and Android treats both as no-ops.
|
||||
/// </remarks>
|
||||
public void Refresh() =>
|
||||
SessionForegroundService.Reconcile(workspace.LiveSessionCount, activeTransfers());
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
private void OnSessionEnded(object? sender, TerminalSessionEndedEventArgs e) => Refresh();
|
||||
}
|
||||
Reference in New Issue
Block a user