Improved logic.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Renci.SshNet;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace aeqw89.tools.Publish;
|
||||
|
||||
sealed record DestinationContext(
|
||||
ProgressTask Task,
|
||||
ProgressContext ProgressContext,
|
||||
MemoryStream Reader,
|
||||
FileInfo PackageFile,
|
||||
string Name,
|
||||
long BufferSize,
|
||||
long PackageSize
|
||||
);
|
||||
|
||||
interface IDestination {
|
||||
Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
sealed record LocalDestination(DestinationContext Context) : IDestination {
|
||||
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
||||
// Path = "$USER/local-%ctx.name/%pkg.filename"
|
||||
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Context.Name, Context.PackageFile.Name);
|
||||
try {
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!); // create the directory
|
||||
await using var writer = File.OpenWrite(path);
|
||||
var buffer = new byte[Context.BufferSize];
|
||||
int read;
|
||||
do {
|
||||
read = await Context.Reader.ReadAsync(buffer, ct);
|
||||
writer.Write(buffer, 0, read);
|
||||
Context.Task.Increment(read);
|
||||
} while (read > 0);
|
||||
return Success.AsResult();
|
||||
} catch (Exception e) {
|
||||
return new ReadableError(string.Format(Exceptions.generic_error, e), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed record CloudDestiantion(DestinationContext Context) : IDestination {
|
||||
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
||||
var connectionTask = Context.ProgressContext.AddTaskBefore($"Preparing cloud-{Context.Name}",
|
||||
new ProgressTaskSettings() {
|
||||
MaxValue = 100
|
||||
}, Context.Task);
|
||||
|
||||
if (!SshHosts.TryGetHost(Context.Name, out var host)) {
|
||||
return new ReadableError(string.Format(Exceptions.cloud_host_not_found.EscapeMarkup(), Context.Name), false);
|
||||
}
|
||||
|
||||
var connectionInfo = SshHosts.GetConnection(Context.Name);
|
||||
using var sshClient = new SshClient(connectionInfo);
|
||||
|
||||
if (!sshClient.IsConnected)
|
||||
await sshClient.ConnectAsync(ct);
|
||||
|
||||
connectionTask.Increment(33); // ? One-Third of the way done (connection success).
|
||||
|
||||
var winC = sshClient.RunCommand("cmd /c ver");
|
||||
var othC = sshClient.RunCommand("uname -s");
|
||||
|
||||
var os = (winC.ExitStatus, othC.ExitStatus) switch {
|
||||
(0, _) => "windows",
|
||||
(_, 0) => "linux",
|
||||
_ => "unknown"
|
||||
};
|
||||
|
||||
string remoteDirectory;
|
||||
string packageFileDirectory;
|
||||
|
||||
if (os == "windows") {
|
||||
var userDirC = sshClient.RunCommand("cmd /c echo %USERPROFILE%");
|
||||
if (userDirC.ExitStatus != 0) { // * missing env variable case (could not work out where to place packages in remote).
|
||||
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
||||
userDirC.Result), false);
|
||||
}
|
||||
|
||||
var userDir = userDirC.Result.Trim();
|
||||
remoteDirectory = RemotePath.Combine(RemoteOs.Windows, userDir, "dotnet-packages");
|
||||
packageFileDirectory = RemotePath.Combine(RemoteOs.Windows, remoteDirectory, Context.PackageFile.Name);
|
||||
|
||||
var mkdirC = sshClient.RunCommand($"cmd /c if not exist \"{remoteDirectory}\" mkdir \"{remoteDirectory}\"");
|
||||
if (mkdirC.ExitStatus != 0) { // * MKDir error case (make directory command failed on remote).
|
||||
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, remoteDirectory, Context.PackageFile.Name, os,
|
||||
mkdirC.Result), false);
|
||||
}
|
||||
}
|
||||
else if (os == "linux") {
|
||||
var homeDirC = sshClient.RunCommand("printf %s \"$HOME\"");
|
||||
if (homeDirC.ExitStatus != 0) { // * missing env variable case 2 (could not work out where to place packages in remote).
|
||||
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
||||
homeDirC.Result), false);
|
||||
}
|
||||
|
||||
var homeDir = homeDirC.Result.Trim(); // no CRLF on unix, but Trim() is safest
|
||||
remoteDirectory = RemotePath.Combine(RemoteOs.Unix, homeDir, ".dotnet-packages");
|
||||
packageFileDirectory = RemotePath.Combine(RemoteOs.Unix, remoteDirectory,
|
||||
Context.PackageFile.Name);
|
||||
|
||||
// Use -p and single quotes to handle spaces safely
|
||||
var mkdirC = sshClient.RunCommand($"mkdir -p '{remoteDirectory}'");
|
||||
if (mkdirC.ExitStatus != 0) { // * MKDir error case (make directory command failed on remote).
|
||||
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, remoteDirectory, Context.PackageFile.Name,
|
||||
os, mkdirC.Result), false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
||||
"Unsupported OS"), false); // * MacOS failure path (or other unrecognizable oses).
|
||||
}
|
||||
|
||||
connectionTask.Increment(33); // ? Two-Thirds of the way done (directory located and ready).
|
||||
|
||||
sshClient.Disconnect();
|
||||
|
||||
using var client = new SftpClient(connectionInfo);
|
||||
if (!client.IsConnected)
|
||||
await client.ConnectAsync(ct);
|
||||
connectionTask.Increment(33); // ? Connection task complete (now proceeding to upload task).
|
||||
connectionTask.StopTask();
|
||||
|
||||
await using var writer = client.OpenWrite(packageFileDirectory);
|
||||
byte[] buffer = new byte[Context.BufferSize];
|
||||
int read;
|
||||
do {
|
||||
read = await Context.Reader.ReadAsync(buffer, ct);
|
||||
writer.Write(buffer, 0, read);
|
||||
Context.Task.Increment(read);
|
||||
} while (read > 0);
|
||||
|
||||
return Success.AsResult();
|
||||
}
|
||||
}
|
||||
|
||||
sealed record GithubDestination(DestinationContext Context, bool Verbose = false) : IDestination {
|
||||
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
||||
var p = Process.Start(new ProcessStartInfo() {
|
||||
FileName = "dotnet",
|
||||
Arguments = $"nuget push \"{Context.PackageFile.FullName}\" --source github",
|
||||
WorkingDirectory = Environment.CurrentDirectory,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
});
|
||||
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
StringBuilder errorLines = new();
|
||||
p?.ErrorDataReceived += (sender, eventArgs) => {
|
||||
cts.Cancel();
|
||||
if (Verbose && eventArgs.Data != null)
|
||||
AnsiConsole.WriteLine(eventArgs.Data);
|
||||
errorLines.Append(eventArgs.Data);
|
||||
};
|
||||
p?.OutputDataReceived += (sender, eventArgs) => {
|
||||
if (eventArgs.Data?.ToLower().Contains("press any key") == true)
|
||||
cts.Cancel();
|
||||
if (Verbose && eventArgs.Data != null)
|
||||
AnsiConsole.WriteLine(eventArgs.Data);
|
||||
};
|
||||
|
||||
p?.BeginOutputReadLine();
|
||||
p?.BeginErrorReadLine();
|
||||
|
||||
Context.Task.Increment(Context.PackageSize / 2);
|
||||
if (p != null)
|
||||
try {
|
||||
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
|
||||
}
|
||||
catch (TaskCanceledException) {
|
||||
p?.Kill();
|
||||
}
|
||||
|
||||
if (p?.ExitCode != 0) {
|
||||
Context.Task.StopTask();
|
||||
return new ReadableError(errorLines.ToString().EscapeMarkup() + "\n" + string.Format(Exceptions.dotnet_nuget_push_failure, p?.ExitCode ?? -1), false);
|
||||
}
|
||||
|
||||
Context.Task.Increment(Context.BufferSize / 2);
|
||||
return Success.AsResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace aeqw89.tools.Publish;
|
||||
|
||||
record Success {
|
||||
public static Ok<Success> AsResult() => new Ok<Success>(new Success());
|
||||
}
|
||||
record Ok<T>(T Value);
|
||||
record ReadableError(string Reason, bool ShowHelp = true, bool RestoreContext = true);
|
||||
union Result<T, E>(Ok<T>, E);
|
||||
|
||||
internal static class ResultExtensions {
|
||||
public static T Unwrap<T>(this Result<T, ReadableError> result, Program.RunContext? rctx = null) {
|
||||
switch (result) {
|
||||
case Ok<T> r:
|
||||
return r.Value;
|
||||
case ReadableError error:
|
||||
Program.ShowError(error.Reason);
|
||||
if (error.ShowHelp) Program.ShowHelp();
|
||||
if (error.RestoreContext) rctx?.Restore();
|
||||
Environment.Exit(1);
|
||||
throw new UnreachableException();
|
||||
}
|
||||
|
||||
throw new UnreachableException();
|
||||
}
|
||||
|
||||
public static async Task<T> Unwrap<T>(this Task<Result<T, ReadableError>> result, Program.RunContext? rctx = null) {
|
||||
return (await result).Unwrap(rctx);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ObjectExtensions {
|
||||
public static Ok<T> Ok<T>(this T any) {
|
||||
return new Ok<T>(any);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace aeqw89.tools.Publish;
|
||||
|
||||
internal static class Shell {
|
||||
internal record ShellResult(string Output, string Error, int ExitCode);
|
||||
internal record PackageInfo(FileInfo FileInfo) {
|
||||
byte[]? _inMemory = null;
|
||||
public long Size() => FileInfo.Length;
|
||||
public async Task<byte[]> ReadAsync() => _inMemory ??= await File.ReadAllBytesAsync(FileInfo.FullName);
|
||||
public byte[] Read() => _inMemory ??= File.ReadAllBytes(FileInfo.FullName);
|
||||
}
|
||||
internal static class Dotnet {
|
||||
internal static async Task<Result<PackageInfo, ReadableError>> FindPackage(string dir) {
|
||||
var package = Directory.GetFiles(dir, "*.nupkg").FirstOrDefault();
|
||||
if (package == null) return new ReadableError(Exceptions.generic_error.EscapeMarkup());
|
||||
return new PackageInfo(new FileInfo(package)).Ok();
|
||||
}
|
||||
|
||||
internal static async Task<Result<ShellResult, ReadableError>> Pack(string tempdir, DataReceivedEventHandler? stderr, DataReceivedEventHandler? stdout) {
|
||||
var p = Process.Start(new ProcessStartInfo() {
|
||||
FileName = "dotnet",
|
||||
Arguments = $"pack -o {tempdir}",
|
||||
WorkingDirectory = Environment.CurrentDirectory,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
});
|
||||
|
||||
StringBuilder errorLines = new();
|
||||
StringBuilder outputLines = new();
|
||||
|
||||
CancellationTokenSource cts = new();
|
||||
p?.ErrorDataReceived += stderr;
|
||||
p?.ErrorDataReceived += (_, e) => {
|
||||
cts.Cancel();
|
||||
errorLines.Append(e.Data);
|
||||
};
|
||||
|
||||
bool success = false;
|
||||
|
||||
p?.OutputDataReceived += stdout;
|
||||
p?.OutputDataReceived += (_, e) => {
|
||||
if (e.Data?.ToLower().Contains("press any key") == true)
|
||||
cts.Cancel();
|
||||
if (e.Data?.ToLower().Contains($"successfully created package '{Path.GetFullPath(tempdir)}") == true) {
|
||||
success = true;
|
||||
}
|
||||
};
|
||||
|
||||
p?.BeginErrorReadLine();
|
||||
p?.BeginOutputReadLine();
|
||||
|
||||
try {
|
||||
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
|
||||
} catch (TaskCanceledException) {
|
||||
p?.Kill();
|
||||
} catch (Exception e) {
|
||||
return new ReadableError(Exceptions.generic_error.EscapeMarkup(), false);
|
||||
}
|
||||
|
||||
return new ShellResult(outputLines.ToString(), errorLines.ToString(), p!.ExitCode).Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Diagnostics;
|
||||
using Spectre.Console;
|
||||
using static aeqw89.tools.Publish.Program;
|
||||
|
||||
namespace aeqw89.tools.Publish;
|
||||
|
||||
internal record Stager(RunContext RunContext) {
|
||||
public async Task<T?> Spinner<T>(string name, Func<RunContext, StatusContext, Task<Result<T, ReadableError>>> factory) {
|
||||
try {
|
||||
var result = AnsiConsole.Status()
|
||||
.Spinner(Spectre.Console.Spinner.Known.Dots)
|
||||
.Start(name, async ctx => await factory(RunContext, ctx));
|
||||
return (await result).Unwrap(RunContext);
|
||||
} catch (Exception e) {
|
||||
ShowError(string.Format(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()));
|
||||
RunContext.Restore();
|
||||
return default;
|
||||
}
|
||||
|
||||
throw new UnreachableException();
|
||||
}
|
||||
|
||||
public ProgressStager Progress() {
|
||||
return new ProgressStager(RunContext, [], AnsiConsole.Progress()
|
||||
.AutoClear(true)
|
||||
.HideCompleted(false)
|
||||
.Columns([
|
||||
new TaskDescriptionColumn(),
|
||||
new ProgressBarColumn()
|
||||
.RemainingStyle(Style.Parse("dim gray slowblink"))
|
||||
.CompletedStyle(Style.Parse("green strikethrough"))
|
||||
.FinishedStyle("green strikethrough"),
|
||||
new DownloadedColumn(),
|
||||
new RemainingTimeColumn(),
|
||||
new TransferSpeedColumn(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
internal record TaskWithProgress<T>(string Name, Task<T> Value);
|
||||
internal record ProgressStager(RunContext RunContext, Task[] Tasks, Progress Progress) {
|
||||
public TaskWithProgress<T> Run<T>(string Name, Func<RunContext, ProgressContext, Task<T>> factory) {
|
||||
return new(Name, Progress.StartAsync(async ctx => {
|
||||
return await factory(RunContext, ctx);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace System.Runtime.CompilerServices {
|
||||
public interface IUnion { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class UnionAttribute : Attribute { }
|
||||
}
|
||||
Reference in New Issue
Block a user