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
6 changes: 3 additions & 3 deletions Icod.Processes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<AssemblyName>Icod.Processes</AssemblyName>
<RootNamespace>Icod.Processes</RootNamespace>
<Configurations>Debug;Release;Staging</Configurations>
<Version>1.0.0</Version>
<Version>1.1.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(PlatformTarget)' == '' ">
<PlatformTarget>AnyCPU</PlatformTarget>
Expand Down Expand Up @@ -47,8 +47,8 @@
<WarningsNotAsErrors>CS1591</WarningsNotAsErrors>
</PropertyGroup>
<PropertyGroup>
<PackageVersion>1.0.0</PackageVersion>
<PackageReleaseNotes>Initial standalone process execution and control release.</PackageReleaseNotes>
<PackageVersion>1.1.0</PackageVersion>
<PackageReleaseNotes>Adds atomic POSIX child file-descriptor duplication for wrapper commands while preserving the 1.0 execution and control contracts.</PackageReleaseNotes>
<Authors>Timothy J. Bruce</Authors>
<Description>Cross-platform .NET process execution and control primitives for safe child launching, process identity, signals, priorities, liveness, waiting, cancellation, and timeouts.</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
921 changes: 758 additions & 163 deletions LICENSE

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ originally incubated under `Icod.CommandFramework.Processes`.

## Requirements

The initial `1.0.0` release targets .NET 10.0. The implementation uses process
The current `1.1.0` release targets .NET 10.0. The implementation uses process
launch capabilities provided by the .NET 10 runtime and intentionally does not
add compatibility shims for older target frameworks.

Expand All @@ -39,13 +39,13 @@ The only runtime package dependency is `Icod.Timing` 1.0.0.
## Installation

```text
Install-Package Icod.Processes -Version 1.0.0
Install-Package Icod.Processes -Version 1.1.0
```

or:

```text
dotnet add package Icod.Processes --version 1.0.0
dotnet add package Icod.Processes --version 1.1.0
```

## Example
Expand Down Expand Up @@ -79,6 +79,7 @@ operations explicitly rather than fabricating Unix semantics.
| Process identity, PID-reuse observation, liveness, and waiting | Yes | Yes | Yes |
| New process group at child launch | Yes | Yes | Yes |
| Custom native `argv[0]` | Unsupported | Yes | Yes |
| Native child file-descriptor duplication | Unsupported | Yes | Yes |
| Process-group target control | Unsupported | Yes | Yes |
| Signal delivery | Termination substitution | Native | Native |
| Signal disposition observation | Unsupported | Yes | Unsupported |
Expand All @@ -97,7 +98,7 @@ can migrate without taking a dependency on ProcPs or CoreUtils.
Replace the package dependency with:

```xml
<PackageReference Include="Icod.Processes" Version="1.0.0" />
<PackageReference Include="Icod.Processes" Version="1.1.0" />
```

and replace:
Expand Down
42 changes: 42 additions & 0 deletions src/PosixFileDescriptorDuplication.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Icod.Processes;

/// <summary>
/// Describes one ordered POSIX child file-descriptor duplication applied atomically at spawn time.
/// </summary>
/// <remarks>
/// The source descriptor is observed in the child launch context. When <see cref="CloseSource"/> is
/// requested, the source is closed immediately after this duplication unless source and destination
/// are the same descriptor. Actions execute in list order, so a later action may duplicate a
/// destination established by an earlier action.
/// </remarks>
public readonly record struct PosixFileDescriptorDuplication {
/// <summary>Gets the descriptor copied by <c>dup2</c>.</summary>
public int SourceDescriptor { get; }

/// <summary>Gets the child descriptor replaced by <c>dup2</c>.</summary>
public int DestinationDescriptor { get; }

/// <summary>Gets whether the source descriptor is closed in the child after duplication.</summary>
public bool CloseSource { get; }

/// <summary>Initializes one ordered POSIX descriptor duplication.</summary>
public PosixFileDescriptorDuplication(
int sourceDescriptor,
int destinationDescriptor,
bool closeSource = false
) {
if ( 0 > sourceDescriptor ) {
throw new ArgumentOutOfRangeException(
nameof( sourceDescriptor )
);
}
if ( 0 > destinationDescriptor ) {
throw new ArgumentOutOfRangeException(
nameof( destinationDescriptor )
);
}
this.SourceDescriptor = sourceDescriptor;
this.DestinationDescriptor = destinationDescriptor;
this.CloseSource = closeSource;
}
}
88 changes: 88 additions & 0 deletions src/PosixSpawnFileActionsScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace Icod.Processes;

