StageKit.Primitives 0.3.3

There is a newer version of this package available.
See the version list below for details.
dotnet add package StageKit.Primitives --version 0.3.3
                    
NuGet\Install-Package StageKit.Primitives -Version 0.3.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="StageKit.Primitives" Version="0.3.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="StageKit.Primitives" Version="0.3.3" />
                    
Directory.Packages.props
<PackageReference Include="StageKit.Primitives" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add StageKit.Primitives --version 0.3.3
                    
#r "nuget: StageKit.Primitives, 0.3.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package StageKit.Primitives@0.3.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=StageKit.Primitives&version=0.3.3
                    
Install as a Cake Addin
#tool nuget:?package=StageKit.Primitives&version=0.3.3
                    
Install as a Cake Tool

StageKit.Primitives

Logo

License GitHub repo size Code size Nuget GitHub Sponsors

StageKit.Primitives is a dependency-light .NET package with reusable low-level helpers used by StageKit libraries and available for other libraries or apps.

All public helpers are exposed from the StageKit.Primitives namespace. IO-related source files are grouped under an IO/ folder for organization only.

Features

  • Atomic file writes with temporary-file replacement through SafeFile
  • Stream-based atomic file writes through SafeFileStream
  • Path, leaf-name validation, temporary file, and temporary directory helpers
  • Bash ANSI-C and Windows batch value quoting helpers through StringExtensions
  • Host-aware operating-system names, network availability, path comparison, URL/file-manager launching, and Unix executable-permission helpers
  • Disposable base type with thread-safe idempotent disposal through DisposableObject
  • Finalizable disposable base type through UnmanagedDisposableObject
  • Leave-open lifecycle base type through LeaveOpenDisposableObject
  • SafeHandle wrapper for pinned GCHandle scenarios through GCSafeHandle
  • MemoryManager<T> wrapper for externally owned unmanaged buffers through UnmanagedMemoryManager<T>

Install

dotnet add package StageKit.Primitives

Requirements

  • .NET 8 or newer
  • C# latest language version

Host information

The operating-system display name and architecture are detected once per process:

using StageKit.Primitives.System;

Console.WriteLine(HostSystem.OperatingSystemName);         // Windows, macOS, Linux, ...
Console.WriteLine(HostSystem.OperatingSystemNameWithArch); // Windows X64, macOS Arm64, ...
Console.WriteLine(HostSystem.SystemManufacturer);          // Device manufacturer, when available
Console.WriteLine(HostSystem.SystemModel);                 // Device model, when available
Console.WriteLine(HostSystem.SystemUptime);                // Time since the operating system started
Console.WriteLine(HostSystem.ProcessorName);               // Processor model, when available

foreach (string graphicsCard in HostSystem.GraphicsCardNames)
{
    Console.WriteLine(graphicsCard);
}

Check whether the host currently has an operational non-loopback, non-tunnel network interface, or actively verify internet access:

if (HostSystem.IsNetworkAvailable())
{
    bool hasInternet = await HostSystem.IsInternetAvailableAsync();
}

IsNetworkAvailable() is a fast local check. IsInternetAvailableAsync() sends a small request to Microsoft's connectivity-test service, validates the expected response to detect common captive portals, and times out after three seconds. It accepts a cancellation token.

Manufacturer, model, processor, and graphics-adapter results are cached after successful detection. Generic firmware placeholder values are treated as unavailable. Windows uses registry hardware data, falls back to baseboard identity for custom-built systems, and excludes indirect USB and software display drivers. Linux uses DMI or device-tree data and parses /proc/cpuinfo and PCI information with a sysfs fallback, while macOS uses sysctl and structured system_profiler output. External hardware queries are limited to five seconds.

Memory

using StageKit.Primitives.System;

if (HostSystem.TryGetMemoryStatus(out var memory))
{
    Console.WriteLine($"Available: {memory.AvailablePhysicalBytes / (1024d * 1024 * 1024):F2} GiB");
    Console.WriteLine($"Used: {memory.MemoryLoadPercentage:F1}%");
}

GetMemoryStatus() returns a fresh snapshot, or an empty snapshot when unavailable. Windows and macOS use native APIs; Linux reads /proc/meminfo without regular expressions. No subprocesses or additional dependencies are needed. Physical-memory properties use bytes and describe OS-visible memory, not container limits. Linux prefers MemAvailable and falls back to MemFree; macOS estimates availability using free plus inactive pages. The returned HostMemoryStatus is an immutable, platform-neutral value containing total, available, and used physical memory plus the percentage in use.

