Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4f3f950
Initial plan
Copilot Feb 11, 2026
92de299
Add ProcessStartOptions class with path resolution and tests
Copilot Feb 11, 2026
0c9d3e2
Fix build errors and test failures in ProcessStartOptions
Copilot Feb 11, 2026
794a8a2
Improve code comments based on review feedback
Copilot Feb 11, 2026
a1b3426
Fix exception documentation and add caching comment
Copilot Feb 11, 2026
10ff95a
Address PR feedback: use Process.ResolvePath, fix tests, add null checks
Copilot Feb 11, 2026
fc0fd30
Refactor: ProcessStartOptions.ResolvePath as main impl, Process calls it
Copilot Feb 11, 2026
f687eb8
address my own feedback
adamsitnik Feb 11, 2026
5459ec9
Simplify Windows tests: remove ConditionalFact, fix nested try-finally
Copilot Feb 11, 2026
c3517ee
Refactor tests: partial classes, remove Test prefix, use new() syntax…
Copilot Feb 12, 2026
bbea91e
Fix test conditions: remove redundant IsWindows check, use GetFullPat…
Copilot Feb 12, 2026
d9d381f
Fix test failures: use ResolveTarget helper for symlinks, fix lambda …
Copilot Feb 12, 2026
b4d6e64
Fix Unix test failures: use EndsWith instead of exact path comparison
Copilot Feb 12, 2026
98d4201
Address code review: add file check, fix .exe logic, update comments,…
Copilot Feb 12, 2026
5b024d4
Cache Windows directory value for performance
Copilot Feb 12, 2026
add4af6
Remove DictionaryWrapper and fix Unix path test for macOS symlinks
Copilot Feb 12, 2026
b3939ad
Use Environment.SystemDirectory and fix Unix path test for macOS
Copilot Feb 13, 2026
63e7fc0
Inline GetSystemDirectory, fix Unix path test, use EndsWith for util …
Copilot Feb 13, 2026
9ceaad1
Update src/libraries/System.Diagnostics.Process/src/System.Diagnostic…
adamsitnik Feb 13, 2026
82e2cec
fix the ResolvePath_UsesCurrentDirectory test
adamsitnik Feb 13, 2026
0f37ab7
Merge remote-tracking branch 'origin/main' into copilot/add-processst…
adamsitnik Feb 13, 2026
bf02bd9
address code review feedback:
adamsitnik Feb 15, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class Kernel32
{
[LibraryImport(Libraries.Kernel32, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
internal static partial uint GetWindowsDirectoryW(ref char lpBuffer, uint uSize);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,17 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
public string WorkingDirectory { get { throw null; } set { } }
}
public sealed partial class ProcessStartOptions
{
public ProcessStartOptions(string fileName) { }
public System.Collections.Generic.IList<string> Arguments { get { throw null; } set { } }
public bool CreateNewProcessGroup { get { throw null; } set { } }
public System.Collections.Generic.IDictionary<string, string?> Environment { get { throw null; } }
public string FileName { get { throw null; } }
public System.Collections.Generic.IList<System.Runtime.InteropServices.SafeHandle> InheritedHandles { get { throw null; } set { } }
public bool KillOnParentExit { get { throw null; } set { } }
public string? WorkingDirectory { get { throw null; } set { } }
}
[System.ComponentModel.DesignerAttribute("System.Diagnostics.Design.ProcessThreadDesigner, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public partial class ProcessThread : System.ComponentModel.Component
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,7 @@
<data name="InvalidPerfData" xml:space="preserve">
<value>Invalid performance counter data with type '{0}'.</value>
</data>
<data name="FileNotFoundResolvePath" xml:space="preserve">
<value>Could not resolve the file.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
<GeneratePlatformNotSupportedAssemblyMessage Condition="'$(TargetPlatformIdentifier)' == ''">SR.Process_PlatformNotSupported</GeneratePlatformNotSupportedAssemblyMessage>
<IsiOSLike Condition="'$(TargetPlatformIdentifier)' == 'maccatalyst' or '$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos'">true</IsiOSLike>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'windows'">$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' != ''">
Expand All @@ -26,6 +27,7 @@
<Compile Include="System\Diagnostics\ProcessModuleCollection.cs" />
<Compile Include="System\Diagnostics\ProcessPriorityClass.cs" />
<Compile Include="System\Diagnostics\ProcessStartInfo.cs" />
<Compile Include="System\Diagnostics\ProcessStartOptions.cs" />
<Compile Include="System\Diagnostics\ProcessThread.cs" />
<Compile Include="System\Diagnostics\ProcessThreadCollection.cs" />
<Compile Include="System\Diagnostics\ProcessWindowStyle.cs" />
Expand All @@ -47,6 +49,8 @@
Link="Common\System\Text\ValueStringBuilder.cs" />
<Compile Include="$(CommonPath)System\Collections\Generic\ArrayBuilder.cs"
Link="Common\System\Collections\Generic\ArrayBuilder.cs" />
<Compile Include="$(CommonPath)System\IO\StringParser.cs"
Link="Common\System\IO\StringParser.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
Expand Down Expand Up @@ -136,6 +140,8 @@
Link="Common\Interop\Windows\Kernel32\Interop.GetProcessPriorityBoost.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SetProcessPriorityBoost.cs"
Link="Common\Interop\Windows\Kernel32\Interop.SetProcessPriorityBoost.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetWindowsDirectoryW.cs"
Link="Common\Interop\Windows\Kernel32\Interop.GetWindowsDirectoryW.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.OpenThread.cs"
Link="Common\Interop\Windows\Kernel32\Interop.OpenThread.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SetThreadPriority.cs"
Expand Down Expand Up @@ -235,8 +241,6 @@
<Compile Include="System\Diagnostics\ProcessStartInfo.Unix.cs" />
<Compile Include="System\Diagnostics\ProcessWaitHandle.Unix.cs" />
<Compile Include="System\Diagnostics\ProcessWaitState.Unix.cs" />
<Compile Include="$(CommonPath)System\IO\StringParser.cs"
Link="Common\System\IO\StringParser.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs"
Link="Common\Interop\Unix\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Errors.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private static DateTime BootTime
ReadOnlySpan<string> allowedProgramsToRun = ["xdg-open", "gnome-open", "kfmclient"];
foreach (var program in allowedProgramsToRun)
{
string? pathToProgram = FindProgramInPath(program);
string? pathToProgram = ProcessStartOptions.FindProgramInPath(program);
if (!string.IsNullOrEmpty(pathToProgram))
{
return pathToProgram;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal DateTime StartTimeCore
/// <summary>Gets execution path</summary>
private static string? GetPathToOpenFile()
{
return FindProgramInPath("xdg-open");
return ProcessStartOptions.FindProgramInPath("xdg-open");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ private bool StartCore(ProcessStartInfo startInfo)
}
else
{
filename = ResolvePath(startInfo.FileName);
filename = ProcessStartOptions.ResolvePath(startInfo.FileName);
argv = ParseArgv(startInfo);
if (Directory.Exists(filename))
{
Expand Down Expand Up @@ -663,7 +663,7 @@ private static string[] CreateEnvp(ProcessStartInfo psi)
// find filename on PATH
else
{
resolvedFilename = FindProgramInPath(filename);
resolvedFilename = ProcessStartOptions.FindProgramInPath(filename);
}
}

Expand All @@ -682,79 +682,7 @@ private static string[] CreateEnvp(ProcessStartInfo psi)
}
}

/// <summary>Resolves a path to the filename passed to ProcessStartInfo. </summary>
/// <param name="filename">The filename.</param>
/// <returns>The resolved path. It can return null in case of URLs.</returns>
private static string? ResolvePath(string filename)
{
// Follow the same resolution that Windows uses with CreateProcess:
// 1. First try the exact path provided
// 2. Then try the file relative to the executable directory
// 3. Then try the file relative to the current directory
// 4. then try the file in each of the directories specified in PATH
// Windows does additional Windows-specific steps between 3 and 4,
// and we ignore those here.

// If the filename is a complete path, use it, regardless of whether it exists.
if (Path.IsPathRooted(filename))
{
// In this case, it doesn't matter whether the file exists or not;
// it's what the caller asked for, so it's what they'll get
return filename;
}

// Then check the executable's directory
string? path = Environment.ProcessPath;
if (path != null)
{
try
{
path = Path.Combine(Path.GetDirectoryName(path)!, filename);
if (File.Exists(path))
{
return path;
}
}
catch (ArgumentException) { } // ignore any errors in data that may come from the exe path
}

// Then check the current directory
path = Path.Combine(Directory.GetCurrentDirectory(), filename);
if (File.Exists(path))
{
return path;
}

// Then check each directory listed in the PATH environment variables
return FindProgramInPath(filename);
}

/// <summary>
/// Gets the path to the program
/// </summary>
/// <param name="program"></param>
/// <returns></returns>
private static string? FindProgramInPath(string program)
{
string path;
string? pathEnvVar = Environment.GetEnvironmentVariable("PATH");
if (pathEnvVar != null)
{
var pathParser = new StringParser(pathEnvVar, ':', skipEmpty: true);
while (pathParser.MoveNext())
{
string subPath = pathParser.ExtractCurrent();
path = Path.Combine(subPath, program);
if (IsExecutable(path))
{
return path;
}
}
}
return null;
}

private static bool IsExecutable(string fullPath)
internal static bool IsExecutable(string fullPath)
{
Interop.Sys.FileStatus fileinfo;

Expand Down
Loading
Loading