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 }