diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 1af01426fe0..1a727133309 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -204,7 +204,7 @@ internal static int Start( { ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("ServerMode"); ProfileOptimization.StartProfile("StartupProfileData-ServerMode"); - System.Management.Automation.Remoting.Server.OutOfProcessMediator.Run(s_cpp.InitialCommand); + System.Management.Automation.Remoting.Server.OutOfProcessMediator.Run(s_cpp.InitialCommand, s_cpp.WorkingDirectory); exitCode = 0; } else if (s_cpp.NamedPipeServerMode) diff --git a/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs b/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs index 3f82bea8ee2..2ec10fa7705 100644 --- a/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs +++ b/src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs @@ -41,15 +41,26 @@ static PowerShellProcessInstance() } /// + /// Initializes a new instance of the class. Initializes the underlying dotnet process class. /// - /// - /// - /// - /// - public PowerShellProcessInstance(Version powerShellVersion, PSCredential credential, ScriptBlock initializationScript, bool useWow64) + /// Specifies the version of powershell. + /// Specifies a user account credentials. + /// Specifies a script that will be executed when the powershell process is initialized. + /// Specifies if the powershell process will be 32-bit. + /// Specifies the initial working directory for the new powershell process. + public PowerShellProcessInstance(Version powerShellVersion, PSCredential credential, ScriptBlock initializationScript, bool useWow64, string workingDirectory) { string processArguments = " -s -NoLogo -NoProfile"; + if (!string.IsNullOrWhiteSpace(workingDirectory)) + { + processArguments = string.Format( + CultureInfo.InvariantCulture, + "{0} -wd {1}", + processArguments, + workingDirectory); + } + if (initializationScript != null) { string scripBlockAsString = initializationScript.ToString(); @@ -57,8 +68,11 @@ public PowerShellProcessInstance(Version powerShellVersion, PSCredential credent { string encodedCommand = Convert.ToBase64String(Encoding.Unicode.GetBytes(scripBlockAsString)); - processArguments = string.Format(CultureInfo.InvariantCulture, - "{0} -EncodedCommand {1}", processArguments, encodedCommand); + processArguments = string.Format( + CultureInfo.InvariantCulture, + "{0} -EncodedCommand {1}", + processArguments, + encodedCommand); } } @@ -91,8 +105,20 @@ public PowerShellProcessInstance(Version powerShellVersion, PSCredential credent } /// + /// Initializes a new instance of the class. Initializes the underlying dotnet process class. + /// + /// + /// + /// + /// + public PowerShellProcessInstance(Version powerShellVersion, PSCredential credential, ScriptBlock initializationScript, bool useWow64) : this(powerShellVersion, credential, initializationScript, useWow64, workingDirectory: null) + { + } + + /// + /// Initializes a new instance of the class. Default initializes the underlying dotnet process class. /// - public PowerShellProcessInstance() : this(null, null, null, false) + public PowerShellProcessInstance() : this(powerShellVersion: null, credential: null, initializationScript: null, useWow64: false, workingDirectory: null) { } diff --git a/src/System.Management.Automation/engine/remoting/commands/StartJob.cs b/src/System.Management.Automation/engine/remoting/commands/StartJob.cs index 0f5cc64ebe9..615d6e9bd30 100644 --- a/src/System.Management.Automation/engine/remoting/commands/StartJob.cs +++ b/src/System.Management.Automation/engine/remoting/commands/StartJob.cs @@ -480,6 +480,13 @@ public virtual ScriptBlock InitializationScript private ScriptBlock _initScript; + /// + /// Gets or sets an initial working directory for the powershell background job. + /// + [Parameter] + [ValidateNotNullOrEmpty] + public string WorkingDirectory { get; set; } + /// /// Launches the background job as a 32-bit process. This can be used on /// 64-bit systems to launch a 32-bit wow process for the background job. @@ -589,6 +596,18 @@ protected override void BeginProcessing() ThrowTerminatingError(errorRecord); } + if (WorkingDirectory != null && !Directory.Exists(WorkingDirectory)) + { + string message = StringUtil.Format(RemotingErrorIdStrings.StartJobWorkingDirectoryNotFound, WorkingDirectory); + var errorRecord = new ErrorRecord( + new DirectoryNotFoundException(message), + "DirectoryNotFoundException", + ErrorCategory.InvalidOperation, + targetObject: null); + + ThrowTerminatingError(errorRecord); + } + CommandDiscovery.AutoloadModulesWithJobSourceAdapters(this.Context, this.CommandOrigin); if (ParameterSetName == DefinitionNameParameterSet) @@ -628,6 +647,7 @@ protected override void CreateHelpersForSpecifiedComputerNames() connectionInfo.InitializationScript = _initScript; connectionInfo.AuthenticationMechanism = this.Authentication; connectionInfo.PSVersion = this.PSVersion; + connectionInfo.WorkingDirectory = this.WorkingDirectory; RemoteRunspace remoteRunspace = (RemoteRunspace)RunspaceFactory.CreateRunspace(connectionInfo, this.Host, Utils.GetTypeTableFromExecutionContextTLS()); diff --git a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs index 5e7a88c9495..2a3c04eecd9 100644 --- a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs +++ b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs @@ -1514,6 +1514,11 @@ internal sealed class NewProcessConnectionInfo : RunspaceConnectionInfo /// public bool RunAs32 { get; set; } + /// + /// Gets or sets an initial working directory for the powershell background process. + /// + public string WorkingDirectory { get; set; } + /// /// Powershell version to execute the job in. /// @@ -1590,6 +1595,7 @@ public NewProcessConnectionInfo Copy() NewProcessConnectionInfo result = new NewProcessConnectionInfo(_credential); result.AuthenticationMechanism = this.AuthenticationMechanism; result.InitializationScript = this.InitializationScript; + result.WorkingDirectory = this.WorkingDirectory; result.RunAs32 = this.RunAs32; result.PSVersion = this.PSVersion; result.Process = Process; diff --git a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs index 7cb6143fc27..c23b875d5b3 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs @@ -1003,7 +1003,8 @@ internal override void CreateAsync() _processInstance = _connectionInfo.Process ?? new PowerShellProcessInstance(_connectionInfo.PSVersion, _connectionInfo.Credential, _connectionInfo.InitializationScript, - _connectionInfo.RunAs32); + _connectionInfo.RunAs32, + _connectionInfo.WorkingDirectory); if (_connectionInfo.Process != null) { _processCreated = false; diff --git a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs index 5f6746eaeec..70757a2ff4b 100644 --- a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs +++ b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs @@ -87,7 +87,9 @@ protected void ProcessingThreadStart(object state) catch (Exception e) { PSEtwLog.LogOperationalError( - PSEventId.TransportError, PSOpcode.Open, PSTask.None, + PSEventId.TransportError, + PSOpcode.Open, + PSTask.None, PSKeyword.UseAlwaysOperational, Guid.Empty.ToString(), Guid.Empty.ToString(), @@ -96,7 +98,9 @@ protected void ProcessingThreadStart(object state) e.StackTrace); PSEtwLog.LogAnalyticError( - PSEventId.TransportError_Analytic, PSOpcode.Open, PSTask.None, + PSEventId.TransportError_Analytic, + PSOpcode.Open, + PSTask.None, PSKeyword.Transport | PSKeyword.UseAlwaysAnalytic, Guid.Empty.ToString(), Guid.Empty.ToString(), @@ -158,7 +162,9 @@ protected void OnDataPacketReceived(byte[] rawData, string stream, Guid psGuid) protected void OnDataAckPacketReceived(Guid psGuid) { - throw new PSRemotingTransportException(PSRemotingErrorId.IPCUnknownElementReceived, RemotingErrorIdStrings.IPCUnknownElementReceived, + throw new PSRemotingTransportException( + PSRemotingErrorId.IPCUnknownElementReceived, + RemotingErrorIdStrings.IPCUnknownElementReceived, OutOfProcessUtils.PS_OUT_OF_PROC_DATA_ACK_TAG); } @@ -301,33 +307,39 @@ protected void OnCloseAckPacketReceived(Guid psGuid) #region Methods - protected OutOfProcessServerSessionTransportManager CreateSessionTransportManager(string configurationName, PSRemotingCryptoHelperServer cryptoHelper) + protected OutOfProcessServerSessionTransportManager CreateSessionTransportManager(string configurationName, PSRemotingCryptoHelperServer cryptoHelper, string workingDirectory) { PSSenderInfo senderInfo; #if !UNIX WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent(); - PSPrincipal userPrincipal = new PSPrincipal(new PSIdentity(string.Empty, true, currentIdentity.Name, null), + PSPrincipal userPrincipal = new PSPrincipal( + new PSIdentity(string.Empty, true, currentIdentity.Name, null), currentIdentity); senderInfo = new PSSenderInfo(userPrincipal, "http://localhost"); #else - PSPrincipal userPrincipal = new PSPrincipal(new PSIdentity(string.Empty, true, string.Empty, null), + PSPrincipal userPrincipal = new PSPrincipal( + new PSIdentity(string.Empty, true, string.Empty, null), null); senderInfo = new PSSenderInfo(userPrincipal, "http://localhost"); #endif OutOfProcessServerSessionTransportManager tm = new OutOfProcessServerSessionTransportManager(originalStdOut, originalStdErr, cryptoHelper); - ServerRemoteSession srvrRemoteSession = ServerRemoteSession.CreateServerRemoteSession(senderInfo, - _initialCommand, tm, configurationName); + ServerRemoteSession.CreateServerRemoteSession( + senderInfo, + _initialCommand, + tm, + configurationName, + workingDirectory); return tm; } - protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoHelper, string configurationName = null) + protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoHelper, string workingDirectory = null, string configurationName = null) { _initialCommand = initialCommand; - sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper); + sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper, workingDirectory); try { @@ -338,7 +350,7 @@ protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoH { if (sessionTM == null) { - sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper); + sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper, workingDirectory); } } @@ -352,8 +364,10 @@ protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoH sessionTM = null; } - throw new PSRemotingTransportException(PSRemotingErrorId.IPCUnknownElementReceived, - RemotingErrorIdStrings.IPCUnknownElementReceived, string.Empty); + throw new PSRemotingTransportException( + PSRemotingErrorId.IPCUnknownElementReceived, + RemotingErrorIdStrings.IPCUnknownElementReceived, + string.Empty); } // process data in a thread pool thread..this way Runspace, Command @@ -366,7 +380,8 @@ protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoH #else ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessingThreadStart), data); #endif - } while (true); + } + while (true); } catch (Exception e) { @@ -468,8 +483,11 @@ private OutOfProcessMediator() : base(true) #region Static Methods /// + /// Starts the out-of-process powershell server instance. /// - internal static void Run(string initialCommand) + /// Specifies the initialization script. + /// Specifies the initial working directory. The working directory is set before the initial command. + internal static void Run(string initialCommand, string workingDirectory) { lock (SyncObject) { @@ -486,7 +504,7 @@ internal static void Run(string initialCommand) // Setup unhandled exception to log events AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(AppDomainUnhandledException); #endif - s_singletonInstance.Start(initialCommand, new PSRemotingCryptoHelperServer()); + s_singletonInstance.Start(initialCommand, new PSRemotingCryptoHelperServer(), workingDirectory); } #endregion diff --git a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs index c082f0a76c8..b23bf8160f7 100644 --- a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs +++ b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; using System.Management.Automation.Host; using System.Management.Automation.Internal; @@ -86,6 +87,9 @@ private Dictionary _associatedShells // Results in a configured remote runspace pushed onto driver host. private string _configurationName; + // Optional initial location of the PowerShell session + private readonly string _initialLocation; + /// /// Event that get raised when the RunspacePool is closed. /// @@ -120,6 +124,7 @@ private Dictionary _associatedShells /// Server capability reported to the client during negotiation (not the actual capability). /// Client PowerShell version. /// Optional endpoint configuration name to create a pushed configured runspace. + /// Optional initial location of the powershell. internal ServerRunspacePoolDriver( Guid clientRunspacePoolId, int minRunspaces, @@ -134,7 +139,8 @@ internal ServerRunspacePoolDriver( bool isAdministrator, RemoteSessionCapability serverCapability, Version psClientVersion, - string configurationName) + string configurationName, + string initialLocation) { Dbg.Assert(configData != null, "ConfigurationData cannot be null"); @@ -142,11 +148,12 @@ internal ServerRunspacePoolDriver( _clientPSVersion = psClientVersion; _configurationName = configurationName; + _initialLocation = initialLocation; // Create a new server host and associate for host call // integration - _remoteHost = new ServerDriverRemoteHost(clientRunspacePoolId, - Guid.Empty, hostInfo, transportManager, null); + _remoteHost = new ServerDriverRemoteHost( + clientRunspacePoolId, Guid.Empty, hostInfo, transportManager, null); _configData = configData; _applicationPrivateData = applicationPrivateData; @@ -615,6 +622,13 @@ private void HandleRunspaceCreated(object sender, RunspaceCreatedEventArgs args) // to ignore all but critical errors. } + if (!string.IsNullOrWhiteSpace(_initialLocation)) + { + var setLocationCommand = new Command("Set-Location"); + setLocationCommand.Parameters.Add(new CommandParameter("LiteralPath", _initialLocation)); + InvokeScript(setLocationCommand, args); + } + // Run startup scripts InvokeStartupScripts(args); diff --git a/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs b/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs index 004f5fb00e9..f33368a0a1b 100644 --- a/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs +++ b/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs @@ -89,6 +89,9 @@ internal class ServerRemoteSession : RemoteSession // Creates a pushed remote runspace session created with this configuration name. private string _configurationName; + // Specifies an initial location of the powershell session. + private string _initialLocation; + #region Events /// /// Raised when session is closed. @@ -176,6 +179,7 @@ internal ServerRemoteSession(PSSenderInfo senderInfo, /// /// /// Optional configuration endpoint name for OutOfProc sessions. + /// Optional configuration initial location of the powershell session. /// /// /// InitialSessionState provider with does @@ -192,9 +196,11 @@ internal static ServerRemoteSession CreateServerRemoteSession(PSSenderInfo sende string configurationProviderId, string initializationParameters, AbstractServerSessionTransportManager transportManager, - string configurationName = null) + string configurationName = null, + string initialLocation = null) { - Dbg.Assert((senderInfo != null) & (senderInfo.UserInfo != null), + Dbg.Assert( + (senderInfo != null) && (senderInfo.UserInfo != null), "senderInfo and userInfo cannot be null."); s_trace.WriteLine("Finding InitialSessionState provider for id : {0}", configurationProviderId); @@ -207,12 +213,14 @@ internal static ServerRemoteSession CreateServerRemoteSession(PSSenderInfo sende string shellPrefix = System.Management.Automation.Remoting.Client.WSManNativeApi.ResourceURIPrefix; int index = configurationProviderId.IndexOf(shellPrefix, StringComparison.OrdinalIgnoreCase); senderInfo.ConfigurationName = (index == 0) ? configurationProviderId.Substring(shellPrefix.Length) : string.Empty; - ServerRemoteSession result = new ServerRemoteSession(senderInfo, + ServerRemoteSession result = new ServerRemoteSession( + senderInfo, configurationProviderId, initializationParameters, transportManager) { - _configurationName = configurationName + _configurationName = configurationName, + _initialLocation = initialLocation }; // start state machine. @@ -229,14 +237,22 @@ internal static ServerRemoteSession CreateServerRemoteSession(PSSenderInfo sende /// /// /// + /// /// - internal static ServerRemoteSession CreateServerRemoteSession(PSSenderInfo senderInfo, + internal static ServerRemoteSession CreateServerRemoteSession( + PSSenderInfo senderInfo, string initializationScriptForOutOfProcessRunspace, AbstractServerSessionTransportManager transportManager, - string configurationName) + string configurationName, + string initialLocation) { - ServerRemoteSession result = CreateServerRemoteSession(senderInfo, - "Microsoft.PowerShell", string.Empty, transportManager, configurationName); + ServerRemoteSession result = CreateServerRemoteSession( + senderInfo, + "Microsoft.PowerShell", + string.Empty, + transportManager, + configurationName: configurationName, + initialLocation: initialLocation); result._initScriptForOutOfProcRS = initializationScriptForOutOfProcessRunspace; return result; } @@ -885,7 +901,8 @@ private void HandleCreateRunspacePool(object sender, RemoteDataEventArgs createR isAdministrator, Context.ServerCapability, psClientVersion, - _configurationName); + _configurationName, + _initialLocation); // attach the necessary event handlers and start the driver. Interlocked.Exchange(ref _runspacePoolDriver, tmpDriver); diff --git a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx index d1bb9290151..db4c55e986e 100644 --- a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx +++ b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx @@ -1301,6 +1301,9 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro Cannot find a scheduled job with type {0} and name {1}. {0} is the job definition type and {1} is the job definition name. + + Cannot find the WorkingDirectory path {0}. + Cannot connect to session {0}. The session no longer exists on computer {1}. {0} is the session name that cannot be found. diff --git a/test/powershell/engine/Job/Jobs.Tests.ps1 b/test/powershell/engine/Job/Jobs.Tests.ps1 index 63cbee16f7c..4e0332afdbb 100644 --- a/test/powershell/engine/Job/Jobs.Tests.ps1 +++ b/test/powershell/engine/Job/Jobs.Tests.ps1 @@ -26,6 +26,14 @@ Describe 'Basic Job Tests' -Tags 'CI' { Context 'Basic tests' { + BeforeAll { + $invalidPathTestCases = @( + @{ path = "This is an invalid path"; case = "invalid path"; errorId = "DirectoryNotFoundException,Microsoft.PowerShell.Commands.StartJobCommand"} + @{ path = ""; case = "empty string"; errorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.StartJobCommand"} + @{ path = " "; case = "whitespace string (single space)"; errorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.StartJobCommand"} + ) + } + AfterEach { Get-Job | Where-Object { $_.Id -ne $startedJob.Id } | Remove-Job -ErrorAction SilentlyContinue -Force } @@ -69,6 +77,18 @@ Describe 'Basic Job Tests' -Tags 'CI' { $ProgressMsg[0].StatusDescription | Should -BeExactly 2 } + It 'Can use the user specified working directory parameter' { + $job = Start-Job -ScriptBlock { $pwd } -WorkingDirectory $TestDrive | Wait-Job + $jobOutput = Receive-Job $job + $jobOutput | Should -BeExactly $TestDrive.ToString() + } + + It 'Throws an error when the working directory parameter is ' -TestCases $invalidPathTestCases { + param($path, $case, $expectedErrorId) + + {Start-Job -ScriptBlock { 1 + 1 } -WorkingDirectory $path} | Should -Throw -ErrorId $expectedErrorId + } + It "Create job with native command" { try { $nativeJob = Start-job { pwsh -c 1+1 }