SafeFile

Use SafeFile when you need to write a file through a temporary file and then replace the destination.

using StageKit.Primitives;

SafeFile.WriteAllText("settings.json", json);

SafeFile.Write("settings.json", stream =>
{
    JsonSerializer.Serialize(stream, settings);
});

Async writes are supported:

await SafeFile.WriteAllTextAsync(
    "settings.json",
    json,
    cancellationToken: cancellationToken);

Temporary files use this pattern:

<destination>.tmp.<guid>

Use SafeFile.IsTemporaryPathFor(...) when filtering a directory that may contain temporary files for an in-progress write:

if (SafeFile.IsTemporaryPathFor(candidatePath, destinationPath))
{
    return;
}

SafeFileStream

Use SafeFileStream when you want stream-style writes with atomic replacement.

using StageKit.Primitives;
using System.Text;

using var stream = new SafeFileStream("settings.json");
stream.Write(Encoding.UTF8.GetBytes(json));
// Dispose commits by default.

Set commitOnDispose to false when you want to commit explicitly:

await using var stream = new SafeFileStream("settings.json", commitOnDispose: false);
await stream.WriteAsync(buffer, cancellationToken);
await stream.CommitAsync(cancellationToken);

If a SafeFileStream is disposed without committing and commitOnDispose is false, the temporary file is deleted and the destination is left unchanged.

IO Helpers

Use PathUtilities for platform-aware path comparisons and archive entry normalization:

if (!PathUtilities.IsSubPathOf(candidatePath, rootPath))
{
    throw new InvalidOperationException("Path escapes the root directory.");
}

var entryName = PathUtilities.NormalizeArchiveEntryName(relativePath);

Use FileUtilities when a value must be one simple file or directory name rather than a rooted or nested path:

if (!FileUtilities.IsPathLeafName(assetName))
{
    throw new InvalidOperationException("The asset name is invalid.");
}

var validatedName = FileUtilities.ValidatePathLeafName(assetName, nameof(assetName));

ValidatePathLeafName(...) returns the validated value and throws InvalidOperationException for blank names, rooted paths, path separators, ./.., or characters rejected by Path.GetInvalidFileNameChars().

Use StringExtensions when generating shell scripts:

var bashValue = value.QuoteBashAnsiCString();
var batchValue = value.EscapeWindowsBatchValue();

QuoteBashAnsiCString() rejects null characters because Bash variables cannot contain them. The batch helper removes carriage returns, writes line feeds as \n, and escapes batch metacharacters for delayed expansion.

Use TemporaryDirectory when a temporary workspace should be removed automatically:

using var directory = new TemporaryDirectory(prefix: "stagekit");
var outputPath = Path.Combine(directory.DirectoryPath, "output.json");

Use TemporaryFile when a temporary file should be removed unless explicitly kept:

using var file = new TemporaryFile(extension: "json");
await File.WriteAllTextAsync(file.FilePath, json);

file.Keep();

Use ShellScriptFile to build and run a .bat or .sh file. It is a TextWriter, so the whole Write/WriteLine family and anything that writes into a TextWriter can compose the script. Execution uses the platform shell and returns the exit code, standard output, and standard error as a ProcessOutput:

using StageKit.Primitives;
using StageKit.Primitives.System;

using var script = ShellScriptFile.CreateTemporary();
script.WriteLineIfWindows("echo %~1");
script.WriteLineIfUnix("printf %s \"$1\"");

ProcessOutput output = await script.ExecuteAsync(["Ready"], cancellationToken);

CreateTemporary(...) and the pathless constructor assign a unique path with the platform script extension (ShellScriptFile.ScriptFileExtension) and delete it on disposal by default. Pass a path to the constructor to keep a script at that path, or explicitly set deleteOnDispose: false when a generated path must outlive the writer:

await using var script = new ShellScriptFile(Path.Combine(profilePath, "install.sh"));
script.WriteLine("echo installing");

await script.FlushAsync(cancellationToken);

Content is buffered in memory and written atomically to FilePath by Flush()/FlushAsync(...), by every Execute overload, and on disposal when writes are still pending and the file is kept. IsFlushPending reports unwritten content, GetScript() returns the buffer, and Keep() clears DeleteOnDispose. On Unix the file is marked executable after each write; set SetExecutablePermission to false to skip that.

