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
12 changes: 1 addition & 11 deletions src/Tests/EvergreenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,16 +254,6 @@ static async Task WaitUntil(Func<bool> condition, TimeSpan timeout)
foreach (var arg in args)
start.ArgumentList.Add(arg);
start.Environment["NDNX_STORE"] = store;

using var process = Process.Start(start) ?? throw new InvalidOperationException("Failed to start ndnx.");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
if (!process.WaitForExit(90_000))
{
process.Kill(entireProcessTree: true);
throw new TimeoutException($"ndnx timed out.{Environment.NewLine}{stdout}{Environment.NewLine}{stderr}");
}

return (process.ExitCode, stdout, stderr);
return ProcessCapture.Run(start);
}
}
21 changes: 13 additions & 8 deletions src/Tests/HelloToolFeed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@ public sealed class HelloToolFeed : IDisposable
public static string RidImplId => $"{RidWrapperId}.{HostRid}";
public static string AnyImplId => $"{AnyWrapperId}.any";

public string Root { get; }
public string FeedDirectory { get; }
static readonly object Gate = new();

public string Root { get; private set; }
public string FeedDirectory { get; private set; }

public HelloToolFeed()
{
Root = Path.Combine(Path.GetTempPath(), "ndnx-hello-tool-feed");
FeedDirectory = Path.Combine(Root, "feed");
lock (Gate)
Build();
}

void Build()
{
Directory.CreateDirectory(FeedDirectory);

var stamp = Path.Combine(Root, "stamp.txt");
Expand Down Expand Up @@ -316,14 +324,11 @@ static void RunDotnet(string[] args, string workingDirectory)
foreach (var arg in args)
start.ArgumentList.Add(arg);

using var process = Process.Start(start) ?? throw new InvalidOperationException("Failed to start dotnet.");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
var (exit, stdout, stderr) = ProcessCapture.Run(start, timeoutMs: 180_000);
if (exit != 0)
{
throw new InvalidOperationException(
$"dotnet {string.Join(' ', args)} failed ({process.ExitCode}).{Environment.NewLine}{stdout}{Environment.NewLine}{stderr}");
$"dotnet {string.Join(' ', args)} failed ({exit}).{Environment.NewLine}{stdout}{Environment.NewLine}{stderr}");
}
}
}
32 changes: 32 additions & 0 deletions src/Tests/PackageFeedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,38 @@ public async Task Http_download_with_progress_disabled_writes_no_progress_ui()
Assert.DoesNotContain(handler.Hits, url => url.Contains("/catalog/", StringComparison.Ordinal));
}

[Fact]
public async Task App_writes_download_progress_to_host_Progress()
{
using var packages = new TempNupkgs();
var body = File.ReadAllBytes(packages.Plain);
var handler = MapFeed(packages);
MapCatalog(handler, PackageId, body.Length);
using var error = new StringWriter();
using var progress = new StringWriter();
using var output = new StringWriter();
var host = new NdnxHost
{
HttpHandler = handler,
Error = error,
Out = output,
Progress = progress,
StoreDirectory = NewStore(),
ShowProgress = true,
ProcessRunner = new RecordingProcessRunner { ExitCode = 0 },
WorkingDirectory = packages.Root,
};

var code = await App.RunAsync([PackageId + "@" + VersionText, "--yes", "--source", Source], host);

Assert.Equal(0, code);
Assert.Equal("", error.ToString());
var ui = progress.ToString();
Assert.Contains("[", ui, StringComparison.Ordinal);
Assert.Contains(" / ", ui, StringComparison.Ordinal);
Assert.Contains(" B", ui, StringComparison.Ordinal);
}

