Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
e778275
Remove executable directory probing per code review feedback
Copilot Feb 16, 2026
168ddf6
Simplify path resolution: remove CWD and Windows dir probing, require…
Copilot Feb 16, 2026
200d9f4
Restore Process.Unix.cs ResolvePath for backward compatibility
Copilot Feb 16, 2026
ec3f354
address my own feedback, fix the tests
adamsitnik Feb 16, 2026
bdd6301
update comments, minor cleanup
adamsitnik Feb 16, 2026
02905f0
changes after reading everything again
adamsitnik Feb 16, 2026
bf6e551
apply improved suggestion
adamsitnik Feb 16, 2026
4415d7e
address code review feedback:
adamsitnik Feb 17, 2026
a012a1e
update the comment
adamsitnik Feb 17, 2026
bc6543b
address code review feedback: move the common logic to a helper class…
adamsitnik Feb 17, 2026
c056741
Refactor FindProgramInPath to ProcessUtils per jkotas feedback
Copilot Feb 17, 2026
c6b1f56
Make IsExecutable private in ProcessUtils.Windows.cs
Copilot Feb 17, 2026
b61103d
Remove WINDOWS compilation constant per jkotas feedback
Copilot Feb 17, 2026
2dd4a3b
Deduplicate FindProgramInPath: move to ProcessUtils.cs and restore co…
Copilot Feb 17, 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
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
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent)</TargetFrameworks>
Expand Down Expand Up @@ -26,6 +26,8 @@
<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\ProcessUtils.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 @@ -225,6 +229,7 @@
<Compile Include="System\Diagnostics\ProcessStartInfo.Windows.cs" />
<Compile Include="System\Diagnostics\ProcessThread.Windows.cs" />
<Compile Include="System\Diagnostics\ProcessThreadTimes.cs" />
<Compile Include="System\Diagnostics\ProcessUtils.Windows.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows'">
Expand All @@ -235,8 +240,7 @@
<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="System\Diagnostics\ProcessUtils.Unix.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 = ProcessUtils.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 ProcessUtils.FindProgramInPath("xdg-open");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ private static string[] CreateEnvp(ProcessStartInfo psi)
// find filename on PATH
else
{
resolvedFilename = FindProgramInPath(filename);
resolvedFilename = ProcessUtils.FindProgramInPath(filename);
}
}

Expand Down Expand Up @@ -726,93 +726,7 @@ private static string[] CreateEnvp(ProcessStartInfo psi)
}

// 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)
{
Interop.Sys.FileStatus fileinfo;

if (Interop.Sys.Stat(fullPath, out fileinfo) < 0)
{
return false;
}

// Check if the path is a directory.
if ((fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)
{
return false;
}

const UnixFileMode AllExecute = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;

UnixFileMode permissions = ((UnixFileMode)fileinfo.Mode) & AllExecute;

// Avoid checking user/group when permission.
if (permissions == AllExecute)
{
return true;
}
else if (permissions == 0)
{
return false;
}

uint euid = Interop.Sys.GetEUid();

if (euid == 0)
{
return true; // We're root.
}

if (euid == fileinfo.Uid)
{
// We own the file.
return (permissions & UnixFileMode.UserExecute) != 0;
}

bool groupCanExecute = (permissions & UnixFileMode.GroupExecute) != 0;
bool otherCanExecute = (permissions & UnixFileMode.OtherExecute) != 0;

// Avoid group check when group and other have same permissions.
if (groupCanExecute == otherCanExecute)
{
return groupCanExecute;
}

if (Interop.Sys.IsMemberOfGroup(fileinfo.Gid))
{
return groupCanExecute;
}
else
{
return otherCanExecute;
}
return ProcessUtils.FindProgramInPath(filename);
}

private static long s_ticksPerSecond;
Expand Down
Loading
Loading