Platform-specific Write... and WriteLine... methods are available for Windows, macOS, Linux, and Unix. The Unix helpers apply to Linux, macOS, and FreeBSD. Clear() resets the script while restoring the platform preamble, and WriteComment(...) prefixes every comment line for the selected shell.

The plural WriteLines, WriteLinesAsync, WriteLinesIf, WriteLinesIfWindows/MacOS/Linux/Unix, and WriteComments write several lines at once, taking either params or any IEnumerable<string?>:

script.WriteComments("Generated by MyApp", "Do not edit");
script.WriteLines("set -e", "cd \"$1\"", "./configure");
script.WriteLinesIfUnix("chmod +x ./run.sh", "./run.sh");
script.WriteLinesIf(OperatingSystem.IsLinux(), commandLines);
await script.WriteLinesAsync("echo done", "exit 0");

They are separate names rather than overloads of WriteLine, so every inherited TextWriter overload — including composite formatting such as WriteLine("{0}", value) — keeps its usual meaning.

When Windows elevation uses runas, the script still returns its exit code but its output strings are empty because that launch mode cannot redirect standard streams.

System Helpers

HostSystem.HostStringComparison provides the comparison StageKit uses for file-system paths: ordinal, case-insensitive comparison on Windows and ordinal comparison elsewhere.

using StageKit.Primitives.System;

bool samePath = string.Equals(leftPath, rightPath, HostSystem.HostStringComparison);

Use HostSystem.TryFindExecutable(...) to resolve an executable without starting a lookup process. It honors PATHEXT on Windows and requires an execute permission bit on Unix:

if (HostSystem.TryFindExecutable("git", out string? gitPath))
    Console.WriteLine(gitPath);

Open URLs, directories, and files with the host's default applications. ShowFileInFileManager(...) selects the file in Windows Explorer or macOS Finder; on Linux it opens the containing directory because there is no portable selection command:

HostSystem.OpenUrl("https://example.com");
HostSystem.OpenDirectory(profileDirectory);
HostSystem.OpenFile(reportPath);
HostSystem.ShowFileInFileManager(reportPath);

Async counterparts such as OpenUrlAsync(...) and ShowFileInFileManagerAsync(...) accept a cancellation token. All open methods return false for invalid or missing targets, unsupported hosts, and launcher failures.

Beep(...) plays a tone through the host speaker, and BeepAsync(...) completes when the tone ends:

HostSystem.Beep();                                        // 800 Hz for 150 ms, in the background
HostSystem.Beep(440, 500, waitForCompletion: true);       // blocks until the tone ends
await HostSystem.BeepAsync(440, 500, cancellationToken);  // completes when the tone ends

Both clamp the frequency to 37 - 20000 Hz and raise durations below 40 ms, because Console.Beep rejects values outside that range. Windows uses Console.Beep, Linux prefers ALSA's speaker-test, and macOS falls back to the fixed-tone system alert sound, so frequency is ignored there. A beep is best-effort and never throws: hosts without a console, audio device, or tone utility return false. BeepAsync(...) observes cancellation only before the tone starts, since neither backend can be interrupted.

Use UnixSystem.SetUnix755Executable(...) to grant owner write/execute and group/other execute permissions to a Unix launcher. The method is a no-op on Windows.

UnixSystem.SetUnix755Executable(scriptPath);

Use ProcessHelper.StartProcess(...) to launch a command, optionally waiting for its exit code. Set requireElevation: true to show the platform administrator prompt through Windows runas, Linux pkexec, or macOS osascript. Elevation is skipped when Environment.IsPrivilegedProcess is already true:

int exitCode = ProcessHelper.StartProcess(
    "system-tool",
    ["--configure", "value with spaces"],
    requireElevation: true,
    waitForCompletion: true);

Use StartShellScript(...) or StartShellScriptAsync(...) to run a .bat or .sh file through the platform shell. Both raw command-line strings and argument lists are supported; prefer an argument list for values containing spaces:

int scriptExitCode = await ProcessHelper.StartShellScriptAsync(
    scriptPath,
    ["--profile", profileName],
    waitForCompletion: true,
    cancellationToken: cancellationToken);

Use StartProcessWithShellExecute(...) or StartProcessWithShellExecuteAsync(...) when the operating system shell must resolve the target, such as a document associated with its default application. Both families mirror all StartProcess(...) overloads and otherwise have the same elevation, wait, timeout, exit-code, failure, and cancellation behavior:

int exitCode = ProcessHelper.StartProcessWithShellExecute(reportPath);
int asyncExitCode = await ProcessHelper.StartProcessWithShellExecuteAsync(
    reportPath,
    cancellationToken: cancellationToken);

