Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1e499d4
Add `exec` function for bash compatibility
SteveL-MSFT Nov 13, 2021
8895262
add tests
SteveL-MSFT Nov 13, 2021
a884a63
change implementation to call execv()
SteveL-MSFT Nov 20, 2021
ecf5c1c
fix so that alias also only shows up on Unix
SteveL-MSFT Nov 20, 2021
ac14ffa
Update src/System.Management.Automation/engine/Modules/SwitchProcessC…
SteveL-MSFT Nov 20, 2021
22a9a54
Update src/System.Management.Automation/engine/Modules/SwitchProcessC…
SteveL-MSFT Nov 20, 2021
8f8e2a9
address Ilya's feedback and added test to verify process got switched
SteveL-MSFT Nov 20, 2021
cdf81d6
change test to use string instead of array
SteveL-MSFT Nov 20, 2021
25ee498
change first arg to just be program name instead of path
SteveL-MSFT Nov 20, 2021
9d5829a
Fix Windows tests
SteveL-MSFT Nov 20, 2021
e235abd
skip help test for Switch-Process as the content doesn't exist
SteveL-MSFT Nov 20, 2021
eb84151
add tracing to see why test is failing in CI
SteveL-MSFT Nov 20, 2021
a233116
fix test by only selecting first instance of sleep command
SteveL-MSFT Nov 20, 2021
c44b5c7
Update src/System.Management.Automation/engine/Modules/SwitchProcessC…
SteveL-MSFT Nov 21, 2021
1064ceb
Update src/System.Management.Automation/resources/CommandBaseStrings.…
SteveL-MSFT Nov 22, 2021
f103f0a
Update test/powershell/Modules/Microsoft.PowerShell.Core/Exec.Tests.ps1
SteveL-MSFT Nov 22, 2021
4dedf66
update error message to include the command line
SteveL-MSFT Nov 22, 2021
72814ba
Update src/System.Management.Automation/engine/Modules/SwitchProcessC…
SteveL-MSFT Dec 17, 2021
9e9a088
Update src/System.Management.Automation/resources/CommandBaseStrings.…
SteveL-MSFT Dec 17, 2021
ef166e5
Change to not accept input from pipeline
SteveL-MSFT Jan 4, 2022
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 @@ -26,6 +26,7 @@ public class ExperimentalFeature
internal const string PSRemotingSSHTransportErrorHandling = "PSRemotingSSHTransportErrorHandling";
internal const string PSCleanBlockFeatureName = "PSCleanBlock";
internal const string PSAMSIMethodInvocationLogging = "PSAMSIMethodInvocationLogging";
internal const string PSExecFeatureName = "PSExec";

#endregion

Expand Down Expand Up @@ -138,6 +139,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: PSAMSIMethodInvocationLogging,
description: "Provides AMSI notification of .NET method invocations."),
new ExperimentalFeature(
name: PSExecFeatureName,
description: "Add 'exec' built-in command on Linux and macOS"),
};

EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
Expand Down
18 changes: 17 additions & 1 deletion src/System.Management.Automation/engine/InitialSessionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4634,7 +4634,7 @@ internal static SessionStateAliasEntry[] BuiltInAliases
const ScopedItemOptions ReadOnly_AllScope = ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope;
const ScopedItemOptions ReadOnly = ScopedItemOptions.ReadOnly;

return new SessionStateAliasEntry[] {
var builtInAliases = new List<SessionStateAliasEntry> {
new SessionStateAliasEntry("foreach", "ForEach-Object", string.Empty, ReadOnly_AllScope),
new SessionStateAliasEntry("%", "ForEach-Object", string.Empty, ReadOnly_AllScope),
new SessionStateAliasEntry("where", "Where-Object", string.Empty, ReadOnly_AllScope),
Expand Down Expand Up @@ -4801,6 +4801,15 @@ internal static SessionStateAliasEntry[] BuiltInAliases
// - do not use AllScope - this causes errors in profiles that set this somewhat commonly used alias.
new SessionStateAliasEntry("sls", "Select-String"),
};

#if UNIX
if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSExecFeatureName))
{
builtInAliases.Add(new SessionStateAliasEntry("exec", "Switch-Process"));
}
#endif

return builtInAliases.ToArray();
}
}

Expand Down Expand Up @@ -5439,6 +5448,13 @@ private static void InitializeCoreCmdletsAndProviders(
cmdlets.Add("Get-PSSubsystem", new SessionStateCmdletEntry("Get-PSSubsystem", typeof(Subsystem.GetPSSubsystemCommand), helpFile));
}

#if UNIX
if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSExecFeatureName))
{
cmdlets.Add("Switch-Process", new SessionStateCmdletEntry("Switch-Process", typeof(SwitchProcessCommand), helpFile));
}
#endif

