Editor-side utility that finds, validates and can download/install the Slang command-line compiler (slangc). It probes overrides, cached cookie, vendored project folder, engine folders, PATH and environment vars, runs slangc -v to get a version, and can download the latest Windows release from GitHub and extract required binaries into the project Tools/slang/bin folder.
using Editor.Prism.Core;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace Editor.Prism.Toolchain;
/// <summary>Where a located <c>slangc</c> came from. Shown in Preferences so the choice is never a mystery.</summary>
public enum SlangToolchainSource
{
/// <summary>Nothing was found.</summary>
None,
/// <summary>An explicit path the user set in Preferences.</summary>
Override,
/// <summary>Vendored in the project under <c>Tools/slang/bin</c>. The recommended location.</summary>
Vendored,
/// <summary>Shipped beside the engine, if Facepunch ever starts shipping it.</summary>
Engine,
/// <summary>Found on <c>PATH</c>.</summary>
Path,
/// <summary>Pointed at by an environment variable.</summary>
Environment,
/// <summary>Found by scanning the usual places.</summary>
Discovered,
/// <summary>Just installed by <see cref="SlangToolchain.Install"/>.</summary>
Installed
}
/// <summary>A located Slang toolchain.</summary>
public sealed record SlangToolchainInfo( string ExecutablePath, string Version, SlangToolchainSource Source )
{
/// <summary>The "there is no toolchain" value. Never null, so call sites never branch on null.</summary>
public static readonly SlangToolchainInfo None = new( null, null, SlangToolchainSource.None );
/// <summary>True when a working executable was located and it answered a version query.</summary>
public bool IsAvailable => !string.IsNullOrWhiteSpace( ExecutablePath );
/// <summary>The folder holding the executable, or null.</summary>
public string Directory =>
string.IsNullOrWhiteSpace( ExecutablePath ) ? null : System.IO.Path.GetDirectoryName( ExecutablePath );
/// <summary>A one-line description for the status chip and the preferences page.</summary>
public string Describe() => IsAvailable
? $"slangc {Version ?? "?"} ({Source.ToString().ToLowerInvariant()})"
: "slangc not found";
/// <inheritdoc/>
public override string ToString() => Describe();
}
/// <summary>Progress of a toolchain download, reported on whichever thread the work is running on.</summary>
/// <param name="Stage">What is happening right now, e.g. <c>Downloading</c>.</param>
/// <param name="Fraction">0..1 where known, negative when indeterminate.</param>
/// <param name="BytesReceived">Bytes transferred so far.</param>
/// <param name="BytesTotal">Total bytes, or 0 when the server did not say.</param>
public readonly record struct SlangInstallProgress( string Stage, float Fraction, long BytesReceived, long BytesTotal )
{
/// <summary>A human-readable one-liner for a progress dialog.</summary>
public string Describe() => BytesTotal > 0
? $"{Stage} — {BytesReceived / 1048576.0:0.0} / {BytesTotal / 1048576.0:0.0} MB"
: Stage;
/// <inheritdoc/>
public override string ToString() => Describe();
}
/// <summary>The outcome of an install attempt. Failure is a value, never an exception.</summary>
public sealed record SlangInstallResult( bool Ok, string Message, SlangToolchainInfo Toolchain )
{
/// <summary>True when the user cancelled rather than anything going wrong.</summary>
public bool Cancelled { get; init; }
/// <inheritdoc/>
public override string ToString() => Ok ? $"installed: {Toolchain}" : $"failed: {Message}";
}
/// <summary>
/// Finds, verifies and installs the <c>slangc</c> command-line compiler.
/// <para>
/// Slang validation is Tier 2 and never gates rendering, so every path through this class degrades
/// quietly: a missing toolchain is a value, not an exception, and a user who never installs Slang has a
/// fully working shader editor. Slang <em>emission</em> is pure text generation from the IR and does
/// not depend on any of this.
/// </para>
/// <para>
/// The probe order is: an explicit override, the cached answer, the project's vendored copy, the engine
/// folder, <c>PATH</c>, a handful of environment variables, then a bounded scan of the usual places.
/// The winner is cached in <c>EditorCookie</c> so the scan happens at most once per machine.
/// </para>
/// </summary>
public static class SlangToolchain
{
/// <summary>The GitHub endpoint that names the newest release without hardcoding a version.</summary>
public const string ReleasesApi = "https://api.github.com/repos/shader-slang/slang/releases/latest";
/// <summary>GitHub rejects API requests with no user agent.</summary>
const string UserAgent = "Prism-ShaderEditor";
/// <summary>How long a <c>slangc -v</c> probe may take before we assume the binary is broken.</summary>
const int VersionTimeoutMs = 8000;
/// <summary>Upper bound on the discovery scan, so a pathological directory tree cannot hang the editor.</summary>
const int ScanBudgetMs = 2500;
/// <summary>The executable name, including its extension on Windows.</summary>
public static string ExecutableName => OperatingSystem.IsWindows() ? "slangc.exe" : "slangc";
static readonly object s_lock = new();
static SlangToolchainInfo s_current;
static bool s_probed;
/// <summary>
/// Files without which <c>slangc.exe</c> will not even start. Extracting only these keeps the
/// install around 32 MB instead of the ZIP's 117.
/// </summary>
public static IReadOnlyList<string> RequiredFiles { get; } = new[]
{
"slangc.exe", "slang.dll", "slang-compiler.dll", "slang-glslang.dll"
};
/// <summary>Files worth having but not required: the language server and the GLSL front end.</summary>
public static IReadOnlyList<string> OptionalFiles { get; } = new[]
{
"slangd.exe", "slang-glsl-module.dll"
};
/// <summary>Raised whenever the located toolchain changes, so the status chip can refresh.</summary>
public static event Action Changed;
/// <summary>The toolchain in use. Probes on first access; never null.</summary>
public static SlangToolchainInfo Current
{
get
{
lock ( s_lock )
{
if ( s_probed && s_current is not null ) return s_current;
}
return Probe();
}
}
/// <summary>True when a working <c>slangc</c> was located.</summary>
public static bool IsAvailable => Current.IsAvailable;
/// <summary>Absolute path of the located <c>slangc</c>, or null.</summary>
public static string ExecutablePath => Current.ExecutablePath;
/// <summary>Version string reported by the located <c>slangc</c>, or null.</summary>
public static string Version => Current.Version;
/// <summary>
/// The user's explicit override, persisted in <c>EditorCookie</c>. Setting it re-probes immediately;
/// setting it to null or empty clears it.
/// </summary>
public static string Override
{
get => PrismLog.Guard( "Reading the Slang override",
() => EditorCookie.GetString( PrismConstants.CookieSlangPathOverride, null ), null );
set
{
PrismLog.Guard( "Writing the Slang override", () =>
EditorCookie.SetString( PrismConstants.CookieSlangPathOverride, value ?? string.Empty ) );
Probe( true );
}
}
/// <summary>Where <see cref="Install"/> puts the binaries: <c><project>/Tools/slang/bin</c>.</summary>
public static string InstallDirectory => PrismLog.Guard( "Resolving the Slang install directory", () =>
{
var root = Project.Current?.GetRootPath();
if ( string.IsNullOrWhiteSpace( root ) ) return null;
return Path.Combine( root, PrismConstants.SlangToolchainDir.Replace( '/', Path.DirectorySeparatorChar ) );
}, null );
// ---- probing ---------------------------------------------------------
/// <summary>
/// Locate a toolchain. Cheap and idempotent after the first call; pass <paramref name="force"/> to
/// ignore both the in-memory result and the cookie. Never throws.
/// </summary>
public static SlangToolchainInfo Probe( bool force = false )
{
lock ( s_lock )
{
if ( !force && s_probed && s_current is not null ) return s_current;
}
var found = PrismLog.Guard( "Probing for slangc", () => Locate( force ), SlangToolchainInfo.None )
?? SlangToolchainInfo.None;
bool changed;
lock ( s_lock )
{
changed = s_current is null || s_current != found;
s_current = found;
s_probed = true;
}
Remember( found );
if ( changed )
{
PrismLog.Trace( $"Slang toolchain: {found.Describe()}" );
PrismLog.Guard( "Raising SlangToolchain.Changed", () => Changed?.Invoke() );
}
return found;
}
/// <summary>Probe off the UI thread. The scan and the version query both spawn processes.</summary>
public static Task<SlangToolchainInfo> ProbeAsync( bool force = false, CancellationToken ct = default ) =>
Task.Run( () => Probe( force ), ct );
/// <summary>Forget everything we know, including the cookie. The next probe starts from scratch.</summary>
public static void Forget()
{
lock ( s_lock )
{
s_current = null;
s_probed = false;
}
PrismLog.Guard( "Clearing the Slang cookie", () =>
{
EditorCookie.SetString( PrismConstants.CookieSlangPath, string.Empty );
EditorCookie.SetString( PrismConstants.CookieSlangVersion, string.Empty );
} );
}
/// <summary>
/// The validator matching the current toolchain: a real <see cref="SlangcValidator"/> when one was
/// found, the honest no-op otherwise. Call sites never branch on availability themselves.
/// </summary>
public static ISlangValidator CreateValidator()
{
var toolchain = Current;
if ( !toolchain.IsAvailable ) return NullSlangValidator.Instance;
return new SlangcValidator( toolchain.ExecutablePath, toolchain.Version );
}
static SlangToolchainInfo Locate( bool force )
{
// 1. an explicit override always wins, even if it is broken — the user asked for it.
var over = Override;
if ( Usable( over, out var resolved ) && TryGetVersion( resolved, out var overrideVersion ) )
{
return new SlangToolchainInfo( resolved, overrideVersion, SlangToolchainSource.Override );
}
// 2. what we found last time, if it is still there.
if ( !force )
{
var cached = PrismLog.Guard( "Reading the Slang cookie",
() => EditorCookie.GetString( PrismConstants.CookieSlangPath, null ), null );
if ( Usable( cached, out var cachedPath ) )
{
var cachedVersion = PrismLog.Guard( "Reading the Slang version cookie",
() => EditorCookie.GetString( PrismConstants.CookieSlangVersion, null ), null );
if ( string.IsNullOrWhiteSpace( cachedVersion ) && !TryGetVersion( cachedPath, out cachedVersion ) )
{
cachedVersion = null;
}
if ( !string.IsNullOrWhiteSpace( cachedVersion ) )
{
return new SlangToolchainInfo( cachedPath, cachedVersion, SlangToolchainSource.Discovered );
}
}
}
foreach ( var (path, source) in Candidates() )
{
if ( !Usable( path, out var candidate ) ) continue;
if ( !TryGetVersion( candidate, out var version ) ) continue;
return new SlangToolchainInfo( candidate, version, source );
}
return SlangToolchainInfo.None;
}
/// <summary>Every place worth looking, in priority order, cheapest first.</summary>
static IEnumerable<(string Path, SlangToolchainSource Source)> Candidates()
{
var install = InstallDirectory;
if ( !string.IsNullOrWhiteSpace( install ) )
{
yield return (Path.Combine( install, ExecutableName ), SlangToolchainSource.Vendored);
}
// Beside the engine. It does not ship slangc today, but it ships slang.dll, so it might.
foreach ( var engine in EngineDirectories() )
{
yield return (Path.Combine( engine, ExecutableName ), SlangToolchainSource.Engine);
}
foreach ( var variable in new[] { "PRISM_SLANGC", "SLANGC", "SLANG_ROOT", "SLANG_DIR", "SLANG_HOME" } )
{
var value = SafeEnvironment( variable );
if ( string.IsNullOrWhiteSpace( value ) ) continue;
yield return (value, SlangToolchainSource.Environment);
yield return (Path.Combine( value, ExecutableName ), SlangToolchainSource.Environment);
yield return (Path.Combine( value, "bin", ExecutableName ), SlangToolchainSource.Environment);
}
foreach ( var entry in PathEntries() )
{
yield return (Path.Combine( entry, ExecutableName ), SlangToolchainSource.Path);
}
foreach ( var found in Discover() )
{
yield return (found, SlangToolchainSource.Discovered);
}
}
/// <summary>Folders the engine keeps native tooling in.</summary>
static IEnumerable<string> EngineDirectories()
{
var root = PrismLog.Guard( "Resolving the engine folder",
() => Editor.FileSystem.Root?.GetFullPath( "/" ), null );
if ( string.IsNullOrWhiteSpace( root ) ) yield break;
yield return Path.Combine( root, "bin", "win64" );
yield return Path.Combine( root, "bin", "managed" );
}
static IEnumerable<string> PathEntries()
{
var path = SafeEnvironment( "PATH" );
if ( string.IsNullOrWhiteSpace( path ) ) yield break;
foreach ( var entry in path.Split( Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries ) )
{
var trimmed = entry.Trim().Trim( '"' );
if ( trimmed.Length == 0 ) continue;
yield return trimmed;
}
}
/// <summary>
/// A bounded scan of the places a Slang release typically gets unpacked to. Depth and wall time are
/// both capped, and the result is cached in a cookie, so this runs at most once and never hangs.
/// </summary>
static IEnumerable<string> Discover()
{
var roots = new List<string>();
void Add( string root, params string[] parts )
{
if ( string.IsNullOrWhiteSpace( root ) ) return;
var combined = parts is { Length: > 0 } ? Path.Combine( new[] { root }.Concat( parts ).ToArray() ) : root;
if ( !roots.Contains( combined, StringComparer.OrdinalIgnoreCase ) ) roots.Add( combined );
}
Add( Project.Current?.GetRootPath(), "Tools" );
Add( SafeEnvironment( "LOCALAPPDATA" ), "Prism" );
Add( SafeEnvironment( "LOCALAPPDATA" ), "slang" );
Add( SafeEnvironment( "USERPROFILE" ), "scoop", "apps", "slang" );
Add( SafeEnvironment( "USERPROFILE" ), ".slang" );
Add( SafeEnvironment( "ProgramFiles" ), "slang" );
Add( Path.GetTempPath() );
var deadline = Stopwatch.StartNew();
var results = new List<string>();
foreach ( var root in roots )
{
if ( deadline.ElapsedMilliseconds > ScanBudgetMs ) break;
PrismLog.Guard( $"Scanning {root} for slangc", () =>
{
if ( !Directory.Exists( root ) ) return;
var options = new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
MaxRecursionDepth = 8,
AttributesToSkip = FileAttributes.ReparsePoint
};
foreach ( var file in Directory.EnumerateFiles( root, ExecutableName, options ) )
{
results.Add( file );
if ( results.Count >= 8 ) break;
if ( deadline.ElapsedMilliseconds > ScanBudgetMs ) break;
}
} );
}
return results;
}
static string SafeEnvironment( string name ) =>
PrismLog.Guard( $"Reading %{name}%", () => Environment.GetEnvironmentVariable( name ), null );
/// <summary>True when the path names a file that exists; resolves to a full path on the way out.</summary>
static bool Usable( string path, out string full )
{
full = Resolve( path );
return full is not null;
}
/// <summary>The absolute path of an existing candidate, or null. A folder resolves to the exe inside it.</summary>
static string Resolve( string path )
{
if ( string.IsNullOrWhiteSpace( path ) ) return null;
return PrismLog.Guard( "Checking a slangc candidate", () =>
{
var candidate = path.Trim().Trim( '"' );
if ( Directory.Exists( candidate ) ) candidate = Path.Combine( candidate, ExecutableName );
return File.Exists( candidate ) ? Path.GetFullPath( candidate ) : null;
}, null );
}
// ---- version ---------------------------------------------------------
static readonly Regex s_version = new( @"\d+\.\d+(\.\d+)*", RegexOptions.Compiled | RegexOptions.CultureInvariant );
/// <summary>
/// Run <c>slangc -v</c> and read the version back. This is also the "does this binary actually work"
/// test: a <c>slangc.exe</c> without <c>slang-compiler.dll</c> beside it fails to start, and that
/// shows up here rather than at the first validation.
/// </summary>
public static bool TryGetVersion( string executablePath, out string version )
{
version = null;
if ( Resolve( executablePath ) is not { } resolved ) return false;
var captured = PrismLog.Guard( $"Running {Path.GetFileName( resolved )} -v", () =>
{
var info = new ProcessStartInfo( resolved, "-v" )
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName( resolved ) ?? string.Empty
};
using var process = Process.Start( info );
if ( process is null ) return null;
// Both pipes are drained concurrently, and the timeout is applied to the drain as well as to
// the exit. Reading them one after the other is the classic child-process deadlock: the
// child blocks writing to a full stderr buffer, ReadToEnd on stdout never returns, and the
// WaitForExit below is never reached — an unbounded hang rather than an eight-second one.
// `slangc -v` prints a few bytes so this would not bite today, but a mis-vendored binary of
// the same name would, and this runs on the main thread on the install path.
var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
if ( !process.WaitForExit( VersionTimeoutMs ) )
{
try { process.Kill( true ); } catch ( InvalidOperationException ) { }
return null;
}
if ( !Task.WaitAll( new Task[] { stdoutTask, stderrTask }, VersionTimeoutMs ) )
{
try { process.Kill( true ); } catch ( InvalidOperationException ) { }
return null;
}
var stdout = stdoutTask.Result;
var stderr = stderrTask.Result;
// slangc prints its version on stdout, but some builds use stderr; take whichever has one.
return string.IsNullOrWhiteSpace( stdout ) ? stderr : stdout;
}, null );
if ( string.IsNullOrWhiteSpace( captured ) ) return false;
foreach ( var line in captured.Split( '\n' ) )
{
var trimmed = line.Trim();
if ( trimmed.Length == 0 ) continue;
var match = s_version.Match( trimmed );
version = match.Success ? match.Value : trimmed;
return true;
}
return false;
}
static void Remember( SlangToolchainInfo info )
{
if ( info is null || !info.IsAvailable ) return;
PrismLog.Guard( "Caching the Slang toolchain", () =>
{
EditorCookie.SetString( PrismConstants.CookieSlangPath, info.ExecutablePath );
EditorCookie.SetString( PrismConstants.CookieSlangVersion, info.Version ?? string.Empty );
} );
}
// ---- installation ----------------------------------------------------
/// <summary>
/// Download the official Windows release from GitHub and extract just the binaries <c>slangc</c>
/// needs into <c><project>/Tools/slang/bin</c>, then verify the result by running it.
/// <para>
/// Cancellable at every step. A cancelled or failed install removes whatever it had written so far,
/// so the vendored folder is either complete or absent, never half a toolchain. Editor assemblies
/// are unsandboxed, so <see cref="HttpClient"/>, <see cref="ZipArchive"/> and
/// <see cref="Process"/> are all legal here.
/// </para>
/// </summary>
public static async Task<SlangInstallResult> Install( IProgress<SlangInstallProgress> progress = null,
CancellationToken ct = default )
{
var target = InstallDirectory;
if ( string.IsNullOrWhiteSpace( target ) )
{
return new SlangInstallResult( false, "There is no current project to install into.", SlangToolchainInfo.None );
}
var archive = Path.Combine( Path.GetTempPath(), $"prism-slang-{Guid.NewGuid():N}.zip" );
var written = new List<string>();
try
{
Report( progress, "Finding the latest release", -1f, 0, 0 );
var url = await ResolveDownloadUrl( ct ).ConfigureAwait( false );
if ( string.IsNullOrWhiteSpace( url ) )
{
return new SlangInstallResult( false,
"Could not find a Windows build in the latest Slang release.", SlangToolchainInfo.None );
}
await Download( url, archive, progress, ct ).ConfigureAwait( false );
Report( progress, "Extracting", -1f, 0, 0 );
Directory.CreateDirectory( target );
var wanted = new HashSet<string>( RequiredFiles.Concat( OptionalFiles ), StringComparer.OrdinalIgnoreCase );
using ( var zip = ZipFile.OpenRead( archive ) )
{
foreach ( var entry in zip.Entries )
{
ct.ThrowIfCancellationRequested();
if ( string.IsNullOrEmpty( entry.Name ) ) continue;
if ( !wanted.Contains( entry.Name ) ) continue;
var destination = Path.Combine( target, entry.Name );
entry.ExtractToFile( destination, true );
written.Add( destination );
}
}
var missing = RequiredFiles
.Where( x => !File.Exists( Path.Combine( target, x ) ) )
.ToArray();
if ( missing.Length > 0 )
{
Rollback( written );
return new SlangInstallResult( false,
$"The release archive did not contain {string.Join( ", ", missing )}.", SlangToolchainInfo.None );
}
Report( progress, "Verifying", -1f, 0, 0 );
var executable = Path.Combine( target, ExecutableName );
if ( !TryGetVersion( executable, out var version ) )
{
Rollback( written );
return new SlangInstallResult( false,
"slangc was installed but would not start. Its companion DLLs may be missing or blocked.",
SlangToolchainInfo.None );
}
var info = new SlangToolchainInfo( executable, version, SlangToolchainSource.Installed );
lock ( s_lock )
{
s_current = info;
s_probed = true;
}
Remember( info );
PrismLog.Guard( "Raising SlangToolchain.Changed", () => Changed?.Invoke() );
Report( progress, "Done", 1f, 0, 0 );
return new SlangInstallResult( true, $"Installed slangc {version}.", info );
}
catch ( OperationCanceledException )
{
Rollback( written );
return new SlangInstallResult( false, "Cancelled.", SlangToolchainInfo.None ) { Cancelled = true };
}
catch ( Exception e )
{
PrismLog.Error( e, "Installing the Slang toolchain failed" );
Rollback( written );
return new SlangInstallResult( false, e.Message, SlangToolchainInfo.None );
}
finally
{
PrismLog.Guard( "Removing the downloaded Slang archive", () =>
{
if ( File.Exists( archive ) ) File.Delete( archive );
} );
}
}
/// <summary>
/// Ask GitHub for the newest release and pick the asset for this machine's architecture. Returns
/// null when the request fails or nothing matches, never throws for a network problem.
/// </summary>
public static async Task<string> ResolveDownloadUrl( CancellationToken ct = default )
{
var suffix = RuntimeInformation.OSArchitecture == Architecture.Arm64
? "windows-aarch64.zip"
: "windows-x86_64.zip";
try
{
using var client = CreateClient();
var json = await client.GetStringAsync( ReleasesApi, ct ).ConfigureAwait( false );
var document = JsonNode.Parse( json );
if ( document?["assets"] is not JsonArray assets ) return null;
string fallback = null;
foreach ( var asset in assets )
{
var name = asset?["name"]?.GetValue<string>();
var url = asset?["browser_download_url"]?.GetValue<string>();
if ( string.IsNullOrWhiteSpace( name ) || string.IsNullOrWhiteSpace( url ) ) continue;
if ( name.Contains( "debug-info", StringComparison.OrdinalIgnoreCase ) ) continue;
if ( !name.StartsWith( "slang-", StringComparison.OrdinalIgnoreCase ) ) continue;
if ( name.EndsWith( suffix, StringComparison.OrdinalIgnoreCase ) ) return url;
if ( fallback is null && name.Contains( "windows", StringComparison.OrdinalIgnoreCase ) &&
name.EndsWith( ".zip", StringComparison.OrdinalIgnoreCase ) )
{
fallback = url;
}
}
return fallback;
}
catch ( OperationCanceledException )
{
throw;
}
catch ( Exception e )
{
PrismLog.Error( e, "Could not reach the Slang release feed" );
return null;
}
}
static async Task Download( string url, string destination, IProgress<SlangInstallProgress> progress,
CancellationToken ct )
{
using var client = CreateClient();
using var response = await client
.GetAsync( url, HttpCompletionOption.ResponseHeadersRead, ct )
.ConfigureAwait( false );
response.EnsureSuccessStatusCode();
var total = response.Content.Headers.ContentLength ?? 0L;
using var source = await response.Content.ReadAsStreamAsync( ct ).ConfigureAwait( false );
using var sink = File.Create( destination );
var buffer = new byte[128 * 1024];
var received = 0L;
var lastReport = 0L;
while ( true )
{
var read = await source.ReadAsync( buffer, ct ).ConfigureAwait( false );
if ( read <= 0 ) break;
await sink.WriteAsync( buffer.AsMemory( 0, read ), ct ).ConfigureAwait( false );
received += read;
// Reporting every chunk would flood the UI; every megabyte is plenty.
if ( received - lastReport < 1048576 && received != total ) continue;
lastReport = received;
Report( progress, "Downloading", total > 0 ? (float)( received / (double)total ) : -1f, received, total );
}
}
static HttpClient CreateClient()
{
var client = new HttpClient { Timeout = TimeSpan.FromMinutes( 10 ) };
client.DefaultRequestHeaders.Add( "User-Agent", UserAgent );
client.DefaultRequestHeaders.Add( "Accept", "application/vnd.github+json" );
return client;
}
static void Report( IProgress<SlangInstallProgress> progress, string stage, float fraction,
long received, long total )
{
if ( progress is null ) return;
PrismLog.Guard( "Reporting Slang install progress",
() => progress.Report( new SlangInstallProgress( stage, fraction, received, total ) ) );
}
static void Rollback( IEnumerable<string> written )
{
foreach ( var file in written )
{
PrismLog.Guard( "Rolling back a partial Slang install", () =>
{
if ( File.Exists( file ) ) File.Delete( file );
} );
}
}
}