The ProcessStartInfo overloads set UseShellExecute to true on the supplied instance. Shell execution does not support redirected standard input, output, or error streams.

Use StartHostProcess(...) or StartHostProcessAsync(...) when a command must run on the host from a Flatpak app. Inside Flatpak these methods route argument lists through flatpak-spawn --host; elsewhere they behave like the normal start helpers. Host execution requires the Flatpak manifest permission --talk-name=org.freedesktop.Flatpak:

int exitCode = await ProcessHelper.StartHostProcessAsync(
    "flatpak",
    ["--user", "info", applicationId],
    waitForCompletion: true,
    cancellationToken: cancellationToken);

CreateHostProcessStartInfo(...) exposes the composed start information when additional launcher configuration is needed. Only the argument-list overload is provided so command boundaries remain intact across the host bridge.

For full control over the working directory, environment, window behavior, and other process settings, configure the created ProcessStartInfo before starting it. Use CreateShellProcessStartInfo(...) when the command needs shell syntax:

var startInfo = ProcessHelper.CreateShellProcessStartInfo("system-tool --configure");
startInfo.WorkingDirectory = workspacePath;
startInfo.Environment["STAGEKIT_MODE"] = "maintenance";

ProcessOutput output = await ProcessHelper.GetProcessOutputAsync(startInfo, cancellationToken);

Pass an IEnumerable<string> to CreateShellProcessStartInfo(...) to supply the command plus additional shell arguments. The factory prepends /d /c on Windows or -c elsewhere.

Use CreateShellScriptProcessStartInfo(...) to run a script file. It passes the path as a discrete argument instead of embedding it in a shell command string, so a path containing spaces survives; bash -c would word-split it apart:

var startInfo = ProcessHelper.CreateShellScriptProcessStartInfo(scriptFilePath, requireElevation: true);
startInfo.WorkingDirectory = workspacePath;

int exitCode = await ProcessHelper.StartProcessAsync(startInfo, waitForCompletion: true, cancellationToken: token);

On Unix the script runs under bash, which reads the file directly and ignores its shebang. Windows still routes through cmd /d /c, because a batch file cannot be launched without a command interpreter.

Detecting a denied elevation

IsExitCodeElevationDenied(...) tells a refused administrator prompt apart from an ordinary command failure, so callers can prompt again instead of reporting a broken install:

int exitCode = await ProcessHelper.StartProcessAsync(startInfo, waitForCompletion: true, cancellationToken: token);

if (ProcessHelper.IsExitCodeElevationDenied(exitCode)) { /* the user declined the prompt */ }
Platform Denial exit code Constant
Windows 1223 WindowsElevationCancelledExitCode
Linux 126, 127 LinuxElevationDismissedExitCode, LinuxElevationNotAuthorizedExitCode
macOS 1 MacOSElevationCancelledExitCode

Windows reports a dismissed runas prompt as ERROR_CANCELLED from process creation rather than as an exit code; the start and output helpers translate it to WindowsElevationCancelledExitCode so it can be matched like any other code. Every other startup failure and every timeout still returns -1.

A cancelled macOS prompt shares exit code 1 with an ordinary command failure, so the single-argument overload always returns false there. Pass the captured standard error to the second overload to cover macOS, which matches the AppleScript cancellation error -128:

ProcessOutput output = await ProcessHelper.GetProcessOutputAsync(startInfo, token);

if (ProcessHelper.IsExitCodeElevationDenied(output.ExitCode, output.StandardError)) { /* declined */ }

Note that Linux 127 is also a shell's "command not found", so it is not an unambiguous denial signal.

StageKit.Primitives.Extensions.ProcessExtensions offers the same check without passing anything, on an exited Process or on a captured ProcessOutput:

using var process = Process.Start(startInfo);
await process.WaitForExitAsync(token);

if (process.IsExitCodeElevationDenied()) { /* the user declined the prompt */ }

ProcessOutput output = await ProcessHelper.GetProcessOutputAsync(startInfo, token);

if (output.IsExitCodeElevationDenied()) { /* also covers the macOS prompt, via standard error */ }

The Process overload reads ExitCode, so it throws InvalidOperationException when the process has not exited, and it inherits the macOS limitation. The ProcessOutput overload has the standard error available and does not.

