241 lines
8.1 KiB
C#
241 lines
8.1 KiB
C#
using System.IO.Pipes;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace Covenant.Setup.Ui;
|
|
|
|
internal sealed class InstallerSessionController(string pipeName, IInstallerView view)
|
|
{
|
|
private readonly object _writerLock = new();
|
|
private TextWriter? _writer;
|
|
private NamedPipeServerStream? _pipe;
|
|
|
|
internal TextWriter? ResponseWriter
|
|
{
|
|
get
|
|
{
|
|
lock (_writerLock)
|
|
{
|
|
return _writer;
|
|
}
|
|
}
|
|
set
|
|
{
|
|
lock (_writerLock)
|
|
{
|
|
_writer = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asks the engine to stop the current operation and roll back. The pipe
|
|
/// stays open so rollback progress and the terminal message still arrive.
|
|
/// </summary>
|
|
public bool RequestCancel()
|
|
{
|
|
if (!WriteResponse(new UiResponse { Type = "cancel_request" }))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UiTrace.Write("cancel_request_sent");
|
|
return true;
|
|
}
|
|
|
|
public void Run()
|
|
{
|
|
try
|
|
{
|
|
UiTrace.Write("pipe_server_create", new { PipeName = pipeName });
|
|
// Asynchronous (overlapped) mode is required: with a synchronous
|
|
// handle Windows serializes I/O, so RequestCancel's write from the
|
|
// UI thread would block behind the message loop's pending ReadLine.
|
|
using (_pipe = new NamedPipeServerStream(
|
|
pipeName,
|
|
PipeDirection.InOut,
|
|
1,
|
|
PipeTransmissionMode.Byte,
|
|
PipeOptions.Asynchronous))
|
|
{
|
|
UiTrace.Write("pipe_wait_for_connection", new { PipeName = pipeName });
|
|
_pipe.WaitForConnection();
|
|
UiTrace.Write("pipe_connected", new { PipeName = pipeName });
|
|
|
|
using var reader = new StreamReader(_pipe, new UTF8Encoding(false), detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true);
|
|
using var writer = new StreamWriter(_pipe, new UTF8Encoding(false), bufferSize: 4096, leaveOpen: true)
|
|
{
|
|
AutoFlush = true,
|
|
NewLine = "\n"
|
|
};
|
|
|
|
ResponseWriter = writer;
|
|
|
|
string? line;
|
|
while ((line = reader.ReadLine()) is not null)
|
|
{
|
|
UiTrace.Write("pipe_receive", SafeMessageSummary(line));
|
|
if (!HandleMessage(line))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UiTrace.Write("pipe_error", new { ex.Message, ex.GetType().FullName, ex.StackTrace });
|
|
view.ShowPipeError(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
ResponseWriter = null;
|
|
UiTrace.Write("pipe_loop_exit");
|
|
}
|
|
}
|
|
|
|
internal bool HandleMessage(string line)
|
|
{
|
|
var message = JsonSerializer.Deserialize<UiMessage>(line, UiJson.Options);
|
|
if (message?.Type is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
switch (message.Type)
|
|
{
|
|
case "init":
|
|
var title = message.Title ?? "covenant-setup";
|
|
if (!string.IsNullOrEmpty(message.AppName) && !string.IsNullOrEmpty(message.InstallDir) && message.Automation != true)
|
|
{
|
|
UiTrace.Write("welcome_show_requested", new { message.AppName, message.InstallDir });
|
|
var welcomeResult = view.ShowWelcomeAsync(message.AppName, message.InstallDir, message.BrandingImage).GetAwaiter().GetResult();
|
|
UiTrace.Write("welcome_response", new { Result = welcomeResult });
|
|
|
|
WriteResponse(new UiResponse
|
|
{
|
|
Type = "welcome_response",
|
|
Result = welcomeResult
|
|
});
|
|
|
|
if (welcomeResult == "cancel")
|
|
{
|
|
view.CloseView();
|
|
return false;
|
|
}
|
|
}
|
|
view.ShowInit(title, message.Message ?? title);
|
|
return true;
|
|
|
|
case "progress":
|
|
if (!string.IsNullOrWhiteSpace(message.Message))
|
|
{
|
|
view.AppendLog(message.Message);
|
|
}
|
|
view.ShowProgress(ComputePercent(message.CurrentStep, message.TotalSteps), message.Message, message.CurrentStep ?? 0);
|
|
return true;
|
|
|
|
case "log":
|
|
view.AppendLog(message.Message ?? string.Empty);
|
|
return true;
|
|
|
|
case "finish":
|
|
UiTrace.Write("finish_message", new { message.Message });
|
|
view.ShowFinished(message.Message ?? "Complete");
|
|
return true;
|
|
|
|
case "fail":
|
|
UiTrace.Write("fail_message", new { message.AppName, message.Operation, message.Message, message.Error });
|
|
view.ShowFailure(BuildFailureMessage(message), message.Error, BuildErrataJson(message));
|
|
return true;
|
|
|
|
case "prompt":
|
|
UiTrace.Write("prompt_show_requested", new { message.Id, message.Title, message.Buttons, message.Icon });
|
|
var result = view.ShowPromptAsync(message).GetAwaiter().GetResult();
|
|
UiTrace.Write("prompt_response", new { message.Id, Result = result });
|
|
WriteResponse(new UiResponse
|
|
{
|
|
Type = "prompt_response",
|
|
Id = message.Id,
|
|
Result = result
|
|
});
|
|
return true;
|
|
|
|
case "close":
|
|
UiTrace.Write("close_message");
|
|
view.CloseView();
|
|
return false;
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
internal static int ComputePercent(int? currentStep, int? totalSteps)
|
|
{
|
|
var total = Math.Max(1, totalSteps ?? 1);
|
|
var current = Math.Max(0, Math.Min(total, currentStep ?? 0));
|
|
return Math.Max(0, Math.Min(100, current * 100 / total));
|
|
}
|
|
|
|
internal static string BuildFailureMessage(UiMessage message)
|
|
{
|
|
var operation = string.IsNullOrWhiteSpace(message.Operation) ? "complete" : message.Operation;
|
|
var appName = string.IsNullOrWhiteSpace(message.AppName) ? "unknown" : message.AppName;
|
|
return string.IsNullOrWhiteSpace(message.Message)
|
|
? $"Error: program {appName} failed to {operation} completely!"
|
|
: message.Message;
|
|
}
|
|
|
|
internal static string BuildErrataJson(UiMessage message)
|
|
{
|
|
if (message.Errata is JsonElement errata &&
|
|
errata.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null)
|
|
{
|
|
return JsonSerializer.Serialize(errata, UiJson.Indented);
|
|
}
|
|
|
|
return JsonSerializer.Serialize(new
|
|
{
|
|
app_name = message.AppName,
|
|
operation = message.Operation,
|
|
message = message.Message,
|
|
error = message.Error
|
|
}, UiJson.Indented);
|
|
}
|
|
|
|
internal static object SafeMessageSummary(string line)
|
|
{
|
|
try
|
|
{
|
|
using var document = JsonDocument.Parse(line);
|
|
var root = document.RootElement;
|
|
return new
|
|
{
|
|
Type = root.TryGetProperty("type", out var type) ? type.GetString() : null,
|
|
Id = root.TryGetProperty("id", out var id) ? id.GetString() : null,
|
|
Message = root.TryGetProperty("message", out var message) ? message.GetString() : null
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
return new { RawLength = line.Length };
|
|
}
|
|
}
|
|
|
|
private bool WriteResponse(UiResponse response)
|
|
{
|
|
lock (_writerLock)
|
|
{
|
|
if (_writer is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_writer.WriteLine(JsonSerializer.Serialize(response, UiJson.Options));
|
|
UiTrace.Write("pipe_send", new { response.Type, response.Id, response.Result });
|
|
return true;
|
|
}
|
|
}
|
|
}
|