Skip to content

Commit 12423cf

Browse files
authored
Rollback changes to ExecuteCommand and ExecuteCommandMonitor. Add script package execution profile to source for SDK support. (#672)
1 parent 7953f8d commit 12423cf

23 files changed

Lines changed: 885 additions & 160 deletions

src/VirtualClient/VirtualClient.Contracts.UnitTests/PlatformSpecificsTests.cs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,145 @@ public void FundamentalPathsMatchExpectedPathsWhenUseUnixStylePathsOnlyIsUsed()
5252
Assert.AreEqual($"{currentDirectory}/state", platformSpecifics.StateDirectory);
5353
}
5454

55+
[Test]
56+
[Platform(Exclude = "Unix,Linux,MacOsX")]
57+
[TestCase("execute", "execute")]
58+
[TestCase("execute --option-a=one --option-b=two --option_c=three", "execute")]
59+
[TestCase("execute --option-a one --option-b \"two\" --option_c 'three'", "execute")]
60+
[TestCase("execute.exe", "execute")]
61+
[TestCase("execute.exe --option-a=one --option-b=two --option_c=three", "execute")]
62+
[TestCase("execute.exe --option-a one --option-b \"two\" --option_c 'three'", "execute")]
63+
[TestCase("./execute", "execute")]
64+
[TestCase("../execute --option-a=one --option-b=two --option_c=three", "execute")]
65+
[TestCase("/home/user/tools/execute --option-a one --option-b \"two\" --option_c 'three'", "execute")]
66+
[TestCase(".\\execute.exe", "execute")]
67+
[TestCase(".\\execute.exe --option-a=one --option-b=two --option_c=three", "execute")]
68+
[TestCase("C:\\Users\\User\\tools\\execute.exe --option-a one --option-b \"two\" --option_c 'three'", "execute")]
69+
public void GetCommmandNameSupportsTheExpectedRangeOfCommands(string commandLine, string expectedCommandName)
70+
{
71+
Assert.IsTrue(PlatformSpecifics.TryGetCommandName(commandLine, out string actualCommandName), $"Failed: {commandLine}");
72+
Assert.AreEqual(expectedCommandName, actualCommandName, $"Failed: {commandLine}");
73+
}
74+
75+
[Test]
76+
[Platform(Exclude = "Unix,Linux,MacOsX")]
77+
[TestCase("bash -c \"hostnamectl\"", "hostnamectl")]
78+
[TestCase("bash -c \"execute.sh\"", "execute")]
79+
[TestCase("bash -c \"execute.sh one two three\"", "execute")]
80+
[TestCase("bash -c \"execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
81+
[TestCase("bash -c \"./execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
82+
[TestCase("bash -c \"../execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
83+
[TestCase("bash -c \"/home/user/tools/execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
84+
[TestCase("bash -c \"sudo execute.sh\"", "execute")]
85+
[TestCase("bash -c \"sudo execute.sh one two three\"", "execute")]
86+
[TestCase("bash -c \"sudo execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
87+
[TestCase("bash -c \"sudo ./execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
88+
[TestCase("bash -c \"sudo ../execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
89+
[TestCase("bash -c \"sudo /home/user/tools/execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
90+
[TestCase("sudo bash -c \"execute.sh\"", "execute")]
91+
[TestCase("sudo bash -c \"execute.sh one two three\"", "execute")]
92+
[TestCase("sudo bash -c \"execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
93+
[TestCase("sudo bash -c \"./execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
94+
[TestCase("sudo bash -c \"../execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
95+
[TestCase("sudo bash -c \"/home/user/tools/execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
96+
[TestCase("sudo bash -c \"/home/user/tools/bash/execute.sh --option-a=one --option-b=two --option_c=three\"", "execute")]
97+
public void GetCommmandNameSupportsTheExpectedRangeOfBashCommands(string commandLine, string expectedCommandName)
98+
{
99+
Assert.IsTrue(PlatformSpecifics.TryGetCommandName(commandLine, out string actualCommandName), $"Failed: {commandLine}");
100+
Assert.AreEqual(expectedCommandName, actualCommandName, $"Failed: {commandLine}");
101+
}
102+
103+
[Test]
104+
[Platform(Exclude = "Unix,Linux,MacOsX")]
105+
[TestCase("cmd /C \"execute.exe\"", "execute")]
106+
[TestCase("cmd /C \"execute.exe one two three\"", "execute")]
107+
[TestCase("cmd /C \"execute.exe --option-a=one --option-b=two --option_c=three\"", "execute")]
108+
[TestCase("cmd /C \"execute.cmd\"", "execute")]
109+
[TestCase("cmd /C \"execute.cmd one two three\"", "execute")]
110+
[TestCase("cmd /C \"execute.cmd --option-a=one --option-b=two --option_c=three\"", "execute")]
111+
[TestCase("cmd /C \"execute.bat\"", "execute")]
112+
[TestCase("cmd /C \"execute.bat one two three\"", "execute")]
113+
[TestCase("cmd /C \"execute.bat --option-a=one --option-b=two --option_c=three\"", "execute")]
114+
[TestCase("cmd.exe /C \"execute.exe\"", "execute")]
115+
[TestCase("cmd.exe /C \"execute.exe one two three\"", "execute")]
116+
[TestCase("cmd.exe /C \"execute.exe --option-a=one --option-b=two --option_c=three\"", "execute")]
117+
[TestCase("cmd.exe /C \"execute.cmd\"", "execute")]
118+
[TestCase("cmd.exe /C \"execute.cmd one two three\"", "execute")]
119+
[TestCase("cmd.exe /C \"execute.cmd --option-a=one --option-b=two --option_c=three\"", "execute")]
120+
[TestCase("cmd.exe /C \"execute.bat\"", "execute")]
121+
[TestCase("cmd.exe /C \"execute.bat one two three\"", "execute")]
122+
[TestCase("cmd.exe /C \"execute.bat --option-a=one --option-b=two --option_c=three\"", "execute")]
123+
[TestCase("cmd /C \".\\execute.bat --option-a=one --option-b=two --option_c=three\"", "execute")]
124+
[TestCase("cmd /C \"..\\execute.bat --option-a=one --option-b=two --option_c=three\"", "execute")]
125+
[TestCase("cmd /C \"C:\\Users\\User\\tools\\execute.bat --option-a=one --option-b=two --option_c=three\"", "execute")]
126+
[TestCase("cmd /C \"C:\\Users\\User\\tools\\cmd\\execute.bat --option-a=one --option-b=two --option_c=three\"", "execute")]
127+
public void GetCommmandNameSupportsTheExpectedRangeOfCmdCommands(string commandLine, string expectedCommandName)
128+
{
129+
Assert.IsTrue(PlatformSpecifics.TryGetCommandName(commandLine, out string actualCommandName), $"Failed: {commandLine}");
130+
Assert.AreEqual(expectedCommandName, actualCommandName, $"Failed: {commandLine}");
131+
}
132+
133+
[Test]
134+
[Platform(Exclude = "Unix,Linux,MacOsX")]
135+
[TestCase("Invoke-This.ps1", "Invoke-This")]
136+
[TestCase("Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three", "Invoke-This")]
137+
[TestCase("pwsh Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three", "Invoke-This")]
138+
[TestCase("pwsh.exe Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three", "Invoke-This")]
139+
[TestCase("pwsh \"Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three\"", "Invoke-This")]
140+
[TestCase("pwsh.exe \"Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three\"", "Invoke-This")]
141+
[TestCase("pwsh -C \"Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three\"", "Invoke-This")]
142+
[TestCase("pwsh.exe -C \"Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three\"", "Invoke-This")]
143+
[TestCase("pwsh -Command \"Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three\"", "Invoke-This")]
144+
[TestCase("pwsh.exe -Command \"Invoke-This.ps1 -OptionA one -OptionB=two -OptionC three\"", "Invoke-This")]
145+
[TestCase("pwsh -Command \"Import-Module AnyModule.psd1;Invoke-This -OptionA one -OptionB=two -OptionC three\"", "pwsh")]
146+
[TestCase("pwsh.exe -Command \"Import-Module AnyModule.psm1;Invoke-This -OptionA one -OptionB=two -OptionC three\"", "pwsh")]
147+
[TestCase("powershell -Command \"Import-Module AnyModule.psd1 && Invoke-This -OptionA one -OptionB=two -OptionC three\"", "powershell")]
148+
[TestCase("powershell.exe -Command \"Import-Module AnyModule.psm1 && Invoke-This -OptionA one -OptionB=two -OptionC three\"", "powershell")]
149+
[TestCase("C:\\ProgramFiles\\PowerShell7\\pwsh.exe -Command \"Import-Module AnyModule.psm1;Invoke-This -OptionA one -OptionB=two -OptionC three\"", "pwsh")]
150+
[TestCase("/home/user/powershell7.5.4/pwsh -Command \"Import-Module AnyModule.psd1;Invoke-This -OptionA one -OptionB=two -OptionC three\"", "pwsh")]
151+
public void GetCommmandNameSupportsTheExpectedRangeOfPowerShellCommands(string commandLine, string expectedCommandName)
152+
{
153+
Assert.IsTrue(PlatformSpecifics.TryGetCommandName(commandLine, out string actualCommandName), $"Failed: {commandLine}");
154+
Assert.AreEqual(expectedCommandName, actualCommandName, $"Failed: {commandLine}");
155+
}
156+
157+
[Test]
158+
[Platform(Exclude = "Unix,Linux,MacOsX")]
159+
[TestCase("execute.py --option-a=one --option-b=two --option_c=three", "execute")]
160+
[TestCase("py \"execute.py --option-a=one --option-b=two --option_c=three\"", "execute")]
161+
[TestCase("py.exe \"execute.py --option-a=one --option-b=two --option_c=three\"", "execute")]
162+
[TestCase("python \"execute.py --option-a=one --option-b=two --option_c=three\"", "execute")]
163+
[TestCase("python.exe \"execute.py --option-a=one --option-b=two --option_c=three\"", "execute")]
164+
[TestCase("python3 \"execute.py --option-a=one --option-b=two --option_c=three\"", "execute")]
165+
[TestCase("python3.exe \"execute.py --option-a=one --option-b=two --option_c=three\"", "execute")]
166+
[TestCase("py -c \"import sys; print(sys.version)\"", "py")]
167+
[TestCase("py -c \"print('Hello from inline Python')\"", "py")]
168+
[TestCase("python3 -c \"print('Hello from inline Python')\"", "python3")]
169+
[TestCase("python3 -c \"import sys; print(sys.version)\"", "python3")]
170+
[TestCase("python.exe -c \"print('Hello from inline Python')\"", "python")]
171+
[TestCase("python.exe -c \"import sys; print(sys.version)\"", "python")]
172+
[TestCase("/home/user/python/python3 -c \"print('Hello from inline Python')\"", "python3")]
173+
[TestCase("C:\\ProgramFiles\\Python3\\python.exe -c \"import sys; print(sys.version)\"", "python")]
174+
public void GetCommmandNameSupportsTheExpectedRangeOfPythonCommands(string commandLine, string expectedCommandName)
175+
{
176+
Assert.IsTrue(PlatformSpecifics.TryGetCommandName(commandLine, out string actualCommandName), $"Failed: {commandLine}");
177+
Assert.AreEqual(expectedCommandName, actualCommandName, $"Failed: {commandLine}");
178+
}
179+
180+
[Test]
181+
[Platform(Exclude = "Unix,Linux,MacOsX")]
182+
[TestCase("anycommand /NOTTHECOMMAND --option-a=one --option-b=two --option_c=three", "anycommand")]
183+
[TestCase("anycommand.exe /NOT_THE_COMMAND --option-a=one --option-b=two --option_c=three", "anycommand")]
184+
[TestCase("./anycommand /NOTTHECOMMAND --option-a=one --option-b=two --option_c=three", "anycommand")]
185+
[TestCase(".\\anycommand /NOTTHECOMMAND --option-a=one --option-b=two --option_c=three", "anycommand")]
186+
[TestCase("/home/user/tools/anycommand /NOTTHECOMMAND --option-a=one --option-b=two --option_c=three", "anycommand")]
187+
[TestCase("C:\\Users\\User\\tools\\anycommand /NOTTHECOMMAND --option-a=one --option-b=two --option_c=three", "anycommand")]
188+
public void GetCommmandNameSupportsCommandAnomalies(string commandLine, string expectedCommandName)
189+
{
190+
Assert.IsTrue(PlatformSpecifics.TryGetCommandName(commandLine, out string actualCommandName), $"Failed: {commandLine}");
191+
Assert.AreEqual(expectedCommandName, actualCommandName, $"Failed: {commandLine}");
192+
}
193+
55194
[Test]
56195
public void GetPackagePathReturnsTheExpectedPathOnUnixSystems()
57196
{

src/VirtualClient/VirtualClient.Contracts/ISystemInfo.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public interface ISystemInfo
1919
/// </summary>
2020
string AgentId { get; }
2121

22+
/// <summary>
23+
/// The name of the execution system launching the application.
24+
/// </summary>
25+
string ExecutionSystem { get; }
26+
2227
/// <summary>
2328
/// The ID of the larger experiment in operation.
2429
/// </summary>

src/VirtualClient/VirtualClient.Contracts/PlatformSpecifics.cs

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ namespace VirtualClient.Contracts
99
using System.Linq;
1010
using System.Runtime.InteropServices;
1111
using System.Text.RegularExpressions;
12-
using Newtonsoft.Json.Linq;
1312
using VirtualClient.Common;
1413
using VirtualClient.Common.Extensions;
15-
using YamlDotNet.Core.Tokens;
1614

1715
/// <summary>
1816
/// Defines platform-specific information and properties for dependencies and
@@ -45,6 +43,29 @@ public class PlatformSpecifics
4543
/// </summary>
4644
public static readonly Regex RelativePathExpression = new Regex(@"\.{1,}[\\\/]{1,2}[\x21\x23-\x7E]*", RegexOptions.Compiled);
4745

46+
/// <summary>
47+
/// When determining the name of the command, we want to exclude certain terms
48+
/// that define the hosting/terminal environment (e.g. pwsh, python).
49+
/// </summary>
50+
/// <remarks>
51+
/// Examples:
52+
/// pwsh S:\any\Script.ps1 -> Script
53+
/// pwsh -Command S:\any\Script.ps1 -> Script
54+
/// </remarks>
55+
private static readonly Regex CommandTerminalExpression = new Regex(
56+
@"^bash|^cmd|^pwsh|^powershell|^python|^python3|^py|^-[a-z-_]|[\(\)]+",
57+
RegexOptions.IgnoreCase | RegexOptions.Compiled);
58+
59+
private static readonly Regex GenericCommandExpression = new Regex(
60+
@"(py|python|python3)(?:\.exe)*\s+-c|(pwsh|powershell)(?:\.exe)*\s+.+Import-Module",
61+
RegexOptions.IgnoreCase | RegexOptions.Compiled);
62+
63+
private static readonly Regex ValidFileNameExpression = new Regex(
64+
@"^(?!(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.[^.]*)?$)[^<>:""/\\|?*\x00-\x1F]*[^<>:""/\\|?*\x00-\x1F .]$",
65+
RegexOptions.IgnoreCase | RegexOptions.Compiled);
66+
67+
private static readonly char[] CommandTrimChars = new char[] { ' ', '"', '\'', '/' };
68+
4869
/// <summary>
4970
/// Initializes a new version of the <see cref="PlatformSpecifics"/> class.
5071
/// </summary>
@@ -364,6 +385,125 @@ public static void ThrowIfNotSupported(Architecture architecture)
364385
}
365386
}
366387

388+
/// <summary>
389+
/// Returns the name of the command being executed.
390+
/// </summary>
391+
/// <param name="commandArguments">The command line arguments.</param>
392+
/// <param name="commandName">The name of the command.</param>
393+
public static bool TryGetCommandName(string commandArguments, out string commandName)
394+
{
395+
commandName = null;
396+
397+
// It is difficult to determine a random command from PowerShell or Python. We default to the terminal name here.
398+
Match genericCommand = PlatformSpecifics.GenericCommandExpression.Match(commandArguments);
399+
if (genericCommand.Success)
400+
{
401+
for (int group = 1; group < genericCommand.Groups.Count; group++)
402+
{
403+
if (!string.IsNullOrWhiteSpace(genericCommand.Groups[group].Value))
404+
{
405+
commandName = genericCommand.Groups[group].Value.Trim();
406+
break;
407+
}
408+
}
409+
}
410+
else
411+
{
412+
string[] commandLineArguments = commandArguments.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
413+
foreach (string argument in commandLineArguments)
414+
{
415+
string normalizedArgument = argument?.Trim(PlatformSpecifics.CommandTrimChars);
416+
if (normalizedArgument?.Length > 1)
417+
{
418+
if (string.Equals(normalizedArgument, "sudo"))
419+
{
420+
continue;
421+
}
422+
423+
// Default command name is the terminal itself (e.g. pwsh, python, cmd, bash).
424+
Match terminalMatch = PlatformSpecifics.CommandTerminalExpression.Match(normalizedArgument);
425+
if (terminalMatch.Success)
426+
{
427+
if (commandName == null)
428+
{
429+
commandName = Path.GetFileNameWithoutExtension(terminalMatch.Value.Trim());
430+
}
431+
432+
continue;
433+
}
434+
435+
// Find the first argument that is not a shell/terminal or command line option. We must also
436+
// account for scenarios where we are referencing a script inline on the command line
437+
// (e.g. python.exe -c "print('Hello from inline Python')" ).
438+
string fileName = Path.GetFileNameWithoutExtension(normalizedArgument);
439+
if (!PlatformSpecifics.ValidFileNameExpression.IsMatch(fileName))
440+
{
441+
continue;
442+
}
443+
444+
commandName = fileName;
445+
break;
446+
}
447+
}
448+
}
449+
450+
return !string.IsNullOrWhiteSpace(commandName);
451+
}
452+
453+
/// <summary>
454+
/// Returns the name of the command being executed.
455+
/// </summary>
456+
/// <param name="commandArguments">The command line arguments.</param>
457+
/// <param name="commandName">The name of the command.</param>
458+
public static bool TryGetCommandName(string[] commandArguments, out string commandName)
459+
{
460+
commandName = null;
461+
462+
foreach (string argument in commandArguments)
463+
{
464+
string normalizedArgument = argument?.Trim(PlatformSpecifics.CommandTrimChars);
465+
if (normalizedArgument?.Length > 1)
466+
{
467+
if (string.Equals(normalizedArgument, "sudo"))
468+
{
469+
continue;
470+
}
471+
472+
// It is difficult to determine a random command from Python. We default to the terminal name here.
473+
if (Regex.IsMatch(normalizedArgument, "py -c|py.exe -c|python -c|python.exe -c|python3 -c|python3.exe -c", RegexOptions.IgnoreCase))
474+
{
475+
commandName = "python";
476+
break;
477+
}
478+
479+
// It is difficult to determine a random command from PowerShell. We default to the terminal name here.
480+
if (Regex.IsMatch(normalizedArgument, "Import-Module[^;&&]+(.+)", RegexOptions.IgnoreCase))
481+
{
482+
commandName = "python";
483+
break;
484+
}
485+
486+
// Default command name is the terminal itself (e.g. pwsh, python, cmd, bash).
487+
Match terminalMatch = PlatformSpecifics.CommandTerminalExpression.Match(normalizedArgument);
488+
if (terminalMatch.Success)
489+
{
490+
if (commandName == null)
491+
{
492+
commandName = Path.GetFileNameWithoutExtension(terminalMatch.Value.Trim());
493+
}
494+
495+
continue;
496+
}
497+
498+
// Find the first argument that is not a shell/terminal or command line option.
499+
commandName = Path.GetFileNameWithoutExtension(normalizedArgument);
500+
break;
501+
}
502+
}
503+
504+
return !string.IsNullOrWhiteSpace(commandName);
505+
}
506+
367507
/// <summary>
368508
/// Returns true if the command parts can be determined and outputs the parts.
369509
/// </summary>

0 commit comments

Comments
 (0)