Run shell syntax through cmd /d /c on Windows or bash -c elsewhere, and use the output helpers when the exit code and both redirected streams are needed. Output helpers also accept requireElevation; Linux and macOS capture through their elevation wrappers, while non-privileged Windows runas capture returns exit code -1 because that API cannot redirect the elevated child streams:

int shellExitCode = ProcessHelper.StartShell("system-tool --configure", waitForCompletion: true);

ProcessOutput output = ProcessHelper.GetShellOutput("system-tool --status", requireElevation: true);
Console.Write(output.StandardOutput);
Console.Error.Write(output.StandardError);

ProcessOutput asyncOutput = await ProcessHelper.GetShellOutputAsync(
    "system-tool --status",
    cancellationToken: cancellationToken);

DisposableObject

Use DisposableObject for classes that need idempotent deterministic cleanup.

using StageKit.Primitives;

public sealed class Worker : DisposableObject
{
    private readonly Stream _stream;

    public Worker(Stream stream)
    {
        _stream = stream;
    }

    public void Run()
    {
        ThrowIfDisposed();

        // Use the stream.
    }

    protected override void DisposeManaged()
    {
        _stream.Dispose();
    }
}

DisposeManaged() runs for normal Dispose() calls. During explicit disposal, DisposeUnmanaged() runs after managed cleanup is attempted, even if managed cleanup throws. The base class does not define a finalizer; types that directly own unmanaged resources should prefer SafeHandle or use UnmanagedDisposableObject.

UnmanagedDisposableObject is available when a type directly owns unmanaged resources and needs finalizer fallback. Prefer SafeHandle when the resource is a native handle.

LeaveOpenDisposableObject

Use LeaveOpenDisposableObject when a type needs to expose a leave-open option for a resource owned by the caller.

using StageKit.Primitives;

public sealed class StreamWriterOwner : LeaveOpenDisposableObject
{
    private readonly Stream _stream;

    public StreamWriterOwner(Stream stream, bool leaveOpen)
        : base(leaveOpen)
    {
        _stream = stream;
    }

    protected override void DisposeManaged()
    {
        if (!LeaveOpen)
        {
            _stream.Dispose();
        }
    }
}

Derived classes are responsible for honoring LeaveOpen.

GCSafeHandle

GCSafeHandle wraps a GCHandle in a SafeHandle so pinned memory can be released reliably.

using StageKit.Primitives;

var buffer = new byte[1024];
using var handle = new GCSafeHandle(buffer);

IntPtr address = handle.DangerousGetHandle();

Use this only when pinning is necessary, such as interop paths that require a stable address. Keep pinning lifetimes short.

UnmanagedMemoryManager

Use UnmanagedMemoryManager<T> when an externally owned unmanaged buffer needs to be exposed as Memory<T>.

using StageKit.Primitives;

using var manager = new UnmanagedMemoryManager<byte>(bufferAddress, bufferLength);
Memory<byte> memory = manager.Memory;

The manager does not allocate, pin, or free the underlying memory. The caller must keep the buffer alive and fixed for every Memory<T> or MemoryHandle produced by the manager.

License

StageKit.Primitives is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on StageKit.Primitives:

Package Downloads
StageKit

Lightweight .NET application infrastructure for JSON settings, autosave, schema migration, atomic persistence, backups, support bundles, retention, onboarding state, single-instance guards, crash reports, and unhandled exception handling.

EmguExtensions

A high-performance .NET library that extends Emgu.CV (OpenCV wrapper) with zero-copy Span/Memory Mat accessors, async stream copy helpers, ROI utilities, pluggable Mat compression (PNG, Deflate, GZip, ZLib, Brotli, Zstd), CMat for memory-efficient compressed image storage, structured contour hierarchy wrappers, and drawing helpers for polygons and multi-line text rendering.

StageKit.Fallout

Reusable .NET build pipeline for self-contained publishing, runtime manifests, portable archives, installers, macOS bundles, and Linux distribution packages.

StageKit.Updatum

Secure GitHub release discovery, download verification, and cross-platform application updates for .NET applications.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.7 0 9/11/2026
0.3.6 28 9/10/2026
0.3.5 64 9/10/2026
0.3.4 83 9/9/2026
0.3.3 88 9/8/2026
0.3.2 138 9/5/2026
0.3.1 125 9/5/2026
0.3.0 132 9/3/2026
0.2.6 110 8/25/2026
0.2.5 114 8/24/2026
0.2.4 566 7/20/2026
0.2.3 141 7/19/2026
0.2.2 436 6/22/2026
0.2.1 151 6/7/2026
0.2.0 236 5/27/2026