[Fact]
public async Task Http_download_succeeds_when_catalog_size_lookup_fails()
{
Expand Down
33 changes: 33 additions & 0 deletions src/Tests/ProcessCapture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Diagnostics;

namespace Tests;

static class ProcessCapture
{
public static (int ExitCode, string Stdout, string Stderr) Run(ProcessStartInfo start, int timeoutMs = 90_000)
{
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.UseShellExecute = false;

using var process = Process.Start(start)
?? throw new InvalidOperationException("Failed to start " + start.FileName);

var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(timeoutMs))
{
try { process.Kill(entireProcessTree: true); }
catch (InvalidOperationException) { }

throw new TimeoutException(
$"{start.FileName} timed out after {timeoutMs}ms.{Environment.NewLine}{Read(stdoutTask)}{Environment.NewLine}{Read(stderrTask)}");
}

Task.WaitAll(stdoutTask, stderrTask);
return (process.ExitCode, stdoutTask.Result, stderrTask.Result);
}

static string Read(Task<string> task)
=> task.IsCompletedSuccessfully ? task.Result : "";
}
12 changes: 1 addition & 11 deletions src/Tests/PublishedToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,6 @@ static bool Cached(string packageId, string version)
foreach (var arg in args)
start.ArgumentList.Add(arg);
start.Environment["NDNX_STORE"] = Store;

using var process = Process.Start(start) ?? throw new InvalidOperationException("Failed to start ndnx.");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
if (!process.WaitForExit(60_000))
{
process.Kill(entireProcessTree: true);
throw new TimeoutException($"ndnx timed out.{Environment.NewLine}{stdout}{Environment.NewLine}{stderr}");
}

return (process.ExitCode, stdout, stderr);
return ProcessCapture.Run(start);
}
}
7 changes: 1 addition & 6 deletions src/Tests/ToolInvokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,11 +391,6 @@ static string Marker(string store, string packageId)
foreach (var arg in args)
start.ArgumentList.Add(arg);
start.Environment["NDNX_STORE"] = store;

using var process = Process.Start(start) ?? throw new InvalidOperationException("Failed to start ndnx.");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
return (process.ExitCode, stdout, stderr);
return ProcessCapture.Run(start);
}
}
14 changes: 10 additions & 4 deletions src/ndnx/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ public sealed class NdnxHost
public TimeSpan? UpdateInterval { get; init; }
public TimeSpan StopTimeout { get; init; } = TimeSpan.FromSeconds(5);
public bool ShowProgress { get; init; }
public TextWriter? Progress { get; init; }

public static NdnxHost CreateDefault() => new()
public static NdnxHost CreateDefault()
{
ShowProgress = !Console.IsOutputRedirected && !Console.IsErrorRedirected,
};
var progress = ConsoleProgress.TryOpen();
return new()
{
Progress = progress,
ShowProgress = progress is not null,
};
}

public static string DefaultStoreDirectory(string? workingDirectory = null)
=> Environment.GetEnvironmentVariable("NDNX_STORE")
Expand Down Expand Up @@ -93,7 +99,7 @@ public static async Task<int> RunAsync(string[] args, NdnxHost? host = null, Can

var log = IsDetailed(invocation.Verbosity) ? host.Out : null;
var sources = PackageSources.Resolve(invocation, host.WorkingDirectory);
var progress = host.ShowProgress ? host.Error : null;
var progress = host.Progress ?? (host.ShowProgress ? host.Error : null);
var feed = new PackageFeed(http, invocation.IgnoreFailedSources, log, progress);
var muxer = host.DotnetMuxer ?? DotnetMuxer.Resolve();
var store = new ToolPackageStore(feed, host.StoreDirectory, log, muxer, host.RuntimeIdentifier);
Expand Down
37 changes: 37 additions & 0 deletions src/ndnx/ConsoleProgress.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace ndnx;

/// <summary>
/// Live console writer for download progress. PowerShell 7 on Windows pipes
/// native stderr, so <see cref="Console.IsErrorRedirected"/> is true even in a
/// terminal; <c>CONOUT$</c> still reaches the screen.
/// </summary>
public static class ConsoleProgress
{
public static TextWriter? TryOpen()
{
if (OperatingSystem.IsWindows())
return TryOpenConOut();

if (!Console.IsOutputRedirected && !Console.IsErrorRedirected)
return Console.Error;

return null;
}

static TextWriter? TryOpenConOut()
{
try
{
var stream = new FileStream("CONOUT$", FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
return new StreamWriter(stream, Console.OutputEncoding) { AutoFlush = true };
}
catch (IOException)
{
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}
}
}
Loading