using System.Runtime.InteropServices;

/// <summary>
/// Owns the opaque POSIX spawn file-actions object used for child-only descriptor duplication.
/// </summary>
internal sealed class PosixSpawnFileActionsScope : IDisposable {
// libc keeps posix_spawn_file_actions_t opaque. Reserve generous storage rather
// than duplicating a private glibc or Darwin structure layout in managed code.
private const int FileActionsStorageSize = 1024;
private bool _initialized;

/// <summary>Gets the native file-actions pointer, or zero when no actions are requested.</summary>
internal IntPtr Pointer {
get;
private set;
}

/// <summary>Creates ordered POSIX spawn file actions.</summary>
internal PosixSpawnFileActionsScope(
IList<PosixFileDescriptorDuplication> duplications
) {
ArgumentNullException.ThrowIfNull( duplications );
if ( 0 == duplications.Count ) {
return;
}

this.Pointer = Marshal.AllocHGlobal( FileActionsStorageSize );
try {
Marshal.Copy(
new byte[ FileActionsStorageSize ],
0,
this.Pointer,
FileActionsStorageSize
);
var result = ProcessNative.PosixSpawnFileActionsInit( this.Pointer );
if ( 0 != result ) {
throw new InvalidOperationException(
$"posix_spawn_file_actions_init failed with error {result}."
);
}
this._initialized = true;

foreach ( var duplication in duplications ) {
result = ProcessNative.PosixSpawnFileActionsAddDup2(
this.Pointer,
duplication.SourceDescriptor,
duplication.DestinationDescriptor
);
if ( 0 != result ) {
throw new InvalidOperationException(
$"posix_spawn_file_actions_adddup2 failed with error {result}."
);
}
if ( duplication.CloseSource
&& duplication.SourceDescriptor != duplication.DestinationDescriptor
) {
result = ProcessNative.PosixSpawnFileActionsAddClose(
this.Pointer,
duplication.SourceDescriptor
);
if ( 0 != result ) {
throw new InvalidOperationException(
$"posix_spawn_file_actions_addclose failed with error {result}."
);
}
}
}
} catch {
this.Dispose();
throw;
}
}

/// <inheritdoc />
public void Dispose() {
if ( IntPtr.Zero == this.Pointer ) {
return;
}
if ( this._initialized ) {
_ = ProcessNative.PosixSpawnFileActionsDestroy( this.Pointer );
}
Marshal.FreeHGlobal( this.Pointer );
this.Pointer = IntPtr.Zero;
this._initialized = false;
}
}
43 changes: 43 additions & 0 deletions src/ProcessNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,49 @@ IntPtr oldSet
/// <summary>Gets the POSIX spawn flag that assigns the child process group.</summary>
internal const short PosixSpawnSetProcessGroup = 0x0002;

/// <summary>Initializes one opaque POSIX spawn file-actions object.</summary>
[DllImport(
"libc",
EntryPoint = "posix_spawn_file_actions_init",
SetLastError = false
)]
internal static extern int PosixSpawnFileActionsInit(
IntPtr fileActions
);

/// <summary>Destroys one initialized POSIX spawn file-actions object.</summary>
[DllImport(
"libc",
EntryPoint = "posix_spawn_file_actions_destroy",
SetLastError = false
)]
internal static extern int PosixSpawnFileActionsDestroy(
IntPtr fileActions
);

/// <summary>Adds one ordered <c>dup2</c> operation to POSIX spawn file actions.</summary>
[DllImport(
"libc",
EntryPoint = "posix_spawn_file_actions_adddup2",
SetLastError = false
)]
internal static extern int PosixSpawnFileActionsAddDup2(
IntPtr fileActions,
int sourceDescriptor,
int destinationDescriptor
);

/// <summary>Adds one ordered descriptor close to POSIX spawn file actions.</summary>
[DllImport(
"libc",
EntryPoint = "posix_spawn_file_actions_addclose",
SetLastError = false
)]
internal static extern int PosixSpawnFileActionsAddClose(
IntPtr fileActions,
int descriptor
);

