Skip to content

Commit

Permalink
Only use ps 'flags' for macOS (#1412)
Browse files Browse the repository at this point in the history
* Only use ps 'flags' for macOS

This PR adds in an operating system check via uname to determine if we
should run the PS command with the 'flags' flag. This feature was
originally used to determine the architecture of a process for macOS M1
machines that can run as x64 or as arm64.

AD7Process only uses the flags field in macOS so this PR updates the
command sent to only use it on macOS machines.

Other changes:
- Updated the setup.csx script to update VS with VS.list instead of
  VS.Codespaces.list
- Updated tests to use the new PSOutputParser
- Added a macOS test

* Addressing PR issues
  • Loading branch information
WardenGnaw authored Aug 7, 2023
1 parent 3986396 commit 76427bb
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 70 deletions.
4 changes: 2 additions & 2 deletions src/SSHDebugPS/AD7/AD7Process.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal class AD7Process : IDebugProcess2, IDebugProcessSecurity2, IDebugProces
/// <summary>
/// Flags are only used in ps command scenarios. It will be set to 0 for others.
/// </summary>
private readonly uint _flags;
private readonly uint? _flags;

/// <summary>
/// Returns true if _commandLine appears to hold a real file name + args rather than just a description
Expand Down Expand Up @@ -297,7 +297,7 @@ string IDebugUnixProcess.GetProcessArchitecture()
{
// For Apple Silicon M1, it is possible that the process we are attaching to is being emulated as x86_64.
// The process is emulated if it has process flags has P_TRANSLATED (0x20000).
if (_port.IsOSX() && _systemArch == "arm64")
if (_port.IsOSX() && _systemArch == "arm64" && _flags.HasValue)
{
if ((_flags & 0x20000) != 0)
{
Expand Down
30 changes: 27 additions & 3 deletions src/SSHDebugPS/IConnection.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Microsoft.DebugEngineHost;
using Microsoft.SSHDebugPS.Utilities;
Expand Down Expand Up @@ -104,13 +106,13 @@ public class Process
/// <summary>
/// Only used by the PSOutputParser
/// </summary>
public uint Flags { get; private set; }
public uint? Flags { get; private set; }
public string SystemArch { get; private set; }
public string CommandLine { get; private set; }
public string UserName { get; private set; }
public bool IsSameUser { get; private set; }

public Process(uint id, string arch, uint flags, string userName, string commandLine, bool isSameUser)
public Process(uint id, string arch, uint? flags, string userName, string commandLine, bool isSameUser)
{
this.Id = id;
this.Flags = flags;
Expand All @@ -121,15 +123,37 @@ public Process(uint id, string arch, uint flags, string userName, string command
}
}

internal static class OperatingSystemStringConverter
{
internal static PlatformID ConvertToPlatformID(string value)
{
if (!string.IsNullOrEmpty(value))
{
value = value.ToLowerInvariant();
if (value.Contains("darwin"))
{
return PlatformID.MacOSX;
} else if (value.Contains("linux"))
{
return PlatformID.Unix;
}
}
Debug.Fail($"Expected a valid platform '{value}' of darwin or linux, but falling back to linux.");
return PlatformID.Unix;
}
}

internal class SystemInformation
{
public string UserName { get; private set; }
public string Architecture { get; private set; }
public PlatformID Platform { get; private set; }

public SystemInformation(string username, string architecture)
public SystemInformation(string username, string architecture, PlatformID platform)
{
this.UserName = username;
this.Architecture = architecture;
Platform = platform;
}
}
}
48 changes: 28 additions & 20 deletions src/SSHDebugPS/PSOutputParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,27 @@ public string Extract(string line)
// Use padding to expand column width. 10 for pid and 32 for userid as that is the max size for each
// Tested this format with different distributions of Linux and container distributions. This command (and the alternative without the flags) seems
// to be the one that works the best between standard *nix and BusyBox implementations of ps.
private const string PSCommandLineFormat = "ps{0}-o pid=pppppppppp -o flags=ffffffff -o ruser=rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr -o args";
private const string PSCommandLineFormat = "ps{0}-o pid=pppppppppp{1} -o ruser=rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr -o args";
private SystemInformation _currentSystemInformation;
private ColumnDef _pidCol;
private ColumnDef _flagsCol;
private ColumnDef _ruserCol;
private ColumnDef _argsCol;

public static string PSCommandLine = PSCommandLineFormat.FormatInvariantWithArgs(" axww ");
public static string AltPSCommandLine = PSCommandLineFormat.FormatInvariantWithArgs(" ");
// In order to determine the architecture of a process, we need to run the ps command with 'flags'.
// However, certain of distros of Linux do not support flags, so only add this for macOS.
private string PSFlagFormat => _currentSystemInformation.Platform == PlatformID.MacOSX ? " -o flags=ffffffff" : string.Empty;

public static List<Process> Parse(string output, SystemInformation systemInformation)
{
return new PSOutputParser().ParseInternal(output, systemInformation);
}
public string PSCommandLine => PSCommandLineFormat.FormatInvariantWithArgs(" axww ", PSFlagFormat);
public string AltPSCommandLine => PSCommandLineFormat.FormatInvariantWithArgs(" ", PSFlagFormat);

private PSOutputParser()
public PSOutputParser(SystemInformation systemInformation)
{
_currentSystemInformation = systemInformation;
}

private List<Process> ParseInternal(string output, SystemInformation systemInformation)
public List<Process> Parse(string output)
{
_currentSystemInformation = systemInformation;
List<Process> processList = new List<Process>();

using (var reader = new StringReader(output))
Expand Down Expand Up @@ -136,14 +135,18 @@ private bool ProcessHeaderLine(/*OPTIONAL*/ string headerLine)
if (!SkipNonWhitespace(headerLine, ref index))
return false;

_flagsCol = new ColumnDef(colStart, index);
/// <see cref PSFlagFormat/> on why this is only executed for macOS.
if (_currentSystemInformation.Platform == PlatformID.MacOSX)
{
_flagsCol = new ColumnDef(colStart, index);

if (!SkipWhitespace(headerLine, ref index))
return false;
if (!SkipWhitespace(headerLine, ref index))
return false;

colStart = index;
if (!SkipNonWhitespace(headerLine, ref index))
return false;
colStart = index;
if (!SkipNonWhitespace(headerLine, ref index))
return false;
}

_ruserCol = new ColumnDef(colStart, index);

Expand All @@ -170,10 +173,15 @@ private Process SplitPSLine(string line)
if (!uint.TryParse(pidText, NumberStyles.None, CultureInfo.InvariantCulture, out pid))
return null;

uint flags;
string flagsText = _flagsCol.Extract(line);
if (!uint.TryParse(flagsText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out flags))
return null;
uint? flags = null;
/// <see cref PSFlagFormat/> on why this is only executed for macOS.
if (_currentSystemInformation.Platform == PlatformID.MacOSX)
{
string flagsText = _flagsCol.Extract(line);
if (!uint.TryParse(flagsText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint tempFlags))
return null;
flags = tempFlags;
}

string ruser = _ruserCol.Extract(line);
string commandLine = _argsCol.Extract(line);
Expand Down
21 changes: 15 additions & 6 deletions src/SSHDebugPS/PipeConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ public override bool IsLinux()
/// <returns>SystemInformation containing username and architecture. If it was unable to obtain any of these, the value will be set to string.Empty.</returns>
public SystemInformation GetSystemInformation()
{
string commandOutput;
string errorMessage;
string commandOutput = string.Empty;
string errorMessage = string.Empty;
int exitCode;

string username = string.Empty;
Expand All @@ -108,13 +108,19 @@ public SystemInformation GetSystemInformation()
username = commandOutput;
}

string platform = string.Empty;
if (ExecuteCommand("uname", Timeout.Infinite, commandOutput: out commandOutput, errorMessage: out errorMessage, exitCode: out exitCode))
{
platform = commandOutput;
}

string architecture = string.Empty;
if (ExecuteCommand("uname -m", Timeout.Infinite, commandOutput: out commandOutput, errorMessage: out errorMessage, exitCode: out exitCode))
{
architecture = commandOutput;
}

return new SystemInformation(username, architecture);
return new SystemInformation(username, architecture, OperatingSystemStringConverter.ConvertToPlatformID(platform));
}

public override List<Process> ListProcesses()
Expand Down Expand Up @@ -152,12 +158,15 @@ private bool PSListProcess(SystemInformation systemInformation, out string error
errorMessage = string.Empty;
string commandOutput;
int exitCode;
if (!ExecuteCommand(PSOutputParser.PSCommandLine, Timeout.Infinite, out commandOutput, out errorMessage, out exitCode))

PSOutputParser psOutputParser = new PSOutputParser(systemInformation);

if (!ExecuteCommand(psOutputParser.PSCommandLine, Timeout.Infinite, out commandOutput, out errorMessage, out exitCode))
{
// Clear output and errorMessage
commandOutput = string.Empty;
errorMessage = string.Empty;
if (!ExecuteCommand(PSOutputParser.AltPSCommandLine, Timeout.Infinite, out commandOutput, out errorMessage, out exitCode))
if (!ExecuteCommand(psOutputParser.AltPSCommandLine, Timeout.Infinite, out commandOutput, out errorMessage, out exitCode))
{
if (exitCode == 127)
{
Expand All @@ -174,7 +183,7 @@ private bool PSListProcess(SystemInformation systemInformation, out string error
}
}

processes = PSOutputParser.Parse(commandOutput, systemInformation);
processes = psOutputParser.Parse(commandOutput);
return true;
}

Expand Down
15 changes: 12 additions & 3 deletions src/SSHDebugPS/SSH/SSHConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,31 @@ public override List<Process> ListProcesses()
username = usernameCommand.Output.TrimEnd('\n', '\r'); // trim line endings because 'id' command ends with a newline
}

string operatingSystem = string.Empty;
var operatingSystemCommand = _remoteSystem.Shell.ExecuteCommand("uname", Timeout.InfiniteTimeSpan);
if (operatingSystemCommand.ExitCode == 0)
{
operatingSystem = operatingSystemCommand.Output.TrimEnd('\n', '\r'); // trim line endings because 'uname' command ends with a newline
}

string architecture = string.Empty;
var architectureCommand = _remoteSystem.Shell.ExecuteCommand("uname -m", Timeout.InfiniteTimeSpan);
if (architectureCommand.ExitCode == 0)
{
architecture = architectureCommand.Output.TrimEnd('\n', '\r'); // trim line endings because 'uname -m' command ends with a newline
}

SystemInformation systemInformation = new SystemInformation(username, architecture);
SystemInformation systemInformation = new SystemInformation(username, architecture, OperatingSystemStringConverter.ConvertToPlatformID(operatingSystem));

PSOutputParser psOutputParser = new PSOutputParser(systemInformation);

var command = _remoteSystem.Shell.ExecuteCommand(PSOutputParser.PSCommandLine, Timeout.InfiniteTimeSpan);
var command = _remoteSystem.Shell.ExecuteCommand(psOutputParser.PSCommandLine, Timeout.InfiniteTimeSpan);
if (command.ExitCode != 0)
{
throw new CommandFailedException(StringResources.Error_PSFailed);
}

return PSOutputParser.Parse(command.Output, systemInformation);
return psOutputParser.Parse(command.Output);
}

/// <inheritdoc/>
Expand Down
59 changes: 43 additions & 16 deletions src/SSHDebugTests/PSOutputParserTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Microsoft.SSHDebugPS;
using System.Collections.Generic;
using Xunit;
Expand All @@ -9,22 +10,46 @@ namespace SSHDebugTests
{
public class PSOutputParserTests
{
[Fact]
public void PSOutputParser_macOS()
{
const string username = "username";
const string architecture = "x86_64";
const string input =
"pppppppppp ffffffff rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr ARGS\n" +
"1 4004 root /sbin/launchd\n" +
"50 1004004 root /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Support/fseventsd\n" +
"70 1004004 root /System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Support/mds\n" +
"83 4004 _timed /usr/libexec/timed\n" +
"96 80004104 root /System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow console\n" +
"7835 4104 username ps axww -o pid=pppppppppp -o flags=ffffffff -o ruser=rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr -o args\n";

PSOutputParser psOutputParser = new PSOutputParser(new SystemInformation(username, architecture, PlatformID.MacOSX));
List<Process> r = psOutputParser.Parse(input);
Assert.Equal(5, r.Count);
// Testing flags here as PID USER ARGS are tested in the other tests.
Assert.Equal(r[0].Flags.Value, (uint)0x4004);
Assert.Equal(r[1].Flags.Value, (uint)0x1004004);
Assert.Equal(r[4].Flags.Value, (uint)0x80004104);
}

[Fact]
public void PSOutputParser_Ubuntu14()
{
const string username = "greggm";
const string architecture = "x86_64";
// example output from ps on a real Ubuntu 14 machine (with many processes removed):
const string input =
"pppppppppp ffffffff rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr COMMAND\n" +
" 1 0 root /sbin/init\n" +
" 2 0 root [kthreadd]\n" +
" 720 0 message+ dbus-daemon --system --fork\n" +
" 2389 0 greggm -bash\n" +
" 2580 0 root /sbin/dhclient -d -sf /usr/lib/NetworkManager/nm-dhcp-client.action -pf /run/sendsigs.omit.d/network-manager.dhclient-eth0.pid -lf /var/lib/NetworkManager/dhclient-d08a482b-ff90-4007-9b13-6500eb94b673-eth0.lease -cf /var/lib/NetworkManager/dhclient-eth0.conf eth0\n" +
" 2913 0 greggm ps axww -o pid=pppppppppp -o flags=ffffffff -o ruser=rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr -o args\n";
"pppppppppp rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr COMMAND\n" +
" 1 root /sbin/init\n" +
" 2 root [kthreadd]\n" +
" 720 message+ dbus-daemon --system --fork\n" +
" 2389 greggm -bash\n" +
" 2580 root /sbin/dhclient -d -sf /usr/lib/NetworkManager/nm-dhcp-client.action -pf /run/sendsigs.omit.d/network-manager.dhclient-eth0.pid -lf /var/lib/NetworkManager/dhclient-d08a482b-ff90-4007-9b13-6500eb94b673-eth0.lease -cf /var/lib/NetworkManager/dhclient-eth0.conf eth0\n" +
" 2913 greggm ps axww -o pid=pppppppppp -o ruser=rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr -o args\n";

List<Process> r = PSOutputParser.Parse(input, new SystemInformation(username, architecture));
PSOutputParser psOutputParser = new PSOutputParser(new SystemInformation(username, architecture, PlatformID.Unix));
List<Process> r = psOutputParser.Parse(input);
Assert.Equal(5, r.Count);

uint[] pids = { 1, 2, 720, 2389, 2580 };
Expand All @@ -47,10 +72,11 @@ public void PSOutputParser_SmallCol()
const string architecture = "x86_64";
// made up output for what could happen if the fields were all just 1 character in size
const string input =
"A B C D\n" +
"9 0 r /sbin/init";
"A B C\n" +
"9 r /sbin/init";

List<Process> r = PSOutputParser.Parse(input, new SystemInformation(username, architecture));
PSOutputParser psOutputParser = new PSOutputParser(new SystemInformation(username, architecture, PlatformID.Unix));
List<Process> r = psOutputParser.Parse(input);
Assert.Single(r);
Assert.Equal<uint>(9, r[0].Id);
Assert.Equal("r", r[0].UserName);
Expand All @@ -64,12 +90,13 @@ public void PSOutputParser_NoUserName()
const string username = "";
const string architecture = "";
const string input =
"pppppppppp ffffffff rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr COMMAND\n" +
" 1 0 root /sbin/init\n" +
" 720 0 dbus-daemon --system --fork\n" +
" 2389 0 greggm -bash\n";
"pppppppppp rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr COMMAND\n" +
" 1 root /sbin/init\n" +
" 720 dbus-daemon --system --fork\n" +
" 2389 greggm -bash\n";

List<Process> r = PSOutputParser.Parse(input, new SystemInformation(username, architecture));
PSOutputParser psOutputParser = new PSOutputParser(new SystemInformation(username, architecture, PlatformID.Unix));
List<Process> r = psOutputParser.Parse(input);
Assert.Equal(3, r.Count);

uint[] pids = { 1, 720, 2389 };
Expand Down
Loading

0 comments on commit 76427bb

Please sign in to comment.