Skip to content

Commit cb53eb6

Browse files
davinci26TravisEz13
authored andcommitted
Add working directory parameter to Start-Job (PowerShell#10324)
1 parent f3a44f7 commit cb53eb6

File tree

10 files changed

+163
-38
lines changed

10 files changed

+163
-38
lines changed

src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ internal static int Start(
204204
{
205205
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("ServerMode");
206206
ProfileOptimization.StartProfile("StartupProfileData-ServerMode");
207-
System.Management.Automation.Remoting.Server.OutOfProcessMediator.Run(s_cpp.InitialCommand);
207+
System.Management.Automation.Remoting.Server.OutOfProcessMediator.Run(s_cpp.InitialCommand, s_cpp.WorkingDirectory);
208208
exitCode = 0;
209209
}
210210
else if (s_cpp.NamedPipeServerMode)

src/System.Management.Automation/engine/hostifaces/PowerShellProcessInstance.cs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,38 @@ static PowerShellProcessInstance()
4141
}
4242

4343
/// <summary>
44+
/// Initializes a new instance of the <see cref="PowerShellProcessInstance"/> class. Initializes the underlying dotnet process class.
4445
/// </summary>
45-
/// <param name="powerShellVersion"></param>
46-
/// <param name="credential"></param>
47-
/// <param name="initializationScript"></param>
48-
/// <param name="useWow64"></param>
49-
public PowerShellProcessInstance(Version powerShellVersion, PSCredential credential, ScriptBlock initializationScript, bool useWow64)
46+
/// <param name="powerShellVersion">Specifies the version of powershell.</param>
47+
/// <param name="credential">Specifies a user account credentials.</param>
48+
/// <param name="initializationScript">Specifies a script that will be executed when the powershell process is initialized.</param>
49+
/// <param name="useWow64">Specifies if the powershell process will be 32-bit.</param>
50+
/// <param name="workingDirectory">Specifies the initial working directory for the new powershell process.</param>
51+
public PowerShellProcessInstance(Version powerShellVersion, PSCredential credential, ScriptBlock initializationScript, bool useWow64, string workingDirectory)
5052
{
5153
string processArguments = " -s -NoLogo -NoProfile";
5254

55+
if (!string.IsNullOrWhiteSpace(workingDirectory))
56+
{
57+
processArguments = string.Format(
58+
CultureInfo.InvariantCulture,
59+
"{0} -wd {1}",
60+
processArguments,
61+
workingDirectory);
62+
}
63+
5364
if (initializationScript != null)
5465
{
5566
string scripBlockAsString = initializationScript.ToString();
5667
if (!string.IsNullOrEmpty(scripBlockAsString))
5768
{
5869
string encodedCommand =
5970
Convert.ToBase64String(Encoding.Unicode.GetBytes(scripBlockAsString));
60-
processArguments = string.Format(CultureInfo.InvariantCulture,
61-
"{0} -EncodedCommand {1}", processArguments, encodedCommand);
71+
processArguments = string.Format(
72+
CultureInfo.InvariantCulture,
73+
"{0} -EncodedCommand {1}",
74+
processArguments,
75+
encodedCommand);
6276
}
6377
}
6478

@@ -91,8 +105,20 @@ public PowerShellProcessInstance(Version powerShellVersion, PSCredential credent
91105
}
92106

93107
/// <summary>
108+
/// Initializes a new instance of the <see cref="PowerShellProcessInstance"/> class. Initializes the underlying dotnet process class.
109+
/// </summary>
110+
/// <param name="powerShellVersion"></param>
111+
/// <param name="credential"></param>
112+
/// <param name="initializationScript"></param>
113+
/// <param name="useWow64"></param>
114+
public PowerShellProcessInstance(Version powerShellVersion, PSCredential credential, ScriptBlock initializationScript, bool useWow64) : this(powerShellVersion, credential, initializationScript, useWow64, workingDirectory: null)
115+
{
116+
}
117+
118+
/// <summary>
119+
/// Initializes a new instance of the <see cref="PowerShellProcessInstance"/> class. Default initializes the underlying dotnet process class.
94120
/// </summary>
95-
public PowerShellProcessInstance() : this(null, null, null, false)
121+
public PowerShellProcessInstance() : this(powerShellVersion: null, credential: null, initializationScript: null, useWow64: false, workingDirectory: null)
96122
{
97123
}
98124

src/System.Management.Automation/engine/remoting/commands/StartJob.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,13 @@ public virtual ScriptBlock InitializationScript
480480

481481
private ScriptBlock _initScript;
482482

483+
/// <summary>
484+
/// Gets or sets an initial working directory for the powershell background job.
485+
/// </summary>
486+
[Parameter]
487+
[ValidateNotNullOrEmpty]
488+
public string WorkingDirectory { get; set; }
489+
483490
/// <summary>
484491
/// Launches the background job as a 32-bit process. This can be used on
485492
/// 64-bit systems to launch a 32-bit wow process for the background job.
@@ -589,6 +596,18 @@ protected override void BeginProcessing()
589596
ThrowTerminatingError(errorRecord);
590597
}
591598

599+
if (WorkingDirectory != null && !Directory.Exists(WorkingDirectory))
600+
{
601+
string message = StringUtil.Format(RemotingErrorIdStrings.StartJobWorkingDirectoryNotFound, WorkingDirectory);
602+
var errorRecord = new ErrorRecord(
603+
new DirectoryNotFoundException(message),
604+
"DirectoryNotFoundException",
605+
ErrorCategory.InvalidOperation,
606+
targetObject: null);
607+
608+
ThrowTerminatingError(errorRecord);
609+
}
610+
592611
CommandDiscovery.AutoloadModulesWithJobSourceAdapters(this.Context, this.CommandOrigin);
593612

594613
if (ParameterSetName == DefinitionNameParameterSet)
@@ -628,6 +647,7 @@ protected override void CreateHelpersForSpecifiedComputerNames()
628647
connectionInfo.InitializationScript = _initScript;
629648
connectionInfo.AuthenticationMechanism = this.Authentication;
630649
connectionInfo.PSVersion = this.PSVersion;
650+
connectionInfo.WorkingDirectory = this.WorkingDirectory;
631651

632652
RemoteRunspace remoteRunspace = (RemoteRunspace)RunspaceFactory.CreateRunspace(connectionInfo, this.Host,
633653
Utils.GetTypeTableFromExecutionContextTLS());

src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,11 @@ internal sealed class NewProcessConnectionInfo : RunspaceConnectionInfo
15141514
/// </summary>
15151515
public bool RunAs32 { get; set; }
15161516

1517+
/// <summary>
1518+
/// Gets or sets an initial working directory for the powershell background process.
1519+
/// </summary>
1520+
public string WorkingDirectory { get; set; }
1521+
15171522
/// <summary>
15181523
/// Powershell version to execute the job in.
15191524
/// </summary>
@@ -1590,6 +1595,7 @@ public NewProcessConnectionInfo Copy()
15901595
NewProcessConnectionInfo result = new NewProcessConnectionInfo(_credential);
15911596
result.AuthenticationMechanism = this.AuthenticationMechanism;
15921597
result.InitializationScript = this.InitializationScript;
1598+
result.WorkingDirectory = this.WorkingDirectory;
15931599
result.RunAs32 = this.RunAs32;
15941600
result.PSVersion = this.PSVersion;
15951601
result.Process = Process;

src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,8 @@ internal override void CreateAsync()
10031003
_processInstance = _connectionInfo.Process ?? new PowerShellProcessInstance(_connectionInfo.PSVersion,
10041004
_connectionInfo.Credential,
10051005
_connectionInfo.InitializationScript,
1006-
_connectionInfo.RunAs32);
1006+
_connectionInfo.RunAs32,
1007+
_connectionInfo.WorkingDirectory);
10071008
if (_connectionInfo.Process != null)
10081009
{
10091010
_processCreated = false;

src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ protected void ProcessingThreadStart(object state)
8787
catch (Exception e)
8888
{
8989
PSEtwLog.LogOperationalError(
90-
PSEventId.TransportError, PSOpcode.Open, PSTask.None,
90+
PSEventId.TransportError,
91+
PSOpcode.Open,
92+
PSTask.None,
9193
PSKeyword.UseAlwaysOperational,
9294
Guid.Empty.ToString(),
9395
Guid.Empty.ToString(),
@@ -96,7 +98,9 @@ protected void ProcessingThreadStart(object state)
9698
e.StackTrace);
9799

98100
PSEtwLog.LogAnalyticError(
99-
PSEventId.TransportError_Analytic, PSOpcode.Open, PSTask.None,
101+
PSEventId.TransportError_Analytic,
102+
PSOpcode.Open,
103+
PSTask.None,
100104
PSKeyword.Transport | PSKeyword.UseAlwaysAnalytic,
101105
Guid.Empty.ToString(),
102106
Guid.Empty.ToString(),
@@ -158,7 +162,9 @@ protected void OnDataPacketReceived(byte[] rawData, string stream, Guid psGuid)
158162

159163
protected void OnDataAckPacketReceived(Guid psGuid)
160164
{
161-
throw new PSRemotingTransportException(PSRemotingErrorId.IPCUnknownElementReceived, RemotingErrorIdStrings.IPCUnknownElementReceived,
165+
throw new PSRemotingTransportException(
166+
PSRemotingErrorId.IPCUnknownElementReceived,
167+
RemotingErrorIdStrings.IPCUnknownElementReceived,
162168
OutOfProcessUtils.PS_OUT_OF_PROC_DATA_ACK_TAG);
163169
}
164170

@@ -301,33 +307,39 @@ protected void OnCloseAckPacketReceived(Guid psGuid)
301307

302308
#region Methods
303309

304-
protected OutOfProcessServerSessionTransportManager CreateSessionTransportManager(string configurationName, PSRemotingCryptoHelperServer cryptoHelper)
310+
protected OutOfProcessServerSessionTransportManager CreateSessionTransportManager(string configurationName, PSRemotingCryptoHelperServer cryptoHelper, string workingDirectory)
305311
{
306312
PSSenderInfo senderInfo;
307313
#if !UNIX
308314
WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent();
309-
PSPrincipal userPrincipal = new PSPrincipal(new PSIdentity(string.Empty, true, currentIdentity.Name, null),
315+
PSPrincipal userPrincipal = new PSPrincipal(
316+
new PSIdentity(string.Empty, true, currentIdentity.Name, null),
310317
currentIdentity);
311318
senderInfo = new PSSenderInfo(userPrincipal, "http://localhost");
312319
#else
313-
PSPrincipal userPrincipal = new PSPrincipal(new PSIdentity(string.Empty, true, string.Empty, null),
320+
PSPrincipal userPrincipal = new PSPrincipal(
321+
new PSIdentity(string.Empty, true, string.Empty, null),
314322
null);
315323
senderInfo = new PSSenderInfo(userPrincipal, "http://localhost");
316324
#endif
317325

318326
OutOfProcessServerSessionTransportManager tm = new OutOfProcessServerSessionTransportManager(originalStdOut, originalStdErr, cryptoHelper);
319327

320-
ServerRemoteSession srvrRemoteSession = ServerRemoteSession.CreateServerRemoteSession(senderInfo,
321-
_initialCommand, tm, configurationName);
328+
ServerRemoteSession.CreateServerRemoteSession(
329+
senderInfo,
330+
_initialCommand,
331+
tm,
332+
configurationName,
333+
workingDirectory);
322334

323335
return tm;
324336
}
325337

326-
protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoHelper, string configurationName = null)
338+
protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoHelper, string workingDirectory = null, string configurationName = null)
327339
{
328340
_initialCommand = initialCommand;
329341

330-
sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper);
342+
sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper, workingDirectory);
331343

332344
try
333345
{
@@ -338,7 +350,7 @@ protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoH
338350
{
339351
if (sessionTM == null)
340352
{
341-
sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper);
353+
sessionTM = CreateSessionTransportManager(configurationName, cryptoHelper, workingDirectory);
342354
}
343355
}
344356

@@ -352,8 +364,10 @@ protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoH
352364
sessionTM = null;
353365
}
354366

355-
throw new PSRemotingTransportException(PSRemotingErrorId.IPCUnknownElementReceived,
356-
RemotingErrorIdStrings.IPCUnknownElementReceived, string.Empty);
367+
throw new PSRemotingTransportException(
368+
PSRemotingErrorId.IPCUnknownElementReceived,
369+
RemotingErrorIdStrings.IPCUnknownElementReceived,
370+
string.Empty);
357371
}
358372

359373
// process data in a thread pool thread..this way Runspace, Command
@@ -366,7 +380,8 @@ protected void Start(string initialCommand, PSRemotingCryptoHelperServer cryptoH
366380
#else
367381
ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessingThreadStart), data);
368382
#endif
369-
} while (true);
383+
}
384+
while (true);
370385
}
371386
catch (Exception e)
372387
{
@@ -468,8 +483,11 @@ private OutOfProcessMediator() : base(true)
468483
#region Static Methods
469484

470485
/// <summary>
486+
/// Starts the out-of-process powershell server instance.
471487
/// </summary>
472-
internal static void Run(string initialCommand)
488+
/// <param name="initialCommand">Specifies the initialization script.</param>
489+
/// <param name="workingDirectory">Specifies the initial working directory. The working directory is set before the initial command.</param>
490+
internal static void Run(string initialCommand, string workingDirectory)
473491
{
474492
lock (SyncObject)
475493
{
@@ -486,7 +504,7 @@ internal static void Run(string initialCommand)
486504
// Setup unhandled exception to log events
487505
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(AppDomainUnhandledException);
488506
#endif
489-
s_singletonInstance.Start(initialCommand, new PSRemotingCryptoHelperServer());
507+
s_singletonInstance.Start(initialCommand, new PSRemotingCryptoHelperServer(), workingDirectory);
490508
}
491509

492510
#endregion

src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Diagnostics;
88
using System.Globalization;
9+
using System.IO;
910
using System.Linq;
1011
using System.Management.Automation.Host;
1112
using System.Management.Automation.Internal;
@@ -86,6 +87,9 @@ private Dictionary<Guid, ServerPowerShellDriver> _associatedShells
8687
// Results in a configured remote runspace pushed onto driver host.
8788
private string _configurationName;
8889

90+
// Optional initial location of the PowerShell session
91+
private readonly string _initialLocation;
92+
8993
/// <summary>
9094
/// Event that get raised when the RunspacePool is closed.
9195
/// </summary>
@@ -120,6 +124,7 @@ private Dictionary<Guid, ServerPowerShellDriver> _associatedShells
120124
/// <param name="serverCapability">Server capability reported to the client during negotiation (not the actual capability).</param>
121125
/// <param name="psClientVersion">Client PowerShell version.</param>
122126
/// <param name="configurationName">Optional endpoint configuration name to create a pushed configured runspace.</param>
127+
/// <param name="initialLocation">Optional initial location of the powershell.</param>
123128
internal ServerRunspacePoolDriver(
124129
Guid clientRunspacePoolId,
125130
int minRunspaces,
@@ -134,19 +139,21 @@ internal ServerRunspacePoolDriver(
134139
bool isAdministrator,
135140
RemoteSessionCapability serverCapability,
136141
Version psClientVersion,
137-
string configurationName)
142+
string configurationName,
143+
string initialLocation)
138144
{
139145
Dbg.Assert(configData != null, "ConfigurationData cannot be null");
140146

141147
_serverCapability = serverCapability;
142148
_clientPSVersion = psClientVersion;
143149

144150
_configurationName = configurationName;
151+
_initialLocation = initialLocation;
145152

146153
// Create a new server host and associate for host call
147154
// integration
148-
_remoteHost = new ServerDriverRemoteHost(clientRunspacePoolId,
149-
Guid.Empty, hostInfo, transportManager, null);
155+
_remoteHost = new ServerDriverRemoteHost(
156+
clientRunspacePoolId, Guid.Empty, hostInfo, transportManager, null);
150157

151158
_configData = configData;
152159
_applicationPrivateData = applicationPrivateData;
@@ -615,6 +622,13 @@ private void HandleRunspaceCreated(object sender, RunspaceCreatedEventArgs args)
615622
// to ignore all but critical errors.
616623
}
617624

625+
if (!string.IsNullOrWhiteSpace(_initialLocation))
626+
{
627+
var setLocationCommand = new Command("Set-Location");
628+
setLocationCommand.Parameters.Add(new CommandParameter("LiteralPath", _initialLocation));
629+
InvokeScript(setLocationCommand, args);
630+
}
631+
618632
// Run startup scripts
619633
InvokeStartupScripts(args);
620634

0 commit comments

Comments
 (0)