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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@

internal TimeSpan StartTimeout { get; set; } = Config.DefaultServiceCreationTimeout;

// Must be set in this (parent) process, before the host is spawned: environment variables
// changed from managed code already running inside the host, right before it initializes
// Python, are not reliably observed by the native interpreter there. Only variables present
// in the process's environment block at OS process-creation time are.
//
// ProcessStartInfo.EnvironmentVariables starts as a copy of this (parent) process's own
// environment, so if PYTHONNOUSERSITE already happens to be set there for unrelated reasons
// (e.g. a customer's own leftover manual workaround), it would otherwise flow straight
// through to the host untouched. When true, explicitly clears it for the host's own
// environment instead, so a venv's declared --system-site-packages behavior is decided by
// that flag alone, not by whatever's ambient on the machine (STUD-81085 follow-up).
internal bool ClearUserSiteEnvironmentOverride { get; set; }

internal HostWrapper Create()
{
StartHostService();
Expand All @@ -46,7 +59,7 @@
var (folder, hostFullPath) = ResolveHostFullPath(hostFile);

if (!File.Exists(hostFullPath))
throw new Exception($"Process path not found: {hostFullPath}");

Check warning on line 62 in Activities/Python/Shared/UiPath.Shared.Service/Client/Controller.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'System.Exception' should not be thrown by user code.

See more on https://sonarcloud.io/project/issues?id=UiPath_Community.Activities&issues=AaAaUOhqEMCC-aJiWyQc&open=AaAaUOhqEMCC-aJiWyQc&pullRequest=596

PythonWrapper.Proc = Process.Start(CreateProcessStartInfo(hostFullPath, folder, isExeMode));

Expand Down Expand Up @@ -97,6 +110,8 @@
// never reach the host until the buffer fills or the interpreter exits — and on
// forced shutdown (Process.Kill) any buffered output is lost.
psi.EnvironmentVariables["PYTHONUNBUFFERED"] = "1";
if (ClearUserSiteEnvironmentOverride)
psi.EnvironmentVariables.Remove("PYTHONNOUSERSITE");
if (!isExeMode)
psi.ArgumentList.Add(hostFullPath);
return psi;
Expand Down
134 changes: 134 additions & 0 deletions Activities/Python/UiPath.Python.Tests/VenvDetectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.IO;
using UiPath.Python.Impl;
using UiPath.TestUtils;
using Xunit;
using Assert = Xunit.Assert;

namespace UiPath.Python.Tests
{
// Direct, fast tests for VenvDetection.GetVenvInfo — no Python engine involved. Covers the
// detection shapes discussed during the STUD-81085 review: venv root, its Scripts/bin
// launcher folder, and the false-positive an unbounded ancestor walk used to allow (an
// unrelated, fully-standalone installation merely sitting near someone else's pyvenv.cfg).
public class VenvDetectionTests : IDisposable
{
private const string Category = "Python";

private readonly string _rootDir;

public VenvDetectionTests()
{
_rootDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "venv-detection-tests", Guid.NewGuid().ToString("N"))).FullName;
}

public void Dispose()

Check warning on line 25 in Activities/Python/UiPath.Python.Tests/VenvDetectionTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change VenvDetectionTests.Dispose() to call GC.SuppressFinalize(object). This will prevent derived types that introduce a finalizer from needing to re-implement 'IDisposable' to call it.

See more on https://sonarcloud.io/project/issues?id=UiPath_Community.Activities&issues=AaAaUOhWEMCC-aJiWyQZ&open=AaAaUOhWEMCC-aJiWyQZ&pullRequest=596
{
try { Directory.Delete(_rootDir, true); } catch { /* best effort cleanup */ }
}

private string WriteVenvCfg(string venvDir, string home = @"C:\FakeBase", string extra = null)

Check warning on line 30 in Activities/Python/UiPath.Python.Tests/VenvDetectionTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'WriteVenvCfg' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=UiPath_Community.Activities&issues=AaAaUOhWEMCC-aJiWyQa&open=AaAaUOhWEMCC-aJiWyQa&pullRequest=596
{
Directory.CreateDirectory(venvDir);
var content = $"home = {home}{Environment.NewLine}version = 3.13.0{Environment.NewLine}{extra}";
File.WriteAllText(Path.Combine(venvDir, "pyvenv.cfg"), content);
return venvDir;
}

[Fact]
[Trait(TestCategories.Category, Category)]
public void VenvRoot_Is_Detected()
{
var venvDir = WriteVenvCfg(Path.Combine(_rootDir, "myvenv"));

var venv = VenvDetection.GetVenvInfo(venvDir);

Assert.NotNull(venv);
Assert.Equal(venvDir, venv.Root);
Assert.Equal(@"C:\FakeBase", venv.Home);
}

[Theory]
[InlineData("Scripts")]
[InlineData("bin")]
[Trait(TestCategories.Category, Category)]
public void LauncherSubfolder_Is_Detected(string folderName)
{
var venvDir = WriteVenvCfg(Path.Combine(_rootDir, "myvenv"));
var launcherDir = Directory.CreateDirectory(Path.Combine(venvDir, folderName)).FullName;

var venv = VenvDetection.GetVenvInfo(launcherDir);

Assert.NotNull(venv);
Assert.Equal(venvDir, venv.Root);
}

[Fact]
[Trait(TestCategories.Category, Category)]
public void UnrelatedFolder_OneLevelBelow_WrongName_Is_Not_Detected()
{
// pyvenv.cfg one level up, but the intermediate folder isn't a real venv launcher
// name — a fully standalone install could legitimately live here and must not be
// mistaken for being inside someone else's venv.
var venvDir = WriteVenvCfg(Path.Combine(_rootDir, "someones_venv"));
var standaloneDir = Directory.CreateDirectory(Path.Combine(venvDir, "runtime")).FullName;

var venv = VenvDetection.GetVenvInfo(standaloneDir);

Assert.Null(venv);
}

[Fact]
[Trait(TestCategories.Category, Category)]
public void TwoLevelsUp_Is_Not_Detected()
{
// Even with the right launcher name one level further up, detection deliberately
// doesn't walk a second level — no real venv layout ever needs it.
var venvDir = WriteVenvCfg(Path.Combine(_rootDir, "myvenv"));
var scriptsDir = Directory.CreateDirectory(Path.Combine(venvDir, "Scripts")).FullName;
var nestedDir = Directory.CreateDirectory(Path.Combine(scriptsDir, "nested")).FullName;

var venv = VenvDetection.GetVenvInfo(nestedDir);

Assert.Null(venv);
}

[Fact]
[Trait(TestCategories.Category, Category)]
public void StrayFile_Without_HomeOrVersion_Is_Not_Treated_As_Venv()
{
var dir = Directory.CreateDirectory(Path.Combine(_rootDir, "notavenv")).FullName;
File.WriteAllText(Path.Combine(dir, "pyvenv.cfg"), "some-unrelated-key = value" + Environment.NewLine);

var venv = VenvDetection.GetVenvInfo(dir);

Assert.Null(venv);
}

[Fact]
[Trait(TestCategories.Category, Category)]
public void SystemSitePackages_Flag_Is_Captured()
{
var venvDir = WriteVenvCfg(Path.Combine(_rootDir, "myvenv"), extra: "include-system-site-packages = true" + Environment.NewLine);

var venv = VenvDetection.GetVenvInfo(venvDir);

Assert.NotNull(venv);
Assert.True(venv.IncludeSystemSitePackages);
Assert.False(venv.ShouldDisableUserSite);
}

[Fact]
[Trait(TestCategories.Category, Category)]
public void Default_DoesNotIncludeSystemSitePackages_ShouldDisableUserSite()
{
var venvDir = WriteVenvCfg(Path.Combine(_rootDir, "myvenv"));

var venv = VenvDetection.GetVenvInfo(venvDir);

Assert.NotNull(venv);
Assert.False(venv.IncludeSystemSitePackages);
Assert.True(venv.ShouldDisableUserSite);
}
}
}
Loading