/// <summary>Initializes one opaque POSIX spawn-attribute object.</summary>
[DllImport(
"libc",
Expand Down
11 changes: 11 additions & 0 deletions src/ProcessRunOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,17 @@ public Stream? StandardOutput {
set;
}

/// <summary>
/// Gets ordered POSIX child file-descriptor duplications applied atomically at spawn time.
/// </summary>
/// <remarks>
/// Adding an item selects the native POSIX launcher. This capability is unsupported on Windows
/// and cannot be combined with managed standard-stream redirection or output capture.
/// </remarks>
public IList<PosixFileDescriptorDuplication> PosixFileDescriptorDuplications {
get;
} = new List<PosixFileDescriptorDuplication>();

/// <summary>
/// Gets or sets whether a POSIX child inherits standard input as a write-only null
/// device so reads fail rather than return end-of-file.
Expand Down
16 changes: 13 additions & 3 deletions src/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ or UnauthorizedAccessException
}
executable = located.Value!;
}
if ( null != options.ArgumentZero || ( options.CreateProcessGroup && !OperatingSystem.IsWindows() ) ) {
if ( null != options.ArgumentZero
|| 0 < options.PosixFileDescriptorDuplications.Count
|| ( options.CreateProcessGroup && !OperatingSystem.IsWindows() )
) {
return await this.RunWithPosixSpawnAsync(
options,
executable,
Expand Down Expand Up @@ -443,10 +446,14 @@ private async Task<ProcessResult> RunWithPosixSpawnAsync(
CancellationToken cancellationToken
) {
if ( OperatingSystem.IsWindows() ) {
var message = 0 < options.PosixFileDescriptorDuplications.Count
? "POSIX file-descriptor duplication is unavailable on Windows."
: "The managed Windows launcher cannot set an independent native argument zero safely."
;
return this.HandlePosixSpawnSetupFailure(
options,
startedTimestamp,
"The managed Windows launcher cannot set an independent native argument zero safely."
message
);
}
if ( null != options.StandardInput
Expand Down Expand Up @@ -486,11 +493,14 @@ CancellationToken cancellationToken
options.SignalPolicy,
options.UseUnreadableStandardInput
);
using var spawnFileActions = new PosixSpawnFileActionsScope(
options.PosixFileDescriptorDuplications
);
using var spawnAttributes = new PosixSpawnAttributeScope( options.CreateProcessGroup );
spawnResult = ProcessNative.PosixSpawn(
out processId,
path.Pointer,
IntPtr.Zero,
spawnFileActions.Pointer,
spawnAttributes.Pointer,
arguments.Pointer,
environmentVector.Pointer
Expand Down
3 changes: 2 additions & 1 deletion src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ The source layer includes:
- process, process-group, session, and priority-selector target models;
- arbitrary-process liveness and wait operations;
- portable signal parsing, observation, and delivery;
- POSIX launch-time signal policy and process-group creation; and
- POSIX launch-time signal policy and process-group creation;
- ordered child-only POSIX file-descriptor duplication at spawn time; and
- POSIX nice values with controlled Windows priority-class substitutions.

ProcPs-specific process enumeration, `/proc` reporting fields, selection grammar,
Expand Down
59 changes: 59 additions & 0 deletions tests/Processes.Tests/src/ProcessRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,65 @@ out var childProcessGroupId
}
}

/// <summary>Verifies native POSIX launch applies ordered child-only descriptor duplications.</summary>
[Fact]
public async Task PosixNativeLaunchAppliesFileDescriptorDuplications() {
if ( OperatingSystem.IsWindows() ) {
return;
}

var outputPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
$"icod-processes-fd-{Guid.NewGuid():N}"
);
ProcessResult result;
try {
await using ( var output = new FileStream(
outputPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.Read | FileShare.Write | FileShare.Delete
) ) {
var descriptor = output.SafeFileHandle.DangerousGetHandle().ToInt32();
var options = CreateNativeHostOptions(
"dual",
"1"
);
options.PosixFileDescriptorDuplications.Add(
new PosixFileDescriptorDuplication(
descriptor,
1,
closeSource: true
)
);
options.PosixFileDescriptorDuplications.Add(
new PosixFileDescriptorDuplication(
1,
2
)
);

result = await ProcessRunner.RunAsync(
options
);
}

var outputText = await File.ReadAllTextAsync(
outputPath
);
Assert.True( result.Started );
Assert.Equal( 0, result.ExitCode );
Assert.Contains( "out-0", outputText, StringComparison.Ordinal );
Assert.Contains( "err-0", outputText, StringComparison.Ordinal );
} finally {
if ( File.Exists( outputPath ) ) {
File.Delete(
outputPath
);
}
}
}

/// <summary>Verifies that native POSIX launch rejects managed output capture predictably.</summary>
[Fact]
public async Task PosixNativeLaunchRejectsCapturedOutput() {
Expand Down
Loading