StageKit.Primitives
0.3.8
dotnet add package StageKit.Primitives --version 0.3.8
NuGet\Install-Package StageKit.Primitives -Version 0.3.8
<PackageReference Include="StageKit.Primitives" Version="0.3.8" />
<PackageVersion Include="StageKit.Primitives" Version="0.3.8" />
<PackageReference Include="StageKit.Primitives" />
paket add StageKit.Primitives --version 0.3.8
#r "nuget: StageKit.Primitives, 0.3.8"
#:package StageKit.Primitives@0.3.8
#addin nuget:?package=StageKit.Primitives&version=0.3.8
#tool nuget:?package=StageKit.Primitives&version=0.3.8
StageKit.Primitives
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 SafeHandlewrapper for pinnedGCHandlescenarios throughGCSafeHandleMemoryManager<T>wrapper for externally owned unmanaged buffers throughUnmanagedMemoryManager<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 runs ALSA's speaker-test for the requested duration, and macOS plays
the fixed-tone Glass system sound through afplay, 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 | Versions 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. |
-
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.8 | 0 | 9/11/2026 |
| 0.3.7 | 38 | 9/11/2026 |
| 0.3.6 | 56 | 9/10/2026 |
| 0.3.5 | 70 | 9/10/2026 |
| 0.3.4 | 92 | 9/9/2026 |
| 0.3.3 | 93 | 9/8/2026 |
| 0.3.2 | 140 | 9/5/2026 |
| 0.3.1 | 127 | 9/5/2026 |
| 0.3.0 | 134 | 9/3/2026 |
| 0.2.6 | 114 | 8/25/2026 |
| 0.2.5 | 117 | 8/24/2026 |
| 0.2.4 | 567 | 7/20/2026 |
| 0.2.3 | 141 | 7/19/2026 |
| 0.2.2 | 436 | 6/22/2026 |
| 0.2.1 | 152 | 6/7/2026 |
| 0.2.0 | 237 | 5/27/2026 |