From 0a06f6ea008fe5a9b6603caec7da3a13ab64ff58 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Thu, 30 Jul 2026 10:43:52 -0700 Subject: [PATCH 1/2] Limit Dev18 profiling to Python 3.12+ Remove the legacy etwtrace payload and reject unsupported interpreters before Diagnostics Hub starts a session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8b082fe2-2f94-4c5d-82a7-ea7b5a3284ef --- Build/PreBuild.ps1 | 36 ++------- Python/Product/Profiling/Profiling.csproj | 11 +-- .../Profiling/PythonProfilerCommandService.cs | 74 ++++++++++++++++++- Python/Product/Profiling/Strings.Designer.cs | 18 +++++ Python/Product/Profiling/Strings.resx | 6 ++ Python/Product/Profiling/diaghub_profile.py | 47 +----------- 6 files changed, 109 insertions(+), 83 deletions(-) diff --git a/Build/PreBuild.ps1 b/Build/PreBuild.ps1 index f54be28166..4ee66fd56f 100644 --- a/Build/PreBuild.ps1 +++ b/Build/PreBuild.ps1 @@ -34,10 +34,6 @@ param ( # The etwtrace release that includes Python 3.12 through 3.14 wheels. [Parameter()] [string] $etwtraceVersion = "0.1b9", - - # The last etwtrace release that includes Python 3.9 through 3.11 wheels. - [Parameter()] - [string] $etwtraceLegacyVersion = "0.1b8", # Run in interactive mode for azure feed authentication, defaults to false [Parameter()] @@ -55,21 +51,16 @@ function Install-Package { param( [string] $packageName, [string] $version, - [string] $outdir, - [string] $installDir + [string] $outdir ) Write-Host "Installing $packageName $version" - if (-not $installDir) { - $installDir = $outdir - } - - $argList = "install_pypi_package.py", $packageName, $version, "`"$installDir`"" + $argList = "install_pypi_package.py", $packageName, $version, "`"$outdir`"" Start-Process -Wait -NoNewWindow "$outdir\python\tools\python.exe" -ErrorAction Stop -ArgumentList $argList | Write-Host $installedVersion = "" - $versionPyFile = Join-Path $installDir "$packageName\_version.py" + $versionPyFile = Join-Path $outdir "$packageName\_version.py" foreach ($line in Get-Content $versionPyFile) { if ($line.Trim().StartsWith("`"version`"")) { $installedVersion = $line.split(":")[1].Trim(" `"") # trim spaces and double quotes @@ -286,26 +277,13 @@ try { } "-----" - # Install a coherent legacy payload for Python 3.9 through 3.11. Newer - # etwtrace releases only publish wheels for currently supported Pythons. - $legacyEtwTraceOutDir = Join-Path $outdir "etwtrace-legacy" - if (Test-Path -Path $legacyEtwTraceOutDir) { - Remove-Item -Recurse -Force -Path $legacyEtwTraceOutDir - } - Install-Package "etwtrace" $etwtraceLegacyVersion $outdir $legacyEtwTraceOutDir | Out-Null - - # Install the current payload for Python 3.12 and later. + # Install etwtrace for the supported Python versions. Install-Package "etwtrace" $etwtraceVersion $outdir | Out-Null # Delete an unsigned file from etwtrace that shouldn't be there. - $filesToDelete = @( - "$outdir\etwtrace\test\DiagnosticsHub.InstrumentationCollector.dll", - "$legacyEtwTraceOutDir\etwtrace\test\DiagnosticsHub.InstrumentationCollector.dll" - ) - foreach ($fileToDelete in $filesToDelete) { - if (Test-Path -Path $fileToDelete) { - Remove-Item -Path $fileToDelete | Out-Null - } + $fileToDelete = "$outdir\etwtrace\test\DiagnosticsHub.InstrumentationCollector.dll" + if (Test-Path -Path $fileToDelete) { + Remove-Item -Path $fileToDelete | Out-Null } } finally { diff --git a/Python/Product/Profiling/Profiling.csproj b/Python/Product/Profiling/Profiling.csproj index ea389f40bd..133b8f4c98 100644 --- a/Python/Product/Profiling/Profiling.csproj +++ b/Python/Product/Profiling/Profiling.csproj @@ -200,15 +200,8 @@ etwtrace\%(RecursiveDir) PreserveNewest - - - true - etwtrace_legacy\etwtrace\%(RecursiveDir)%(Filename)%(Extension) - etwtrace_legacy\etwtrace\%(RecursiveDir) - PreserveNewest - - - + + diff --git a/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs b/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs index e89e2a0469..2339553046 100644 --- a/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs +++ b/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs @@ -16,10 +16,13 @@ namespace Microsoft.PythonTools.Profiling { using System; + using System.ComponentModel; using System.ComponentModel.Composition; using System.Diagnostics; + using System.Linq; using System.Threading.Tasks; using System.Windows; + using Microsoft.PythonTools.Infrastructure; using Microsoft.VisualStudio.Shell; /// @@ -28,6 +31,10 @@ namespace Microsoft.PythonTools.Profiling { [Export(typeof(IPythonProfilerCommandService))] [PartCreationPolicy(CreationPolicy.Shared)] public sealed class PythonProfilerCommandService : IPythonProfilerCommandService { + private const int MinimumSupportedPythonMinorVersion = 12; + private const int MaximumSupportedPythonMinorVersion = 14; + private static readonly TimeSpan InterpreterVersionTimeout = TimeSpan.FromSeconds(10); + private readonly CommandArgumentBuilder _commandArgumentBuilder; private readonly IServiceProvider _serviceProvider; private readonly UserInputDialog _userInputDialog; @@ -54,7 +61,38 @@ public async Task GetCommandArgsFromUserInput() { if (_userInputDialog.ShowDialog(targetView, _serviceProvider)) { var target = targetView.GetTarget(); - return _commandArgumentBuilder.BuildCommandArgsFromTarget(target, _serviceProvider); + var commandArgs = _commandArgumentBuilder.BuildCommandArgsFromTarget(target, _serviceProvider); + if (commandArgs == null) { + return null; + } + + var pythonVersion = await GetPythonVersionAsync(commandArgs.PythonExePath); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (pythonVersion == null) { + MessageBox.Show( + Strings.ProfilingInterpreterVersionUnavailable.FormatUI(commandArgs.PythonExePath), + Strings.ProductTitle, + MessageBoxButton.OK, + MessageBoxImage.Error + ); + return null; + } + + if (!IsSupportedPythonVersion(pythonVersion)) { + MessageBox.Show( + Strings.ProfilingUnsupportedPythonVersion.FormatUI( + pythonVersion.Major, + pythonVersion.Minor + ), + Strings.ProductTitle, + MessageBoxButton.OK, + MessageBoxImage.Warning + ); + return null; + } + + return commandArgs; } } catch (Exception ex) { Debug.Fail($"Error displaying user input dialog: {ex.Message}"); @@ -63,5 +101,39 @@ public async Task GetCommandArgsFromUserInput() { return null; } + + private static bool IsSupportedPythonVersion(Version version) { + return version.Major == 3 && + version.Minor >= MinimumSupportedPythonMinorVersion && + version.Minor <= MaximumSupportedPythonMinorVersion; + } + + private static async Task GetPythonVersionAsync(string interpreterPath) { + try { + using (var output = ProcessOutput.RunHiddenAndCapture( + interpreterPath, + "-c", + "import sys; print('{}.{}'.format(sys.version_info[0], sys.version_info[1]))" + )) { + var exited = await Task.Run(() => output.Wait(InterpreterVersionTimeout)); + if (!exited) { + output.Kill(); + return null; + } + + Version version; + var versionText = output.ExitCode == 0 + ? output.StandardOutputLines.FirstOrDefault() + : null; + return Version.TryParse(versionText, out version) ? version : null; + } + } catch (Win32Exception ex) { + Debug.WriteLine($"Failed to query Python version: {ex.Message}"); + return null; + } catch (InvalidOperationException ex) { + Debug.WriteLine($"Failed to query Python version: {ex.Message}"); + return null; + } + } } } diff --git a/Python/Product/Profiling/Strings.Designer.cs b/Python/Product/Profiling/Strings.Designer.cs index c26b7697a1..df0a3b37dd 100644 --- a/Python/Product/Profiling/Strings.Designer.cs +++ b/Python/Product/Profiling/Strings.Designer.cs @@ -469,6 +469,15 @@ public static string ProductTitle { } } + /// + /// Looks up a localized string similar to Visual Studio could not determine the version of the selected Python interpreter '{0}'. Verify that it is a valid Python interpreter and try again.. + /// + public static string ProfilingInterpreterVersionUnavailable { + get { + return ResourceManager.GetString("ProfilingInterpreterVersionUnavailable", resourceCulture); + } + } + /// /// Looks up a localized string similar to Profiling session has not been configured. Configure now and launch?. /// @@ -487,6 +496,15 @@ public static string ProfilingSupportMissingError { } } + /// + /// Looks up a localized string similar to Python {0}.{1} is not supported for profiling. Visual Studio Python profiling supports Python 3.12 through 3.14. Select a supported interpreter and try again.. + /// + public static string ProfilingUnsupportedPythonVersion { + get { + return ResourceManager.GetString("ProfilingUnsupportedPythonVersion", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not find interpreter for project {0}. /// diff --git a/Python/Product/Profiling/Strings.resx b/Python/Product/Profiling/Strings.resx index dd075bd607..2dac25f2a5 100644 --- a/Python/Product/Profiling/Strings.resx +++ b/Python/Product/Profiling/Strings.resx @@ -197,6 +197,12 @@ Error: Profiling support seems to be missing or corrupt. Try repairing your Visual Studio installation. + + Visual Studio could not determine the version of the selected Python interpreter '{0}'. Verify that it is a valid Python interpreter and try again. + + + Python {0}.{1} is not supported for profiling. Visual Studio Python profiling supports Python 3.12 through 3.14. Select a supported interpreter and try again. + Profiling session has not been configured. Configure now and launch? diff --git a/Python/Product/Profiling/diaghub_profile.py b/Python/Product/Profiling/diaghub_profile.py index 99549f811a..38f71f6044 100644 --- a/Python/Product/Profiling/diaghub_profile.py +++ b/Python/Product/Profiling/diaghub_profile.py @@ -12,35 +12,8 @@ _TARGET_ARGUMENTS = "PTVS_DIAGHUB_TARGET_ARGUMENTS" -def _load_legacy_collector(): - import ctypes - - collector_root = os.getenv("DIAGHUB_INSTR_COLLECTOR_ROOT") - runtime_name = os.getenv("DIAGHUB_INSTR_RUNTIME_NAME") - if not collector_root or not runtime_name: - raise RuntimeError( - "Python profiling must be launched from the Visual Studio " - "Performance Profiler." - ) - - if sys.winver.endswith("-32"): - architecture = "x86" - elif sys.winver.endswith("-arm64"): - architecture = "arm64" - else: - architecture = "amd64" - - collector_path = os.path.join(collector_root, architecture, runtime_name) - collector = ctypes.WinDLL(collector_path) - collector.ChildAttach.argtypes = [] - collector.ChildAttach.restype = ctypes.c_int - if not collector.ChildAttach(): - raise RuntimeError("Failed to attach Python to the profiling session.") - return collector - - def _is_supported_version(version): - return (3, 9) <= tuple(version[:2]) <= (3, 14) + return (3, 12) <= tuple(version[:2]) <= (3, 14) def _has_valid_target(arguments): @@ -82,7 +55,7 @@ def _run_target(arguments): def main(): if not _is_supported_version(sys.version_info): print( - "Visual Studio Python profiling supports Python 3.9 through 3.14.", + "Visual Studio Python profiling supports Python 3.12 through 3.14.", file=sys.stderr, ) return 2 @@ -96,16 +69,9 @@ def main(): if not is_child: return _run_profiled_child(arguments) - package_root = os.path.dirname(os.path.abspath(__file__)) - is_legacy = sys.version_info[:2] <= (3, 11) - if is_legacy: - package_root = os.path.join(package_root, "etwtrace_legacy") - sys.path.insert(0, package_root) + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: - # etwtrace 0.1b8 expects Visual Studio to load and attach the collector. - # Keep this reference alive while the extension uses the collector. - collector = _load_legacy_collector() if is_legacy else None from etwtrace import DiagnosticsHubTracer tracer = DiagnosticsHubTracer() except (ImportError, OSError, RuntimeError, AttributeError) as exc: @@ -122,14 +88,7 @@ def main(): arguments[0] = os.path.abspath(arguments[0]) sys.path[0] = os.path.dirname(arguments[0]) - if not hasattr(tracer, "_data"): - # etwtrace 0.1b8 only initializes this field for its test collector. - tracer._data = [] tracer.ignore(os.path.abspath(__file__)) - # Keep profiling and the compatibility collector alive until interpreter - # shutdown. Worker threads may retain native profile callbacks after the - # top-level script returns. - tracer._collector_keepalive = collector tracer.enable() _run_target(arguments) return 0 From ab31f67e0a86eba82907a3f8fb584fe19b275f06 Mon Sep 17 00:00:00 2001 From: Stella Huang <100439259+StellaHuang95@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:25:47 -0700 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Profiling/Profiling/PythonProfilerCommandService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs b/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs index 2339553046..babcd074ab 100644 --- a/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs +++ b/Python/Product/Profiling/Profiling/PythonProfilerCommandService.cs @@ -127,6 +127,9 @@ private static async Task GetPythonVersionAsync(string interpreterPath) : null; return Version.TryParse(versionText, out version) ? version : null; } + } catch (ArgumentException ex) { + Debug.WriteLine($"Failed to query Python version: {ex.Message}"); + return null; } catch (Win32Exception ex) { Debug.WriteLine($"Failed to query Python version: {ex.Message}"); return null;