foreach (var val in cmdlets.Values)
{
val.SetPSSnapIn(psSnapInInfo);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable

using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Runtime.InteropServices;

using Dbg = System.Management.Automation.Diagnostics;

#if UNIX

namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// Implements a cmdlet that allows use of execv API.
/// </summary>
[Experimental(ExperimentalFeature.PSExecFeatureName, ExperimentAction.Show)]
[Cmdlet(VerbsCommon.Switch, "Process", HelpUri = "https://go.microsoft.com/fwlink/?linkid=2181448")]
public sealed class SwitchProcessCommand : PSCmdlet
{
/// <summary>
/// Get or set the command and arguments to replace the current pwsh process.
/// </summary>
[Parameter(Position = 0, Mandatory = false, ValueFromRemainingArguments = true)]
public string[] WithCommand { get; set; } = Array.Empty<string>();

/// <summary>
/// Execute the command and arguments
/// </summary>
protected override void EndProcessing()
{
if (WithCommand.Length == 0)
{
return;
}

// execv requires command to be full path so resolve command to first match
var command = this.SessionState.InvokeCommand.GetCommand(WithCommand[0], CommandTypes.Application);
if (command is null)
{
ThrowTerminatingError(
new ErrorRecord(
new CommandNotFoundException(
string.Format(
System.Globalization.CultureInfo.InvariantCulture,
CommandBaseStrings.NativeCommandNotFound,
command
)
),
"CommandNotFound",
ErrorCategory.InvalidArgument,
WithCommand
)
);
}

var execArgs = new string?[WithCommand.Length + 1];

// execv convention is the first arg is the program name
execArgs[0] = command.Name;

for (int i = 1; i < WithCommand.Length; i++)
{
execArgs[i] = WithCommand[i];
}

// need null terminator at end
execArgs[execArgs.Length - 1] = null;

int exitCode = Exec(command.Source, execArgs);

if (exitCode < 0)
{
ThrowTerminatingError(
new ErrorRecord(
new Exception(
string.Format(
System.Globalization.CultureInfo.InvariantCulture,
CommandBaseStrings.ExecFailed,
Marshal.GetLastPInvokeError(),
string.Join(" ", WithCommand)
)
),
"ExecutionFailed",
ErrorCategory.InvalidOperation,
WithCommand
)
);
}
}

/// <summary>
/// The `execv` POSIX syscall we use to exec /bin/sh.
/// </summary>
/// <param name="path">The path to the executable to exec.</param>
/// <param name="args">
/// The arguments to send through to the executable.
/// Array must have its final element be null.
/// </param>
/// <returns>
/// An exit code if exec failed, but if successful the calling process will be overwritten.
/// </returns>
[DllImport("libc",
EntryPoint = "execv",
CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Ansi,
SetLastError = true)]
private static extern int Exec(string path, string?[] args);
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,10 @@ Reviewed by TArcher on 2010-07-20
<data name="UseOfDeprecatedCommandWarning" xml:space="preserve">
<value>The {0} is obsolete. {1}</value>
</data>
<data name="ExecFailed" xml:space="preserve">
<value>Exec call failed with errorno {0} for command line: {1}</value>
</data>
<data name="NativeCommandNotFound" xml:space="preserve">
<value>Command '{0}' was not found. The specified command must be an executable.</value>
</data>
</root>
74 changes: 74 additions & 0 deletions test/powershell/Modules/Microsoft.PowerShell.Core/Exec.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Switch-Process tests for Unix' -Tags 'CI' {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if (-not [ExperimentalFeature]::IsEnabled('PSExec') -or $IsWindows)
{
$PSDefaultParameterValues['It:Skip'] = $true
return
}
}

AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}

It 'Exec alias should map to Switch-Process' {
$alias = Get-Command exec
$alias | Should -BeOfType [System.Management.Automation.AliasInfo]
$alias.Definition | Should -BeExactly 'Switch-Process'
}

It 'Exec by itself does nothing' {
exec | Should -BeNullOrEmpty
}

It 'Exec given a cmdlet should fail' {
{ exec Get-Command } | Should -Throw -ErrorId 'CommandNotFound,Microsoft.PowerShell.Commands.SwitchProcessCommand'
}

It 'Exec given an exe should work' {
$id, $uname = pwsh -noprofile -noexit -outputformat text -command { $pid; exec uname }
Get-Process -Id $id -ErrorAction Ignore| Should -BeNullOrEmpty
$uname | Should -BeExactly (uname)
}

It 'Exec given an exe and arguments should work' {
$id, $uname = pwsh -noprofile -noexit -outputformat text -command { $pid; exec uname -a }
Get-Process -Id $id -ErrorAction Ignore| Should -BeNullOrEmpty
$uname | Should -BeExactly (uname -a)
}

It 'Exec will replace the process' {
$sleep = Get-Command sleep -CommandType Application | Select-Object -First 1
$p = Start-Process pwsh -ArgumentList "-noprofile -command exec $($sleep.Source) 90" -PassThru
Wait-UntilTrue {
($p | Get-Process).Name -eq 'sleep'
} -timeout 60000 -interval 100 | Should -BeTrue
}
}

Describe 'Switch-Process for Windows' {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if (-not $IsWindows)
{
$PSDefaultParameterValues['It:Skip'] = $true
return
}
}

AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}

It 'Switch-Process should not be available' {
Get-Command -Name Switch-Process -ErrorAction Ignore | Should -BeNullOrEmpty
}

It 'Exec alias should not be available' {
Get-Alias -Name exec -ErrorAction Ignore | Should -BeNullOrEmpty
}
}
3 changes: 2 additions & 1 deletion test/powershell/engine/Help/HelpSystem.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ $script:cmdletsToSkip = @(
"Get-ExperimentalFeature",
"Enable-ExperimentalFeature",
"Disable-ExperimentalFeature",
"Get-PSSubsystem"
"Get-PSSubsystem",
"Switch-Process"
)

function UpdateHelpFromLocalContentPath {
Expand Down