Skip to content
Open
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
2 changes: 0 additions & 2 deletions Activities/Python/UiPath.Python.Host.Shared/PythonService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,8 @@ internal async void RunServer()
private bool IsWindows()
{
bool isWindows = true;
#if NETCOREAPP
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
isWindows = false;
#endif
return isWindows;
}
private void WaitForPipeDrain(NamedPipeServerStream pipe)
Expand Down
102 changes: 70 additions & 32 deletions Activities/Python/UiPath.Python/EngineProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using UiPath.Python.Impl;
using UiPath.Python.Properties;
using System.Runtime.InteropServices;

namespace UiPath.Python
{
Expand All @@ -14,8 +15,9 @@ namespace UiPath.Python
public static class EngineProvider
{
private const string PythonHomeEnv = "PYTHONHOME";
private const string PythonExe = "python.exe";
private const string PythonLinux = "python";
private static readonly string[] PythonExeWin = ["python.exe", "python3.exe"];
private static readonly string[] PythonLinux = ["python", "python3"];
private static readonly string[] PythonBinFolders = ["", "bin"];
private const string PythonVersionArgument = "--version";
Comment on lines +18 to 21
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions searching in the install folder and in the ../bin folder, but the current candidate generation only checks path and path/bin via PythonBinFolders = ["", "bin"]. If path can point at a .../lib location (common for some Python envs), the executable will be in a sibling ../bin and won’t be found. Consider adding an explicit ../bin candidate (e.g., include Path.Combine("..", "bin") in the folder list) and normalizing with GetFullPath after combining.

Copilot uses AI. Check for mistakes.

// engines cache
Expand All @@ -31,7 +33,7 @@ public static IEngine Get(Version version, string path, string libraryPath, bool
{
// read path from env variable
path = Environment.GetEnvironmentVariable(PythonHomeEnv);
Trace.TraceInformation($"Found Pyhton path {path}");
Trace.TraceInformation($"Found Python path {path}");
}
if (!version.IsValid())
{
Expand All @@ -43,7 +45,7 @@ public static IEngine Get(Version version, string path, string libraryPath, bool
}

// TODO: target&visible are meaningless when running in-process (at least now), maybe it should be split
if(inProcess)
if (inProcess)
{
if (!_cache.TryGetValue(version, out engine))
{
Expand All @@ -62,37 +64,73 @@ public static IEngine Get(Version version, string path, string libraryPath, bool

public static void Autodetect(string path, out Version version)
{
version = Version.Auto;
Trace.TraceInformation($"Trying to autodetect Python version from path {path}");
var pythonExec = PythonExe;
#if NETCOREAPP
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
pythonExec = PythonLinux;
#endif
string pyExe = Path.GetFullPath(Path.Combine(path, pythonExec));
if (!File.Exists(pyExe))

var exes = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? PythonExeWin : PythonLinux;
var pythonCandidates = PythonBinFolders.SelectMany(folder => exes, Path.Combine).Select(p => Path.Combine(path, p)).Distinct().Select(Path.GetFullPath).ToList();
var existingFiles = pythonCandidates.Where(File.Exists).ToList();

if (existingFiles.Count == 0)
{
throw new FileNotFoundException(Resources.PythonExeNotFoundException, pyExe);
throw new FileNotFoundException(Resources.PythonExeNotFoundException, string.Join(", ", pythonCandidates));
}
Process process = new Process();
process.StartInfo = new ProcessStartInfo()

Dictionary<string, Exception> errors = new Dictionary<string, Exception>();
bool versionDetected = false;

foreach (var python in existingFiles)
{
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden,
FileName = pyExe,
Arguments = PythonVersionArgument,
RedirectStandardError = true,
RedirectStandardOutput = true
};
process.Start();
// Now read the value, parse to int and add 1 (from the original script)
string ver = process.StandardError.ReadToEnd();
if(string.IsNullOrEmpty(ver))
ver = process.StandardOutput.ReadToEnd();
process.WaitForExit();
version = ver.GetVersionFromStr();
Trace.TraceInformation($"Autodetected Python version {version}");
if (Autodetect(python, out version, out Exception ex))
{
versionDetected = true;
break;
}
else
{
errors[python] = ex;
}
}

// if we are here, it means that we found some candidates but all of them failed to be detected, so we throw an aggregate exception with all the details
if (!versionDetected)
{
throw new AggregateException(errors.Where(kv => kv.Value != null).Select(kv => new Exception($"{kv.Key}: {kv.Value.Message}", kv.Value)));
}
}

private static bool Autodetect(string pythonFullPath, out Version version, out Exception exception)
{
version = Version.Auto;
exception = null;
try
{
using Process process = new Process();
process.StartInfo = new ProcessStartInfo()
{
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = pythonFullPath,
Arguments = PythonVersionArgument,
RedirectStandardError = true,
RedirectStandardOutput = true
};
process.Start();
process.WaitForExit(3000);
// Now read the value, parse to int and add 1 (from the original script)
string ver = process.StandardError.ReadToEnd();
if (string.IsNullOrEmpty(ver))
ver = process.StandardOutput.ReadToEnd();

version = ver.GetVersionFromStr();
return version != Version.Auto;
}
catch (Exception ex)
{
exception = ex;
return false;
}
}

}
}
2 changes: 0 additions & 2 deletions Activities/Python/UiPath.Python/Impl/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,8 @@ internal Engine(Version version, string path, string libraryPath)
_version = version;
_path = path;
_libraryPath = libraryPath;
#if NETCOREAPP
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
_isWindows = false;
#endif
}

#region IEngine
Expand Down
2 changes: 0 additions & 2 deletions Activities/Python/UiPath.Python/Service/PythonProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,8 @@ private PythonResponse ReadResponse(PythonRequest request, CancellationToken ct)
private bool IsWindows()
{
bool isWindows = true;
#if NETCOREAPP
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
isWindows = false;
#endif
return isWindows;
}
private void WaitForPipeDrain()
Expand Down
2 changes: 0 additions & 2 deletions Activities/Shared/UiPath.Shared.Service/Client/Controller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ internal void ForceStop()
private void StartHostService()
{
var isWindows = true;
#if NETCOREAPP
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
isWindows = false;
#endif
string folder = Path.GetDirectoryName(HostLibFile);
var hostLibFullPath = HostLibFile;
if (folder.IsNullOrEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public void Dispose()

//Prevent process leak in certain error scenarios
Proc?.Kill();
Proc?.Dispose();
Proc = null;
Comment on lines 30 to 32
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proc?.Kill() can throw (e.g., process already exited / has no associated process / access denied). If that happens, Proc?.Dispose() and Proc = null won’t run, which can reintroduce the leak this method is trying to prevent. Consider wrapping Kill() in a try/catch (or checking HasExited first), and ensure Dispose() runs in a finally-like path.

Suggested change
Proc?.Kill();
Proc?.Dispose();
Proc = null;
var proc = Proc;
if (proc != null)
{
try
{
// Attempt to kill the process only if it has not already exited.
if (!proc.HasExited)
{
proc.Kill();
}
}
catch (InvalidOperationException)
{
// Process may have already exited or have no associated process; ignore in dispose path.
}
catch (System.ComponentModel.Win32Exception)
{
// Access denied or other OS-level issue; ignore in dispose path to avoid masking earlier errors.
}
finally
{
proc.Dispose();
Proc = null;
}
}

Copilot uses AI. Check for mistakes.
}

Expand Down
Loading