Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions app/e2e_drive_receipt_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"bytes"
"context"
"encoding/json"
"os"
"strings"
Expand Down Expand Up @@ -105,3 +107,84 @@ func TestE2EDriveSharedNativeGoldens(t *testing.T) {
t.Error("the reboot report must bind only to the reboot directive")
}
}

// The native shell drives through its authenticated configuration session
// (shell/Wootc.Shell.Core/E2EDriveLoop.cs). Only that session carries the
// drive channel, and only while this engine's own environment enables it.
func TestE2EDriveNativeConfigurationSessionCarriesTheDriveChannel(t *testing.T) {
read := func(name string) string {
raw, err := os.ReadFile("../tests/unit/fixtures/e2e-drive/" + name)
if err != nil {
t.Fatal(err)
}
return string(raw)
}
param := func(value string) json.RawMessage {
encoded, err := json.Marshal([]string{value})
if err != nil {
t.Fatal(err)
}
return encoded
}
directive, report := read("directive-install.json"), read("report-install-ready.json")
ready := `{"schemaVersion":1,"screen":"loading"}`
writer := &synchronizedWriter{out: &bytes.Buffer{}}
configurationReads := 0
native := newConfigurationAssessmentServer(&App{}, writer, func(context.Context) (NativeConfigurationSnapshot, error) {
configurationReads++
return NativeConfigurationSnapshot{}, nil
})

t.Setenv("WOOTC_E2E_DRIVE", "1")
removeE2EFiles(t)
defer removeE2EFiles(t)
if err := os.WriteFile(e2eDrivePath("e2e-drive.json"), []byte(directive), 0o600); err != nil {
t.Fatal(err)
}
for _, other := range []*Server{NewAssessmentServer(&App{}, writer)} {
for _, method := range []string{"E2EDriveDirective", "E2EDriveReport", "E2EDriveReady"} {
if _, failure := other.dispatch(context.Background(), jsonrpcRequest{Method: method, Params: param(report)}); failure == nil || failure.Code != errCodeMethodNotFound {
t.Fatalf("%s reached a session without the configuration capability", method)
}
}
}
if _, err := os.Stat(e2eDrivePath("e2e-drive-state.json")); !os.IsNotExist(err) {
t.Fatal("a refused session wrote a drive report")
}
got, failure := native.dispatch(context.Background(), jsonrpcRequest{Method: "E2EDriveDirective"})
if failure != nil || got != directive {
t.Fatalf("native session did not return the directive: %v %v", got, failure)
}
if _, failure = native.dispatch(context.Background(), jsonrpcRequest{Method: "E2EDriveReport", Params: param(report)}); failure != nil {
t.Fatal(failure)
}
if state, err := os.ReadFile(e2eDrivePath("e2e-drive-state.json")); err != nil || string(state) != report {
t.Fatalf("native session did not write the bound report: %v", err)
}
if _, failure = native.dispatch(context.Background(), jsonrpcRequest{Method: "E2EDriveReady", Params: param(ready)}); failure != nil {
t.Fatal(failure)
}
if marker, err := os.ReadFile(e2eDrivePath("e2e-ready.json")); err != nil || string(marker) != ready {
t.Fatalf("native session did not write the readiness marker: %v", err)
}

// Without drive mode in the engine's environment the channel is inert.
t.Setenv("WOOTC_E2E_DRIVE", "")
removeE2EFiles(t)
if err := os.WriteFile(e2eDrivePath("e2e-drive.json"), []byte(directive), 0o600); err != nil {
t.Fatal(err)
}
if got, failure = native.dispatch(context.Background(), jsonrpcRequest{Method: "E2EDriveDirective"}); failure != nil || got != "" {
t.Fatal("drive directive returned while drive mode is off")
}
_, _ = native.dispatch(context.Background(), jsonrpcRequest{Method: "E2EDriveReport", Params: param(report)})
_, _ = native.dispatch(context.Background(), jsonrpcRequest{Method: "E2EDriveReady", Params: param(ready)})
for _, name := range []string{"e2e-drive-state.json", "e2e-ready.json"} {
if _, err := os.Stat(e2eDrivePath(name)); !os.IsNotExist(err) {
t.Fatalf("%s written while drive mode is off", name)
}
}
if configurationReads != 0 {
t.Fatal("the drive channel read the configuration")
}
}
10 changes: 10 additions & 0 deletions app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ var ProtocolMethods = []string{
"GetInstallRecovery",
"E2EDriveDirective",
"E2EDriveReport",
"E2EDriveReady",
"GetVMCapability",
"GetFreshVMCapability",
"GetVMState",
Expand Down Expand Up @@ -286,6 +287,15 @@ func (s *Server) dispatch(ctx context.Context, req jsonrpcRequest) (any, *jsonrp
if s.configurationRead == nil {
return nil, &jsonrpcError{Code: errCodeMethodNotFound, Message: "method unavailable in assessment session"}
}
case "E2EDriveDirective", "E2EDriveReport", "E2EDriveReady":
// The native shell's drive mode reads the harness directive and
// writes its reports through its authenticated configuration
// session, as the Wails frontend does through its bridge. All
// three stay inert unless WOOTC_E2E_DRIVE=1 is set in this
// engine's own environment; the shell cannot turn them on.
if s.configurationRead == nil {
return nil, &jsonrpcError{Code: errCodeMethodNotFound, Message: "method unavailable in assessment session"}
}
default:
return nil, &jsonrpcError{Code: errCodeMethodNotFound, Message: "method unavailable in assessment session"}
}
Expand Down
7 changes: 4 additions & 3 deletions docs/winui-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This revision corrects the transport and identity assumptions in the earlier des
|---|---|---|
| A: engine protocol | Go stdio RPC and DTO generator exist; legacy disconnect and console controls pass | Native operation authorization and the full consumer journey remain open |
| B: scaffold (#343) | Native projects, authenticated assessment RPC, startup UI tests, and five branded preview packages pass hosted component checks. Each installed package shows its palette and logo and refuses Windows builds before 17763. Windows policy refuses elevation for a standard user, and the shell keeps the branded assessment and the retry ([run 38776425860](https://github.com/tuna-os/wootc/actions/runs/38776425860)) | Interactive UAC consent, original-user collection for an alternative administrator (#606), proof on build 17763, the native typeface, and preview release assets (#608) |
| C: experience (#344) | A read-only startup view, a configuration form, a route for an interrupted install, an install route that needs an engine grant and a confirmation, progress and done views fed by that install session, manage and recovery views whose uninstall and boot repair need their own engine grant in an install session, Linux window views whose VM actions each need an engine grant, UI Automation controls, and a screen gallery exist | Consumer screens, VM and migration flows, drive mode, accessibility, persistence, and the full journey |
| C: experience (#344) | A read-only startup view, a configuration form, a route for an interrupted install, an install route that needs an engine grant and a confirmation, progress and done views fed by that install session, manage and recovery views whose uninstall and boot repair need their own engine grant in an install session, Linux window views whose VM actions each need an engine grant, UI Automation controls, a screen gallery, and an E2E drive mode (`WOOTC_E2E_DRIVE=1`) that operates these views exist | Consumer screens, VM and migration flows, a harness launch of the native shell and a drive-mode run in a VM, accessibility, persistence, and the full journey |
| D: release (#345) | Not started | Native default only after C; retain a legacy artifact for one release |
| E: removal (#346) | Not started | Remove Wails/web frontend only after a clean native release and docs audit |

Expand Down Expand Up @@ -265,8 +265,9 @@ Retain the legacy build until a native release passes its gates, before phase E
| `GetLastRun` | — | `LifecycleState` |
| `GetRecoveryVerdict` | — | `RecoveryVerdict` |
| `GetInstallRecovery` | — | `InstallRecoverySummary` (class, safe actions and engine text; no evidence or step error) |
| `E2EDriveDirective` | — | `string` |
| `E2EDriveReport` | `string` | `null` |
| `E2EDriveDirective` | — | `string` (empty unless the engine runs with `WOOTC_E2E_DRIVE=1`; native shell: configuration session only) |
| `E2EDriveReport` | `string` | `null` (written only when bound to the current directive; native shell: configuration session only) |
| `E2EDriveReady` | `string` | `null` (first-render marker, bound to no directive; native shell: configuration session only) |
| `GetVMCapability` | — | `VMCapability` | Signed runtime and persistent disk availability. |
| `GetFreshVMCapability` | — | `VMCapability` | Signed runtime and helper availability. |
| `GetVMState` | — | `VMState` | Durable disk lifecycle; desktop readiness requires independent evidence. |
Expand Down
4 changes: 3 additions & 1 deletion shell/Wootc.Shell.Core/ConsumerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ public DriveMappingObservation ApplyDriveMapping(InstallDriveMapping directive,
PasswordConfirmation = directive.Password;
Hostname = directive.Hostname;
DiskSizeGb = directive.DiskSizeGb == 0 ? catalogue!.Defaults.DiskSizeGb : directive.DiskSizeGb;
Encryption = directive.Encryption.Length == 0 ? catalogue!.Defaults.Encryption : directive.Encryption;
// As the Wails drive loop does (directive.encryption || 'none'): a
// directive that names no encryption installs unencrypted.
Encryption = directive.Encryption.Length == 0 ? "none" : directive.Encryption;
LuksPassphrase = directive.LuksPassphrase;
// Mapping a directive is not an Install click or a backend mutation.
return new(directive.RunId, directive.DirectiveId, true, false, false, InstallationBlockedReason);
Expand Down
130 changes: 130 additions & 0 deletions shell/Wootc.Shell.Core/E2EDriveLoop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.Text.Json;

namespace Wootc.Shell.Core;

// The native window's drive loop, the counterpart of startE2EDrive in
// app/frontend/src/lib/e2e.js. Each tick reads the harness directive through
// the engine, performs at most one command through the view, and reports the
// view's state back through the engine. The engine binds every report to the
// directive it returned, so a report for another run is never written.
//
// Drive mode is diagnostic: no failure here may break the window. A failed
// read is retried on the next tick; a malformed, changed or second directive
// is never followed.

// The engine side of the loop: E2EDriveDirective, E2EDriveReport and
// E2EDriveReady over the authenticated configuration session.
public interface IE2EDriveChannel
{
Task<string> ReadDirectiveAsync(CancellationToken cancellationToken);
Task ReportAsync(string state, CancellationToken cancellationToken);
Task ReadyAsync(string state, CancellationToken cancellationToken);
}

// The window side: what is on screen, and the controls a person would use.
public interface IE2EDriveView
{
DriveObservation Observe();
// Fill the form from the directive through the configuration view-model.
DriveMappingObservation MapInstall(E2EDriveDirective directive);
// The install controls a person uses. True only when the engine accepted
// the confirmation and the install started.
Task<bool> ClickInstallAsync();
Task RebootAsync();
}

public sealed class E2EDriveLoop
{
public const int PollMilliseconds = 2000;
private readonly IE2EDriveChannel channel;
private readonly IE2EDriveView view;
public E2EDriveSession Session { get; } = new();

public E2EDriveLoop(IE2EDriveChannel channel, IE2EDriveView view)
{
this.channel = channel ?? throw new ArgumentNullException(nameof(channel));
this.view = view ?? throw new ArgumentNullException(nameof(view));
}

// The readiness marker carries only the schema and the screen, as the
// Wails loop's does; it binds to no directive.
public static string ReadyState(DriveScreen screen)
{
using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer))
{
writer.WriteStartObject();
writer.WriteNumber("schemaVersion", 1);
writer.WriteString("screen", screen.ToString().ToLowerInvariant());
writer.WriteEndObject();
}
return System.Text.Encoding.UTF8.GetString(buffer.ToArray());
}

public async Task TickAsync(CancellationToken cancellationToken = default)
{
string raw = "";
try { raw = await channel.ReadDirectiveAsync(cancellationToken) ?? ""; }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch { /* A transient read failure: signal readiness and retry. */ }
if (raw.StartsWith('')) raw = raw[1..];
if (raw.Length == 0)
{
// No directive yet. Readiness is observable before one exists.
try { await channel.ReadyAsync(ReadyState(view.Observe().Screen), cancellationToken); }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch { /* Best effort; the next tick retries. */ }
return;
}
E2EDriveDirective directive;
DriveCommand command;
try
{
directive = E2EDriveDirective.Parse(raw);
command = Session.Next(directive, view.Observe());
}
catch (Exception error) when (error is InvalidDataException or JsonException)
{
// A malformed directive, another run, or a second install
// directive: never followed, never reported against.
return;
}
switch (command)
{
case DriveCommand.MapInstall:
try
{
var outcome = view.MapInstall(directive);
Session.MarkMapped(directive);
// A refused target is reported, not swallowed. An image
// mismatch is reported through imageMismatch instead.
if (!outcome.Mapped && !outcome.ImageMismatch)
Session.MarkRefused(outcome.BlockedReason.Length != 0 ? outcome.BlockedReason : "The drive directive could not be mapped.");
}
catch (Exception error) when (error is not OperationCanceledException)
{
Session.MarkMapped(directive);
Session.MarkRefused("The drive directive could not be mapped: " + error.Message);
}
break;
case DriveCommand.ClickInstall:
// Before the click: a click whose handler partly ran is never replayed.
Session.MarkInstallRequested();
bool started;
try { started = await view.ClickInstallAsync(); }
catch (Exception error) when (error is not OperationCanceledException) { started = false; }
if (started) Session.MarkInstallDriven();
break;
case DriveCommand.Reboot:
Session.MarkRebootDriven();
try { await view.RebootAsync(); }
catch (Exception error) when (error is not OperationCanceledException) { /* The report shows the done screen still up. */ }
break;
}
string? report = Session.Report(directive, view.Observe());
if (report is null) return;
try { await channel.ReportAsync(report, cancellationToken); }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch { /* The next tick reports again. */ }
}
}
35 changes: 34 additions & 1 deletion shell/Wootc.Shell.Core/StartupController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@ public interface IConfigurationEngineSession : IEngineSession
Task<NativeConfigurationSnapshot> ReadConfigurationAsync(CancellationToken cancellationToken, Action<long>? requestFlushed = null);
}

public sealed class StartupController : IAsyncDisposable
// The E2E drive channel of an assessment session. The engine answers only
// when its own environment enables drive mode; otherwise the directive is
// empty and reports are dropped.
public interface IDriveEngineSession : IEngineSession
{
Task<string> ReadDriveDirectiveAsync(CancellationToken cancellationToken);
Task ReportDriveAsync(string state, CancellationToken cancellationToken);
Task ReportDriveReadyAsync(string state, CancellationToken cancellationToken);
}

public sealed class StartupController : IAsyncDisposable, IE2EDriveChannel
{
private readonly IEngineConnector connector;
private readonly SemaphoreSlim connectionGate = new(1, 1);
Expand Down Expand Up @@ -149,6 +159,29 @@ public async Task<NativeConfigurationSnapshot> ReadConfigurationAsync(Cancellati
finally { connectionGate.Release(); }
}

// The drive loop never waits behind a connection or configuration read:
// a busy controller fails this tick, and the loop tries again.
private async Task<T> DriveAsync<T>(Func<IDriveEngineSession, Task<T>> call)
{
if (!await connectionGate.WaitAsync(0)) throw new InvalidOperationException("The engine connection is busy");
try
{
if (Connection != ConnectionState.Ready || session is not IDriveEngineSession drive)
throw new InvalidOperationException("Authenticated drive channel is unavailable");
return await call(drive);
}
finally { connectionGate.Release(); }
}

public Task<string> ReadDirectiveAsync(CancellationToken cancellationToken) =>
DriveAsync(drive => drive.ReadDriveDirectiveAsync(cancellationToken));

public Task ReportAsync(string state, CancellationToken cancellationToken) =>
DriveAsync(async drive => { await drive.ReportDriveAsync(state, cancellationToken); return true; });

public Task ReadyAsync(string state, CancellationToken cancellationToken) =>
DriveAsync(async drive => { await drive.ReportDriveReadyAsync(state, cancellationToken); return true; });

private async Task DisconnectAsync()
{
if (session is not null)
Expand Down
33 changes: 32 additions & 1 deletion shell/Wootc.Shell.Native/NativeEngineSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Wootc.Shell.Native;

internal sealed class NativeEngineSession : IConfigurationEngineSession, IInstallEngineSession, IMaintenanceEngineSession, IVMEngineSession
internal sealed class NativeEngineSession : IConfigurationEngineSession, IInstallEngineSession, IMaintenanceEngineSession, IVMEngineSession, IDriveEngineSession
{
// Set only for an install-purpose session; an assessment session refuses
// install calls locally as well as in the engine.
Expand Down Expand Up @@ -292,6 +292,37 @@ public async Task<NativeConfigurationSnapshot> ReadConfigurationAsync(Cancellati
return await CallAsync<NativeConfigurationSnapshot>("GetNativeConfiguration", deadline.Token, requestFlushed);
}

// The drive channel belongs to the assessment session only; the engine
// refuses it in an install session as well.
private void RequireAssessmentSession()
{
if (installSession is not null) throw new InvalidOperationException("The drive channel is not part of an install session");
}

public async Task<string> ReadDriveDirectiveAsync(CancellationToken cancellationToken)
{
RequireAssessmentSession();
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
deadline.CancelAfter(TimeSpan.FromSeconds(10));
return await CallAsync<string>("E2EDriveDirective", deadline.Token);
}

public async Task ReportDriveAsync(string state, CancellationToken cancellationToken)
{
RequireAssessmentSession();
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
deadline.CancelAfter(TimeSpan.FromSeconds(10));
await CallAsync<object?>("E2EDriveReport", new[] { state ?? "" }, deadline.Token);
}

public async Task ReportDriveReadyAsync(string state, CancellationToken cancellationToken)
{
RequireAssessmentSession();
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
deadline.CancelAfter(TimeSpan.FromSeconds(10));
await CallAsync<object?>("E2EDriveReady", new[] { state ?? "" }, deadline.Token);
}

public async ValueTask DisposeAsync()
{
if (!disconnected)
Expand Down
Loading
Loading