├── .gitattributes ├── .gitignore ├── LICENSE ├── PSThreadJob ├── AssemblyInfo.cs ├── PSThreadJob.cs ├── PSThreadJob.csproj ├── Resources.Designer.cs ├── Resources.resx └── ThreadJob.psd1 ├── README.md ├── build.ps1 ├── test ├── ThreadJob.CL.Tests.ps1 └── ThreadJob.Tests.ps1 └── tools ├── helper.psm1 └── releaseBuild ├── releaseBuild.yml ├── sign-catalog.xml ├── sign-module-files.xml └── templates └── compliance.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.gitattributes text 7 | *.gitignore text 8 | *.sln text 9 | *.csproj text 10 | *.cs text 11 | *.resx text 12 | *.xml text 13 | *.yml text 14 | *.ps1 text 15 | *.psd1 text 16 | *.psm1 text 17 | *.md text 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | 4 | # VS auto-generated solution files for project.json solutions 5 | *.xproj 6 | *.xproj.user 7 | *.suo 8 | 9 | # VS auto-generated files for csproj files 10 | *.csproj.user 11 | 12 | # Visual Studio IDE directory 13 | .vs/ 14 | 15 | # Ignore binaries and symbols 16 | *.pdb 17 | *.dll 18 | *.wixpdb 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Higinbotham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PSThreadJob/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | /********************************************************************++ 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | --********************************************************************/ 4 | 5 | using System.Reflection; 6 | using System.Runtime.InteropServices; 7 | 8 | // General Information about an assembly is controlled through the following 9 | // set of attributes. Change these attribute values to modify the information 10 | // associated with an assembly. 11 | [assembly: AssemblyDescription("Implements PowerShell Start-ThreadJob")] 12 | [assembly: AssemblyCopyright("")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("aba48637-8365-4c8f-90b5-dc424f5f5281")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | -------------------------------------------------------------------------------- /PSThreadJob/PSThreadJob.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.ComponentModel; 8 | using System.Globalization; 9 | using System.Linq; 10 | using System.Management.Automation; 11 | using System.Management.Automation.Host; 12 | using System.Management.Automation.Language; 13 | using System.Management.Automation.Runspaces; 14 | using System.Management.Automation.Security; 15 | using System.Text; 16 | using System.Threading; 17 | 18 | namespace ThreadJob 19 | { 20 | [Cmdlet(VerbsLifecycle.Start, "ThreadJob")] 21 | [OutputType(typeof(ThreadJob))] 22 | public sealed class StartThreadJobCommand : PSCmdlet 23 | { 24 | #region Private members 25 | 26 | private bool _processFirstRecord; 27 | private string _command; 28 | private string _currentLocationPath; 29 | private ThreadJob _threadJob; 30 | 31 | #endregion 32 | 33 | #region Parameters 34 | 35 | private const string ScriptBlockParameterSet = "ScriptBlock"; 36 | private const string FilePathParameterSet = "FilePath"; 37 | 38 | [Parameter(ParameterSetName = ScriptBlockParameterSet, Mandatory=true, Position=0)] 39 | [ValidateNotNullAttribute] 40 | public ScriptBlock ScriptBlock { get; set; } 41 | 42 | [Parameter(ParameterSetName = FilePathParameterSet, Mandatory=true, Position=0)] 43 | [ValidateNotNullOrEmpty] 44 | public string FilePath { get; set; } 45 | 46 | [Parameter(ParameterSetName = ScriptBlockParameterSet)] 47 | [Parameter(ParameterSetName = FilePathParameterSet)] 48 | [ValidateNotNullOrEmpty] 49 | public string Name { get; set; } 50 | 51 | [Parameter(ParameterSetName = ScriptBlockParameterSet)] 52 | [Parameter(ParameterSetName = FilePathParameterSet)] 53 | [ValidateNotNull] 54 | public ScriptBlock InitializationScript { get; set; } 55 | 56 | [Parameter(ParameterSetName = ScriptBlockParameterSet, ValueFromPipeline=true)] 57 | [Parameter(ParameterSetName = FilePathParameterSet, ValueFromPipeline=true)] 58 | [ValidateNotNull] 59 | public PSObject InputObject { get; set; } 60 | 61 | [Parameter(ParameterSetName = ScriptBlockParameterSet)] 62 | [Parameter(ParameterSetName = FilePathParameterSet)] 63 | public Object[] ArgumentList { get; set; } 64 | 65 | [Parameter(ParameterSetName = ScriptBlockParameterSet)] 66 | [Parameter(ParameterSetName = FilePathParameterSet)] 67 | [ValidateRange(1, 1000000)] 68 | public int ThrottleLimit { get; set; } 69 | 70 | [Parameter(ParameterSetName = ScriptBlockParameterSet)] 71 | [Parameter(ParameterSetName = FilePathParameterSet)] 72 | public PSHost StreamingHost { get; set; } 73 | 74 | #endregion 75 | 76 | #region Overrides 77 | 78 | protected override void BeginProcessing() 79 | { 80 | base.BeginProcessing(); 81 | 82 | if (ParameterSetName.Equals(ScriptBlockParameterSet)) 83 | { 84 | _command = ScriptBlock.ToString(); 85 | } 86 | else 87 | { 88 | _command = FilePath; 89 | } 90 | 91 | try 92 | { 93 | _currentLocationPath = SessionState.Path.CurrentLocation.Path; 94 | } 95 | catch (PSInvalidOperationException) 96 | { 97 | } 98 | } 99 | 100 | protected override void ProcessRecord() 101 | { 102 | base.ProcessRecord(); 103 | 104 | if (!_processFirstRecord) 105 | { 106 | if (StreamingHost != null) 107 | { 108 | _threadJob = new ThreadJob(Name, _command, ScriptBlock, FilePath, InitializationScript, ArgumentList, 109 | InputObject, this, _currentLocationPath, StreamingHost); 110 | } 111 | else 112 | { 113 | _threadJob = new ThreadJob(Name, _command, ScriptBlock, FilePath, InitializationScript, ArgumentList, 114 | InputObject, this, _currentLocationPath); 115 | } 116 | 117 | ThreadJob.StartJob(_threadJob, ThrottleLimit); 118 | WriteObject(_threadJob); 119 | 120 | _processFirstRecord = true; 121 | } 122 | else 123 | { 124 | // Inject input. 125 | if (InputObject != null) 126 | { 127 | _threadJob.InjectInput(InputObject); 128 | } 129 | } 130 | } 131 | 132 | protected override void EndProcessing() 133 | { 134 | base.EndProcessing(); 135 | 136 | _threadJob.CloseInputStream(); 137 | } 138 | 139 | #endregion 140 | } 141 | 142 | public sealed class ThreadJobSourceAdapter : JobSourceAdapter 143 | { 144 | #region Members 145 | 146 | private ConcurrentDictionary _repository; 147 | 148 | #endregion 149 | 150 | #region Constructor 151 | 152 | /// 153 | /// Constructor 154 | /// 155 | public ThreadJobSourceAdapter() 156 | { 157 | Name = "ThreadJobSourceAdapter"; 158 | _repository = new ConcurrentDictionary(); 159 | } 160 | 161 | #endregion 162 | 163 | #region JobSourceAdapter Implementation 164 | 165 | /// 166 | /// NewJob 167 | /// 168 | public override Job2 NewJob(JobInvocationInfo specification) 169 | { 170 | var job = specification.Parameters[0][0].Value as ThreadJob; 171 | if (job != null) 172 | { 173 | _repository.TryAdd(job.InstanceId, job); 174 | } 175 | return job; 176 | } 177 | 178 | /// 179 | /// GetJobs 180 | /// 181 | public override IList GetJobs() 182 | { 183 | return _repository.Values.ToArray(); 184 | } 185 | 186 | /// 187 | /// GetJobsByName 188 | /// 189 | public override IList GetJobsByName(string name, bool recurse) 190 | { 191 | List rtnList = new List(); 192 | foreach (var job in _repository.Values) 193 | { 194 | if (job.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) 195 | { 196 | rtnList.Add(job); 197 | } 198 | } 199 | return rtnList; 200 | } 201 | 202 | /// 203 | /// GetJobsByCommand 204 | /// 205 | public override IList GetJobsByCommand(string command, bool recurse) 206 | { 207 | List rtnList = new List(); 208 | foreach (var job in _repository.Values) 209 | { 210 | if (job.Command.Equals(command, StringComparison.OrdinalIgnoreCase)) 211 | { 212 | rtnList.Add(job); 213 | } 214 | } 215 | return rtnList; 216 | } 217 | 218 | /// 219 | /// GetJobByInstanceId 220 | /// 221 | public override Job2 GetJobByInstanceId(Guid instanceId, bool recurse) 222 | { 223 | Job2 job; 224 | if (_repository.TryGetValue(instanceId, out job)) 225 | { 226 | return job; 227 | } 228 | return null; 229 | } 230 | 231 | /// 232 | /// GetJobBySessionId 233 | /// 234 | public override Job2 GetJobBySessionId(int id, bool recurse) 235 | { 236 | foreach (var job in _repository.Values) 237 | { 238 | if (job.Id == id) 239 | { 240 | return job; 241 | } 242 | } 243 | return null; 244 | } 245 | 246 | /// 247 | /// GetJobsByState 248 | /// 249 | public override IList GetJobsByState(JobState state, bool recurse) 250 | { 251 | List rtnList = new List(); 252 | foreach (var job in _repository.Values) 253 | { 254 | if (job.JobStateInfo.State == state) 255 | { 256 | rtnList.Add(job); 257 | } 258 | } 259 | return rtnList; 260 | } 261 | 262 | /// 263 | /// GetJobsByFilter 264 | /// 265 | public override IList GetJobsByFilter(Dictionary filter, bool recurse) 266 | { 267 | throw new PSNotSupportedException(); 268 | } 269 | 270 | /// 271 | /// RemoveJob 272 | /// 273 | public override void RemoveJob(Job2 job) 274 | { 275 | Job2 removeJob; 276 | if (_repository.TryGetValue(job.InstanceId, out removeJob)) 277 | { 278 | removeJob.StopJob(); 279 | _repository.TryRemove(job.InstanceId, out removeJob); 280 | } 281 | } 282 | 283 | #endregion 284 | } 285 | 286 | internal sealed class ThreadJobDebugger : Debugger 287 | { 288 | #region Members 289 | 290 | private Debugger _wrappedDebugger; 291 | private string _jobName; 292 | 293 | #endregion 294 | 295 | #region Constructor 296 | 297 | private ThreadJobDebugger() { } 298 | 299 | public ThreadJobDebugger( 300 | Debugger debugger, 301 | string jobName) 302 | { 303 | if (debugger == null) 304 | { 305 | throw new PSArgumentNullException("debugger"); 306 | } 307 | 308 | _wrappedDebugger = debugger; 309 | _jobName = jobName ?? string.Empty; 310 | 311 | // Create handlers for wrapped debugger events. 312 | _wrappedDebugger.BreakpointUpdated += HandleBreakpointUpdated; 313 | _wrappedDebugger.DebuggerStop += HandleDebuggerStop; 314 | } 315 | 316 | #endregion 317 | 318 | #region Debugger overrides 319 | 320 | /// 321 | /// Evaluates provided command either as a debugger specific command 322 | /// or a PowerShell command. 323 | /// 324 | /// PowerShell command. 325 | /// Output. 326 | /// DebuggerCommandResults. 327 | public override DebuggerCommandResults ProcessCommand(PSCommand command, PSDataCollection output) 328 | { 329 | // Special handling for the prompt command. 330 | if (command.Commands[0].CommandText.Trim().Equals("prompt", StringComparison.OrdinalIgnoreCase)) 331 | { 332 | return HandlePromptCommand(output); 333 | } 334 | 335 | return _wrappedDebugger.ProcessCommand(command, output); 336 | } 337 | 338 | /// 339 | /// Adds the provided set of breakpoints to the debugger. 340 | /// 341 | /// Breakpoints. 342 | public override void SetBreakpoints(IEnumerable breakpoints) 343 | { 344 | _wrappedDebugger.SetBreakpoints(breakpoints); 345 | } 346 | 347 | /// 348 | /// Sets the debugger resume action. 349 | /// 350 | /// DebuggerResumeAction. 351 | public override void SetDebuggerAction(DebuggerResumeAction resumeAction) 352 | { 353 | _wrappedDebugger.SetDebuggerAction(resumeAction); 354 | } 355 | 356 | /// 357 | /// Stops a running command. 358 | /// 359 | public override void StopProcessCommand() 360 | { 361 | _wrappedDebugger.StopProcessCommand(); 362 | } 363 | 364 | /// 365 | /// Returns current debugger stop event arguments if debugger is in 366 | /// debug stop state. Otherwise returns null. 367 | /// 368 | /// DebuggerStopEventArgs. 369 | public override DebuggerStopEventArgs GetDebuggerStopArgs() 370 | { 371 | return _wrappedDebugger.GetDebuggerStopArgs(); 372 | } 373 | 374 | /// 375 | /// Sets the parent debugger, breakpoints, and other debugging context information. 376 | /// 377 | /// Parent debugger. 378 | /// List of breakpoints. 379 | /// Debugger mode. 380 | /// PowerShell host. 381 | /// Current path. 382 | public override void SetParent( 383 | Debugger parent, 384 | IEnumerable breakPoints, 385 | DebuggerResumeAction? startAction, 386 | PSHost host, 387 | PathInfo path) 388 | { 389 | // For now always enable step mode debugging. 390 | SetDebuggerStepMode(true); 391 | } 392 | 393 | /// 394 | /// Sets the debugger mode. 395 | /// 396 | public override void SetDebugMode(DebugModes mode) 397 | { 398 | _wrappedDebugger.SetDebugMode(mode); 399 | 400 | base.SetDebugMode(mode); 401 | } 402 | 403 | /// 404 | /// Returns IEnumerable of CallStackFrame objects. 405 | /// 406 | /// 407 | public override IEnumerable GetCallStack() 408 | { 409 | return _wrappedDebugger.GetCallStack(); 410 | } 411 | 412 | /// 413 | /// Sets debugger stepping mode. 414 | /// 415 | /// True if stepping is to be enabled. 416 | public override void SetDebuggerStepMode(bool enabled) 417 | { 418 | _wrappedDebugger.SetDebuggerStepMode(enabled); 419 | } 420 | 421 | /// 422 | /// True when debugger is stopped at a breakpoint. 423 | /// 424 | public override bool InBreakpoint 425 | { 426 | get { return _wrappedDebugger.InBreakpoint; } 427 | } 428 | 429 | #endregion 430 | 431 | #region Private methods 432 | 433 | private void HandleDebuggerStop(object sender, DebuggerStopEventArgs e) 434 | { 435 | this.RaiseDebuggerStopEvent(e); 436 | } 437 | 438 | private void HandleBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) 439 | { 440 | this.RaiseBreakpointUpdatedEvent(e); 441 | } 442 | 443 | private DebuggerCommandResults HandlePromptCommand(PSDataCollection output) 444 | { 445 | // Nested debugged runspace prompt should look like: 446 | // [DBG]: [JobName]: PS C:\>> 447 | string promptScript = "'[DBG]: '" + " + " + "'[" + CodeGeneration.EscapeSingleQuotedStringContent(_jobName) + "]: '" + " + " + @"""PS $($executionContext.SessionState.Path.CurrentLocation)>> """; 448 | PSCommand promptCommand = new PSCommand(); 449 | promptCommand.AddScript(promptScript); 450 | _wrappedDebugger.ProcessCommand(promptCommand, output); 451 | 452 | return new DebuggerCommandResults(null, true); 453 | } 454 | 455 | #endregion 456 | } 457 | 458 | /// 459 | /// ThreadJob 460 | /// 461 | public sealed class ThreadJob : Job2, IJobDebugger 462 | { 463 | #region Private members 464 | 465 | private ScriptBlock _sb; 466 | private string _filePath; 467 | private ScriptBlock _initSb; 468 | private object[] _argumentList; 469 | private Dictionary _usingValuesMap; 470 | private PSDataCollection _input; 471 | private Runspace _rs; 472 | private PowerShell _ps; 473 | private PSDataCollection _output; 474 | private bool _runningInitScript; 475 | private PSHost _streamingHost; 476 | private Debugger _jobDebugger; 477 | private string _currentLocationPath; 478 | 479 | private const string VERBATIM_ARGUMENT = "--%"; 480 | 481 | private static ThreadJobQueue s_JobQueue; 482 | 483 | #endregion 484 | 485 | #region Properties 486 | 487 | /// 488 | /// Specifies the job definition for the JobManager 489 | /// 490 | public JobDefinition ThreadJobDefinition 491 | { 492 | get; 493 | private set; 494 | } 495 | 496 | #endregion 497 | 498 | #region Constructors 499 | 500 | // Constructors 501 | static ThreadJob() 502 | { 503 | s_JobQueue = new ThreadJobQueue(5); 504 | } 505 | 506 | private ThreadJob() 507 | { } 508 | 509 | /// 510 | /// Constructor. 511 | /// 512 | /// 513 | /// 514 | /// 515 | /// 516 | /// 517 | /// 518 | /// 519 | /// 520 | /// 521 | public ThreadJob( 522 | string name, 523 | string command, 524 | ScriptBlock sb, 525 | string filePath, 526 | ScriptBlock initSb, 527 | object[] argumentList, 528 | PSObject inputObject, 529 | PSCmdlet psCmdlet, 530 | string currentLocationPath) 531 | : this(name, command, sb, filePath, initSb, argumentList, inputObject, psCmdlet, currentLocationPath, null) 532 | { 533 | } 534 | 535 | /// 536 | /// Constructor. 537 | /// 538 | /// 539 | /// 540 | /// 541 | /// 542 | /// 543 | /// 544 | /// 545 | /// 546 | /// 547 | /// 548 | public ThreadJob( 549 | string name, 550 | string command, 551 | ScriptBlock sb, 552 | string filePath, 553 | ScriptBlock initSb, 554 | object[] argumentList, 555 | PSObject inputObject, 556 | PSCmdlet psCmdlet, 557 | string currentLocationPath, 558 | PSHost streamingHost) 559 | : base(command, name) 560 | { 561 | _sb = sb; 562 | _filePath = filePath; 563 | _initSb = initSb; 564 | _argumentList = argumentList; 565 | _input = new PSDataCollection(); 566 | if (inputObject != null) 567 | { 568 | _input.Add(inputObject); 569 | } 570 | _output = new PSDataCollection(); 571 | _streamingHost = streamingHost; 572 | _currentLocationPath = currentLocationPath; 573 | 574 | this.PSJobTypeName = "ThreadJob"; 575 | 576 | // Get script block to run. 577 | if (!string.IsNullOrEmpty(_filePath)) 578 | { 579 | _sb = GetScriptBlockFromFile(_filePath, psCmdlet); 580 | if (_sb == null) 581 | { 582 | throw new InvalidOperationException(Properties.Resources.CannotParseScriptFile); 583 | } 584 | } 585 | else if (_sb == null) 586 | { 587 | throw new PSArgumentNullException(Properties.Resources.NoScriptToRun); 588 | } 589 | 590 | // Create Runspace/PowerShell object and state callback. 591 | // The job script/command will run in a separate thread associated with the Runspace. 592 | var iss = InitialSessionState.CreateDefault2(); 593 | 594 | // Determine session language mode for Windows platforms 595 | WarningRecord lockdownWarning = null; 596 | if (Environment.OSVersion.Platform.ToString().Equals("Win32NT", StringComparison.OrdinalIgnoreCase)) 597 | { 598 | bool enforceLockdown = (SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce); 599 | if (enforceLockdown && !string.IsNullOrEmpty(_filePath)) 600 | { 601 | // If script source is a file, check to see if it is trusted by the lock down policy 602 | enforceLockdown = (SystemPolicy.GetLockdownPolicy(_filePath, null) == SystemEnforcementMode.Enforce); 603 | 604 | if (!enforceLockdown && (_initSb != null)) 605 | { 606 | // Even if the script file is trusted, an initialization script cannot be trusted, so we have to enforce 607 | // lock down. Otherwise untrusted script could be run in FullLanguage mode along with the trusted file script. 608 | enforceLockdown = true; 609 | lockdownWarning = new WarningRecord( 610 | string.Format( 611 | CultureInfo.InvariantCulture, 612 | Properties.Resources.CannotRunTrustedFileInFL, 613 | _filePath)); 614 | } 615 | } 616 | 617 | iss.LanguageMode = enforceLockdown ? PSLanguageMode.ConstrainedLanguage : PSLanguageMode.FullLanguage; 618 | } 619 | 620 | if (_streamingHost != null) 621 | { 622 | _rs = RunspaceFactory.CreateRunspace(_streamingHost, iss); 623 | } 624 | else 625 | { 626 | _rs = RunspaceFactory.CreateRunspace(iss); 627 | } 628 | _ps = PowerShell.Create(); 629 | _ps.Runspace = _rs; 630 | _ps.InvocationStateChanged += (sender, psStateChanged) => 631 | { 632 | var newStateInfo = psStateChanged.InvocationStateInfo; 633 | 634 | // Update Job state. 635 | switch (newStateInfo.State) 636 | { 637 | case PSInvocationState.Running: 638 | SetJobState(JobState.Running); 639 | break; 640 | 641 | case PSInvocationState.Stopped: 642 | SetJobState(JobState.Stopped, newStateInfo.Reason, disposeRunspace:true); 643 | break; 644 | 645 | case PSInvocationState.Failed: 646 | SetJobState(JobState.Failed, newStateInfo.Reason, disposeRunspace:true); 647 | break; 648 | 649 | case PSInvocationState.Completed: 650 | if (_runningInitScript) 651 | { 652 | // Begin running main script. 653 | _runningInitScript = false; 654 | RunScript(); 655 | } 656 | else 657 | { 658 | SetJobState(JobState.Completed, newStateInfo.Reason, disposeRunspace:true); 659 | } 660 | break; 661 | } 662 | }; 663 | 664 | // Get any using variables. 665 | var usingAsts = _sb.Ast.FindAll(ast => ast is UsingExpressionAst, searchNestedScriptBlocks: true).Cast(); 666 | if (usingAsts != null && 667 | usingAsts.FirstOrDefault() != null) 668 | { 669 | // Get using variables as dictionary, since we now only support PowerShell version 5.1 and greater 670 | _usingValuesMap = GetUsingValuesAsDictionary(usingAsts, psCmdlet); 671 | } 672 | 673 | // Hook up data streams. 674 | this.Output = _output; 675 | this.Output.EnumeratorNeverBlocks = true; 676 | 677 | this.Error = _ps.Streams.Error; 678 | this.Error.EnumeratorNeverBlocks = true; 679 | 680 | this.Progress = _ps.Streams.Progress; 681 | this.Progress.EnumeratorNeverBlocks = true; 682 | 683 | this.Verbose = _ps.Streams.Verbose; 684 | this.Verbose.EnumeratorNeverBlocks = true; 685 | 686 | this.Warning = _ps.Streams.Warning; 687 | this.Warning.EnumeratorNeverBlocks = true; 688 | if (lockdownWarning != null) 689 | { 690 | this.Warning.Add(lockdownWarning); 691 | } 692 | 693 | this.Debug = _ps.Streams.Debug; 694 | this.Debug.EnumeratorNeverBlocks = true; 695 | 696 | this.Information = _ps.Streams.Information; 697 | this.Information.EnumeratorNeverBlocks = true; 698 | 699 | // Create the JobManager job definition and job specification, and add to the JobManager. 700 | ThreadJobDefinition = new JobDefinition(typeof(ThreadJobSourceAdapter), "", Name); 701 | Dictionary parameterCollection = new Dictionary(); 702 | parameterCollection.Add("NewJob", this); 703 | var jobSpecification = new JobInvocationInfo(ThreadJobDefinition, parameterCollection); 704 | var newJob = psCmdlet.JobManager.NewJob(jobSpecification); 705 | System.Diagnostics.Debug.Assert(newJob == this, "JobManager must return this job"); 706 | } 707 | 708 | #endregion 709 | 710 | #region Public methods 711 | 712 | /// 713 | /// StartJob 714 | /// 715 | public override void StartJob() 716 | { 717 | if (this.JobStateInfo.State != JobState.NotStarted) 718 | { 719 | throw new Exception(Properties.Resources.CannotStartJob); 720 | } 721 | 722 | // Initialize Runspace state 723 | _rs.Open(); 724 | 725 | // Set current location path on the runspace, if available. 726 | if (_currentLocationPath != null) 727 | { 728 | using (var ps = PowerShell.Create()) 729 | { 730 | ps.Runspace = _rs; 731 | ps.AddCommand("Set-Location").AddParameter("LiteralPath", _currentLocationPath).Invoke(); 732 | } 733 | } 734 | 735 | // If initial script block provided then execute. 736 | if (_initSb != null) 737 | { 738 | // Run initial script and then the main script. 739 | _ps.Commands.Clear(); 740 | _ps.AddScript(_initSb.ToString()); 741 | _runningInitScript = true; 742 | _ps.BeginInvoke(_input, _output); 743 | } 744 | else 745 | { 746 | // Run main script. 747 | RunScript(); 748 | } 749 | } 750 | 751 | /// 752 | /// InjectInput 753 | /// 754 | /// 755 | public void InjectInput(PSObject psObject) 756 | { 757 | if (psObject != null) 758 | { 759 | _input.Add(psObject); 760 | } 761 | } 762 | 763 | /// 764 | /// CloseInputStream 765 | /// 766 | public void CloseInputStream() 767 | { 768 | _input.Complete(); 769 | } 770 | 771 | /// 772 | /// StartJob 773 | /// 774 | /// 775 | /// 776 | public static void StartJob(ThreadJob job, int throttleLimit) 777 | { 778 | s_JobQueue.EnqueueJob(job, throttleLimit); 779 | } 780 | 781 | /// 782 | /// Dispose 783 | /// 784 | /// 785 | protected override void Dispose(bool disposing) 786 | { 787 | if (disposing) 788 | { 789 | if (_ps.InvocationStateInfo.State == PSInvocationState.Running) 790 | { 791 | _ps.Stop(); 792 | } 793 | _ps.Dispose(); 794 | 795 | _input.Complete(); 796 | _output.Complete(); 797 | } 798 | 799 | base.Dispose(disposing); 800 | } 801 | 802 | /// 803 | /// StatusMessage 804 | /// 805 | public override string StatusMessage 806 | { 807 | get { return string.Empty; } 808 | } 809 | 810 | /// 811 | /// HasMoreData 812 | /// 813 | public override bool HasMoreData 814 | { 815 | get 816 | { 817 | return (this.Output.Count > 0 || 818 | this.Error.Count > 0 || 819 | this.Progress.Count > 0 || 820 | this.Verbose.Count > 0 || 821 | this.Debug.Count > 0 || 822 | this.Warning.Count > 0); 823 | } 824 | } 825 | 826 | /// 827 | /// Location 828 | /// 829 | public override string Location 830 | { 831 | get { return "PowerShell"; } 832 | } 833 | 834 | /// 835 | /// StopJob 836 | /// 837 | public override void StopJob() 838 | { 839 | _ps.Stop(); 840 | } 841 | 842 | /// 843 | /// ReportError 844 | /// 845 | /// 846 | public void ReportError(Exception e) 847 | { 848 | try 849 | { 850 | SetJobState(JobState.Failed); 851 | 852 | this.Error.Add( 853 | new ErrorRecord(e, "ThreadJobError", ErrorCategory.InvalidOperation, this)); 854 | } 855 | catch (ObjectDisposedException) 856 | { 857 | // Ignore. Thrown if Job is disposed (race condition.). 858 | } 859 | catch (PSInvalidOperationException) 860 | { 861 | // Ignore. Thrown if Error collection is closed (race condition.). 862 | } 863 | } 864 | 865 | #endregion 866 | 867 | #region Base class overrides 868 | 869 | /// 870 | /// OnStartJobCompleted 871 | /// 872 | /// 873 | protected override void OnStartJobCompleted(AsyncCompletedEventArgs eventArgs) 874 | { 875 | base.OnStartJobCompleted(eventArgs); 876 | } 877 | 878 | /// 879 | /// StartJobAsync 880 | /// 881 | public override void StartJobAsync() 882 | { 883 | this.StartJob(); 884 | this.OnStartJobCompleted( 885 | new AsyncCompletedEventArgs(null, false, this)); 886 | } 887 | 888 | /// 889 | /// StopJob 890 | /// 891 | /// 892 | /// 893 | public override void StopJob(bool force, string reason) 894 | { 895 | _ps.Stop(); 896 | } 897 | 898 | /// 899 | /// OnStopJobCompleted 900 | /// 901 | /// 902 | protected override void OnStopJobCompleted(AsyncCompletedEventArgs eventArgs) 903 | { 904 | base.OnStopJobCompleted(eventArgs); 905 | } 906 | 907 | /// 908 | /// StopJobAsync 909 | /// 910 | public override void StopJobAsync() 911 | { 912 | _ps.BeginStop((iasync) => { OnStopJobCompleted(new AsyncCompletedEventArgs(null, false, this)); }, null); 913 | } 914 | 915 | /// 916 | /// StopJobAsync 917 | /// 918 | /// 919 | /// 920 | public override void StopJobAsync(bool force, string reason) 921 | { 922 | _ps.BeginStop((iasync) => { OnStopJobCompleted(new AsyncCompletedEventArgs(null, false, this)); }, null); 923 | } 924 | 925 | #region Not implemented 926 | 927 | /// 928 | /// SuspendJob 929 | /// 930 | public override void SuspendJob() 931 | { 932 | throw new NotImplementedException(); 933 | } 934 | 935 | /// 936 | /// SuspendJob 937 | /// 938 | /// 939 | /// 940 | public override void SuspendJob(bool force, string reason) 941 | { 942 | throw new NotImplementedException(); 943 | } 944 | 945 | /// 946 | /// ResumeJobAsync 947 | /// 948 | public override void ResumeJobAsync() 949 | { 950 | throw new NotImplementedException(); 951 | } 952 | 953 | /// 954 | /// ResumeJob 955 | /// 956 | public override void ResumeJob() 957 | { 958 | throw new NotImplementedException(); 959 | } 960 | 961 | /// 962 | /// SuspendJobAsync 963 | /// 964 | public override void SuspendJobAsync() 965 | { 966 | throw new NotImplementedException(); 967 | } 968 | 969 | /// 970 | /// SuspendJobAsync 971 | /// 972 | /// 973 | /// 974 | public override void SuspendJobAsync(bool force, string reason) 975 | { 976 | throw new NotImplementedException(); 977 | } 978 | 979 | /// 980 | /// UnblockJobAsync 981 | /// 982 | public override void UnblockJobAsync() 983 | { 984 | throw new NotImplementedException(); 985 | } 986 | 987 | /// 988 | /// UnblockJob 989 | /// 990 | public override void UnblockJob() 991 | { 992 | throw new NotImplementedException(); 993 | } 994 | 995 | #endregion 996 | 997 | #endregion 998 | 999 | #region IJobDebugger 1000 | 1001 | /// 1002 | /// Job Debugger 1003 | /// 1004 | public Debugger Debugger 1005 | { 1006 | get 1007 | { 1008 | if (_jobDebugger == null && _rs.Debugger != null) 1009 | { 1010 | _jobDebugger = new ThreadJobDebugger(_rs.Debugger, this.Name); 1011 | } 1012 | 1013 | return _jobDebugger; 1014 | } 1015 | } 1016 | 1017 | /// 1018 | /// IsAsync 1019 | /// 1020 | public bool IsAsync 1021 | { 1022 | get; 1023 | set; 1024 | } 1025 | 1026 | #endregion 1027 | 1028 | #region Private methods 1029 | 1030 | // Private methods 1031 | private void RunScript() 1032 | { 1033 | _ps.Commands.Clear(); 1034 | _ps.AddScript(_sb.ToString()); 1035 | 1036 | if (_argumentList != null) 1037 | { 1038 | foreach (var arg in _argumentList) 1039 | { 1040 | _ps.AddArgument(arg); 1041 | } 1042 | } 1043 | 1044 | // Using variables 1045 | if (_usingValuesMap != null && _usingValuesMap.Count > 0) 1046 | { 1047 | _ps.AddParameter(VERBATIM_ARGUMENT, _usingValuesMap); 1048 | } 1049 | 1050 | _ps.BeginInvoke(_input, _output); 1051 | } 1052 | 1053 | private ScriptBlock GetScriptBlockFromFile(string filePath, PSCmdlet psCmdlet) 1054 | { 1055 | if (WildcardPattern.ContainsWildcardCharacters(filePath)) 1056 | { 1057 | throw new ArgumentException(Properties.Resources.FilePathWildcards); 1058 | } 1059 | 1060 | if (!filePath.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) 1061 | { 1062 | throw new ArgumentException(Properties.Resources.FilePathExt); 1063 | } 1064 | 1065 | ProviderInfo provider = null; 1066 | string resolvedPath = psCmdlet.GetResolvedProviderPathFromPSPath(filePath, out provider).FirstOrDefault(); 1067 | if (!string.IsNullOrEmpty(resolvedPath)) 1068 | { 1069 | Token[] tokens; 1070 | ParseError[] errors; 1071 | ScriptBlockAst scriptBlockAst = Parser.ParseFile(resolvedPath, out tokens, out errors); 1072 | if (scriptBlockAst != null && errors.Length == 0) 1073 | { 1074 | return scriptBlockAst.GetScriptBlock(); 1075 | } 1076 | 1077 | foreach (var error in errors) 1078 | { 1079 | this.Error.Add( 1080 | new ErrorRecord( 1081 | new ParseException(error.Message), "ThreadJobError", ErrorCategory.InvalidData, this)); 1082 | } 1083 | } 1084 | 1085 | return null; 1086 | } 1087 | 1088 | private void SetJobState(JobState jobState, Exception reason, bool disposeRunspace = false) 1089 | { 1090 | base.SetJobState(jobState, reason); 1091 | if (disposeRunspace) 1092 | { 1093 | _rs.Dispose(); 1094 | } 1095 | } 1096 | 1097 | private static Dictionary GetUsingValuesAsDictionary(IEnumerable usingAsts, PSCmdlet psCmdlet) 1098 | { 1099 | Dictionary usingValues = new Dictionary(); 1100 | 1101 | foreach (var usingAst in usingAsts) 1102 | { 1103 | var varAst = usingAst.SubExpression as VariableExpressionAst; 1104 | if (varAst == null) 1105 | { 1106 | var msg = string.Format(CultureInfo.InvariantCulture, 1107 | Properties.Resources.UsingNotVariableExpression, 1108 | new object[] { usingAst.Extent.Text }); 1109 | throw new PSInvalidOperationException(msg); 1110 | } 1111 | 1112 | try 1113 | { 1114 | var usingValue = psCmdlet.GetVariableValue(varAst.VariablePath.UserPath); 1115 | var usingKey = GetUsingExpressionKey(usingAst); 1116 | if (!usingValues.ContainsKey(usingKey)) 1117 | { 1118 | usingValues.Add(usingKey, usingValue); 1119 | } 1120 | } 1121 | catch (Exception ex) 1122 | { 1123 | var msg = string.Format(CultureInfo.InvariantCulture, 1124 | Properties.Resources.UsingVariableNotFound, 1125 | new object[] { usingAst.Extent.Text }); 1126 | throw new PSInvalidOperationException(msg, ex); 1127 | } 1128 | } 1129 | 1130 | return usingValues; 1131 | } 1132 | 1133 | /// 1134 | /// This method creates a dictionary key for a Using expression value that is bound to 1135 | /// a thread job script block parameter. PowerShell version 5.0+ recognizes this and performs 1136 | /// the correct Using parameter argument binding. 1137 | /// 1138 | /// A using expression 1139 | /// Base64 encoded string as the key of the UsingExpressionAst 1140 | private static string GetUsingExpressionKey(UsingExpressionAst usingAst) 1141 | { 1142 | string usingAstText = usingAst.ToString(); 1143 | if (usingAst.SubExpression is VariableExpressionAst) 1144 | { 1145 | usingAstText = usingAstText.ToLowerInvariant(); 1146 | } 1147 | 1148 | return Convert.ToBase64String(Encoding.Unicode.GetBytes(usingAstText.ToCharArray())); 1149 | } 1150 | 1151 | #endregion 1152 | } 1153 | 1154 | /// 1155 | /// ThreadJobQueue 1156 | /// 1157 | internal sealed class ThreadJobQueue 1158 | { 1159 | #region Private members 1160 | 1161 | // Private members 1162 | ConcurrentQueue _jobQueue = new ConcurrentQueue(); 1163 | object _syncObject = new object(); 1164 | int _throttleLimit = 5; 1165 | int _currentJobs; 1166 | bool _haveRunningJobs; 1167 | private ManualResetEvent _processJobsHandle = new ManualResetEvent(true); 1168 | 1169 | #endregion 1170 | 1171 | #region Constructors 1172 | 1173 | /// 1174 | /// Constructor 1175 | /// 1176 | public ThreadJobQueue() 1177 | { } 1178 | 1179 | /// 1180 | /// Constructor 1181 | /// 1182 | /// 1183 | public ThreadJobQueue(int throttleLimit) 1184 | { 1185 | _throttleLimit = throttleLimit; 1186 | } 1187 | 1188 | #endregion 1189 | 1190 | #region Public properties 1191 | 1192 | /// 1193 | /// ThrottleLimit 1194 | /// 1195 | public int ThrottleLimit 1196 | { 1197 | get { return _throttleLimit; } 1198 | set 1199 | { 1200 | if (value > 0) 1201 | { 1202 | lock (_syncObject) 1203 | { 1204 | _throttleLimit = value; 1205 | if (_currentJobs < _throttleLimit) 1206 | { 1207 | _processJobsHandle.Set(); 1208 | } 1209 | } 1210 | } 1211 | } 1212 | } 1213 | 1214 | /// 1215 | /// CurrentJobs 1216 | /// 1217 | public int CurrentJobs 1218 | { 1219 | get { return _currentJobs; } 1220 | } 1221 | 1222 | /// 1223 | /// Count 1224 | /// 1225 | public int Count 1226 | { 1227 | get { return _jobQueue.Count; } 1228 | } 1229 | 1230 | #endregion 1231 | 1232 | #region Public methods 1233 | 1234 | /// 1235 | /// EnqueueJob 1236 | /// 1237 | /// 1238 | /// 1239 | public void EnqueueJob(ThreadJob job, int throttleLimit) 1240 | { 1241 | if (job == null) 1242 | { 1243 | throw new ArgumentNullException("job"); 1244 | } 1245 | 1246 | ThrottleLimit = throttleLimit; 1247 | job.StateChanged += new EventHandler(HandleJobStateChanged); 1248 | 1249 | lock (_syncObject) 1250 | { 1251 | _jobQueue.Enqueue(job); 1252 | 1253 | if (_haveRunningJobs) 1254 | { 1255 | return; 1256 | } 1257 | 1258 | if (_jobQueue.Count > 0) 1259 | { 1260 | _haveRunningJobs = true; 1261 | System.Threading.ThreadPool.QueueUserWorkItem(new WaitCallback(ServiceJobs)); 1262 | } 1263 | } 1264 | } 1265 | 1266 | #endregion 1267 | 1268 | #region Private methods 1269 | 1270 | private void HandleJobStateChanged(object sender, JobStateEventArgs e) 1271 | { 1272 | ThreadJob job = sender as ThreadJob; 1273 | JobState state = e.JobStateInfo.State; 1274 | if (state == JobState.Completed || 1275 | state == JobState.Stopped || 1276 | state == JobState.Failed) 1277 | { 1278 | job.StateChanged -= new EventHandler(HandleJobStateChanged); 1279 | DecrementCurrentJobs(); 1280 | } 1281 | } 1282 | 1283 | private void IncrementCurrentJobs() 1284 | { 1285 | lock (_syncObject) 1286 | { 1287 | if (++_currentJobs >= _throttleLimit) 1288 | { 1289 | _processJobsHandle.Reset(); 1290 | } 1291 | } 1292 | } 1293 | 1294 | private void DecrementCurrentJobs() 1295 | { 1296 | lock (_syncObject) 1297 | { 1298 | if ((_currentJobs > 0) && 1299 | (--_currentJobs < _throttleLimit)) 1300 | { 1301 | _processJobsHandle.Set(); 1302 | } 1303 | } 1304 | } 1305 | 1306 | private void ServiceJobs(object toProcess) 1307 | { 1308 | while (true) 1309 | { 1310 | lock (_syncObject) 1311 | { 1312 | if (_jobQueue.Count == 0) 1313 | { 1314 | _haveRunningJobs = false; 1315 | return; 1316 | } 1317 | } 1318 | 1319 | _processJobsHandle.WaitOne(); 1320 | 1321 | ThreadJob job; 1322 | if (_jobQueue.TryDequeue(out job)) 1323 | { 1324 | try 1325 | { 1326 | // Start job running on its own thread/runspace. 1327 | IncrementCurrentJobs(); 1328 | job.StartJob(); 1329 | } 1330 | catch (Exception e) 1331 | { 1332 | DecrementCurrentJobs(); 1333 | job.ReportError(e); 1334 | } 1335 | } 1336 | } 1337 | } 1338 | 1339 | #endregion 1340 | } 1341 | } 1342 | -------------------------------------------------------------------------------- /PSThreadJob/PSThreadJob.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | ThreadJob 6 | Microsoft.PowerShell.ThreadJob 7 | 2.0.1.0 8 | 2.0.1 9 | 2.0.1 10 | net461;netcoreapp2.1 11 | 12 | 13 | 14 | False 15 | C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /PSThreadJob/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ThreadJob.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ThreadJob.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Unable to parse script file.. 65 | /// 66 | internal static string CannotParseScriptFile { 67 | get { 68 | return ResourceManager.GetString("CannotParseScriptFile", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Cannot start job because it is not in NotStarted state.. 74 | /// 75 | internal static string CannotStartJob { 76 | get { 77 | return ResourceManager.GetString("CannotStartJob", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Invalid file path extension. Extension should be .ps1.. 83 | /// 84 | internal static string FilePathExt { 85 | get { 86 | return ResourceManager.GetString("FilePathExt", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to FilePath cannot contain wildcards.. 92 | /// 93 | internal static string FilePathWildcards { 94 | get { 95 | return ResourceManager.GetString("FilePathWildcards", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to No script block or script file was provided for the job to run.. 101 | /// 102 | internal static string NoScriptToRun { 103 | get { 104 | return ResourceManager.GetString("NoScriptToRun", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to Cannot get the value of the Using expression {0}. Start-ThreadJob only supports using variable expressions.. 110 | /// 111 | internal static string UsingNotVariableExpression { 112 | get { 113 | return ResourceManager.GetString("UsingNotVariableExpression", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to Unable to find Using variable {0}.. 119 | /// 120 | internal static string UsingVariableNotFound { 121 | get { 122 | return ResourceManager.GetString("UsingVariableNotFound", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to Cannot run trusted script file {0} in FullLanguage mode because an initialization script block is included in the job, and the script block is not trusted.. 128 | /// 129 | internal static string CannotRunTrustedFileInFL{ 130 | get { 131 | return ResourceManager.GetString("CannotRunTrustedFileInFL", resourceCulture); 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /PSThreadJob/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Unable to parse script file. 122 | 123 | 124 | Cannot start job because it is not in NotStarted state. 125 | 126 | 127 | Invalid file path extension. Extension should be .ps1. 128 | 129 | 130 | FilePath cannot contain wildcards. 131 | 132 | 133 | No script block or script file was provided for the job to run. 134 | 135 | 136 | Cannot get the value of the Using expression {0}. Start-ThreadJob only supports using variable expressions. 137 | 138 | 139 | Unable to find Using variable {0}. 140 | 141 | 142 | Cannot run trusted script file {0} in FullLanguage mode because an initialization script block is included in the job, and the script block is not trusted. 143 | 144 | -------------------------------------------------------------------------------- /PSThreadJob/ThreadJob.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'ThreadJob' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = '.\Microsoft.PowerShell.ThreadJob.dll' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '2.0.3' 12 | 13 | # ID used to uniquely identify this module 14 | GUID = '0e7b895d-2fec-43f7-8cae-11e8d16f6e40' 15 | 16 | Author = 'Microsoft Corporation' 17 | CompanyName = 'Microsoft Corporation' 18 | Copyright = '(c) Microsoft Corporation. All rights reserved.' 19 | 20 | # Description of the functionality provided by this module 21 | Description = " 22 | PowerShell's built-in BackgroundJob jobs (Start-Job) are run in separate processes on the local machine. 23 | They provide excellent isolation but are resource heavy. Running hundreds of BackgroundJob jobs can quickly 24 | absorb system resources. 25 | 26 | This module extends the existing PowerShell BackgroundJob to include a new thread based ThreadJob job. This is a 27 | lighter weight solution for running concurrent PowerShell scripts that works within the existing PowerShell job 28 | infrastructure. 29 | 30 | ThreadJob jobs will tend to run quicker because there is lower overhead and they do not use the remoting serialization 31 | system. And they will use up fewer system resources. In addition output objects returned from the job will be 32 | 'live' since they are not re-hydrated from the serialization system. However, there is less isolation. If one 33 | ThreadJob job crashes the process then all ThreadJob jobs running in that process will be terminated. 34 | 35 | This module exports a single cmdlet, Start-ThreadJob, which works similarly to the existing Start-Job cmdlet. 36 | The main difference is that the jobs which are created run in separate threads within the local process. 37 | 38 | One difference is that ThreadJob jobs support a ThrottleLimit parameter to limit the number of running jobs, 39 | and thus active threads, at a time. If more jobs are started then they go into a queue and wait until the current 40 | number of jobs drops below the throttle limit. 41 | 42 | Source for this module is at GitHub. Please submit any issues there. 43 | https://github.com/PaulHigin/PSThreadJob 44 | 45 | Added Runspace cleanup. 46 | Added Using variable expression support. 47 | Added StreamingHost parameter to stream host data writes to a provided host object. 48 | Added Information stream handling. 49 | Bumped version to 2.0.0, and now only support PowerShell version 5.1 and higher. 50 | Fixed using keyword bug with PowerShell preview version, and removed unneeded version check. 51 | Added setting current working directory to running jobs, when available. 52 | Added help URI to module. 53 | " 54 | 55 | # Minimum version of the Windows PowerShell engine required by this module 56 | PowerShellVersion = '5.1' 57 | 58 | # Cmdlets to export from this module 59 | CmdletsToExport = 'Start-ThreadJob' 60 | 61 | HelpInfoURI = 'https://go.microsoft.com/fwlink/?linkid=2113345' 62 | 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSThreadJob 2 | A PowerShell module for running concurrent jobs based on threads rather than processes 3 | 4 | PowerShell's built-in BackgroundJob jobs (Start-Job) are run in separate processes on the local machine. They provide excellent isolation but are resource heavy. Running hundreds of BackgroundJob jobs can quickly absorb system resources by creating hundreds of processes. There is no throttling mechanism and so all jobs are started immediately and are all run currently. 5 | 6 | This module extends the existing PowerShell BackgroundJob to include a new thread based ThreadJob job. It is a lighter weight solution for running concurrent PowerShell scripts that works within the existing PowerShell job infrastructure. So these jobs work with existing PowerShell job cmdlets. 7 | 8 | ThreadJob jobs will tend to run much faster because there is lower overhead and they do not use the remoting serialization system as BackgroundJob jobs do. And they will use up fewer system resources. In addition output objects returned from the job will be 'live' since they are not re-hydrated from the serialization system. However, there is less isolation. If one ThreadJob job crashes the process then all ThreadJob jobs running in that process will be terminated. 9 | 10 | This module exports a single cmdlet, Start-ThreadJob, which works similarly to the existing Start-Job cmdlet. The main difference is that the jobs which are created run in separate threads within the local process. 11 | 12 | Also ThreadJob jobs support a ThrottleLimit parameter to limit the number of running jobs, and thus running threads, at a time. If more jobs are started then they go into a queue and wait until the current number of jobs drops below the throttle limit. 13 | 14 | ## Examples 15 | 16 | ```powershell 17 | PS C:\> Start-ThreadJob -ScriptBlock { 1..100 | % { sleep 1; "Output $_" } } -ThrottleLimit 2 18 | PS C:\> Start-ThreadJob -ScriptBlock { 1..100 | % { sleep 1; "Output $_" } } 19 | PS C:\> Start-ThreadJob -ScriptBlock { 1..100 | % { sleep 1; "Output $_" } } 20 | PS C:\> get-job 21 | 22 | Id Name PSJobTypeName State HasMoreData Location Command 23 | -- ---- ------------- ----- ----------- -------- ------- 24 | 1 Job1 ThreadJob Running True PowerShell 1..100 | % { sleep 1;... 25 | 2 Job2 ThreadJob Running True PowerShell 1..100 | % { sleep 1;... 26 | 3 Job3 ThreadJob NotStarted False PowerShell 1..100 | % { sleep 1;... 27 | ``` 28 | 29 | ```powershell 30 | PS C:\> $job = Start-ThreadJob { Get-Process -id $pid } 31 | PS C:\> $myProc = Receive-Job $job 32 | # !!Don't do this. $myProc is a live object!! 33 | PS C:\> $myProc.Kill() 34 | ``` 35 | 36 | ```powershell 37 | # start five background jobs each running 1 second 38 | PS C:\> Measure-Command {1..5 | % {Start-Job {Sleep 1}} | Wait-Job} | Select TotalSeconds 39 | PS C:\> Measure-Command {1..5 | % {Start-ThreadJob {Sleep 1}} | Wait-Job} | Select TotalSeconds 40 | 41 | TotalSeconds 42 | ------------ 43 | 5.7665849 # jobs creation time > 4.7 sec; results may vary 44 | 1.5735008 # jobs creation time < 0.6 sec (8 times less!) 45 | ``` 46 | 47 | ## Installing 48 | 49 | You can install this module from [PowerShell Gallery](https://www.powershellgallery.com/packages/ThreadJob/1.1.2) using this command: 50 | 51 | ```powershell 52 | Install-Module -Name ThreadJob -Scope CurrentUser 53 | ``` 54 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | A script that provides simple entry points for bootstrapping, building and testing. 4 | .DESCRIPTION 5 | A script to make it easy to bootstrap, build and run tests. 6 | .EXAMPLE 7 | PS > .\build.ps1 -Bootstrap 8 | Check and install prerequisites for the build. 9 | .EXAMPLE 10 | PS > .\build.ps1 -Configuration Release -Framework net461 11 | Build the main module with 'Release' configuration and targeting 'net461'. 12 | .EXAMPLE 13 | PS > .\build.ps1 14 | Build the main module with the default configuration (Debug) and the default target framework (determined by the current session). 15 | .EXAMPLE 16 | PS > .\build.ps1 -Test 17 | Run xUnit tests with the default configuration (Debug) and the default target framework (determined by the current session). 18 | .PARAMETER Clean 19 | Clean the local repo, but keep untracked files. 20 | .PARAMETER Bootstrap 21 | Check and install the build prerequisites. 22 | .PARAMETER Test 23 | Run tests. 24 | .PARAMETER Configuration 25 | The configuration setting for the build. The default value is 'Debug'. 26 | .PARAMETER Framework 27 | The target framework for the build. 28 | When not specified, the target framework is determined by the current PowerShell session: 29 | - If the current session is PowerShell Core, then use 'netcoreapp2.1' as the default target framework. 30 | - If the current session is Windows PowerShell, then use 'net461' as the default target framework. 31 | #> 32 | [CmdletBinding()] 33 | param( 34 | [switch] 35 | $Clean, 36 | 37 | [switch] 38 | $Bootstrap, 39 | 40 | [switch] 41 | $Test, 42 | 43 | [ValidateSet("Debug", "Release")] 44 | [string] 45 | $Configuration = "Debug", 46 | 47 | [ValidateSet("net461", "netcoreapp2.1")] 48 | [string] 49 | $Framework 50 | ) 51 | 52 | # Clean step 53 | if ($Clean) { 54 | try { 55 | Push-Location $PSScriptRoot 56 | git clean -fdX 57 | return 58 | } finally { 59 | Pop-Location 60 | } 61 | } 62 | 63 | Import-Module "$PSScriptRoot/tools/helper.psm1" 64 | 65 | if ($Bootstrap) { 66 | Write-Log "Validate and install missing prerequisits for building ..." 67 | 68 | Install-Dotnet 69 | return 70 | } 71 | 72 | function Invoke-Build 73 | { 74 | param ( 75 | [string] $Configuration, 76 | [string] $Framework 77 | ) 78 | 79 | $sourcePath = Join-Path $PSScriptRoot PSThreadJob 80 | Push-Location $sourcePath 81 | try { 82 | Write-Log "Building PSThreadJob binary..." 83 | dotnet publish --configuration $Configuration --framework $Framework --output bin 84 | 85 | Write-Log "Create Signed signing destination directory..." 86 | $signedPath = Join-Path . "bin\$Configuration\Signed" 87 | if (! (Test-Path $signedPath)) 88 | { 89 | $null = New-Item -Path $signedPath -ItemType Directory 90 | } 91 | 92 | Write-Log "Creating PSThreadJob signing source directory..." 93 | $destPath = Join-Path . "bin\$Configuration\PSThreadJob" 94 | if (! (Test-Path $destPath)) 95 | { 96 | $null = New-Item -Path $destPath -ItemType Directory 97 | } 98 | 99 | Write-Log "Copying ThreadJob.psd1 file for signing to $destPath..." 100 | $psd1FilePath = Join-Path . ThreadJob.psd1 101 | Copy-Item -Path $psd1FilePath -Destination $destPath -Force 102 | 103 | Write-Log "Copying Microsoft.PowerShell.ThreadJob.dll file for signing to $destPath..." 104 | $binFilePath = Join-Path . "bin\$Configuration\$Framework\Microsoft.PowerShell.ThreadJob.dll" 105 | Copy-Item -Path $binFilePath -Destination $destPath -Force 106 | } 107 | finally { 108 | Pop-Location 109 | } 110 | } 111 | 112 | # Build/Test step 113 | # $buildTask = if ($Test) { "RunTests" } else { "ZipRelease" } 114 | 115 | $arguments = @{ Configuration = $Configuration } 116 | if ($Framework) { $arguments.Add("Framework", $Framework) } 117 | Invoke-Build @arguments 118 | -------------------------------------------------------------------------------- /test/ThreadJob.CL.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | ## 5 | ## ---------- 6 | ## Test Note: 7 | ## ---------- 8 | ## Since these tests change session and system state (constrained language and system lockdown) 9 | ## they will all use try/finally blocks instead of Pester AfterEach/AfterAll to ensure session 10 | ## and system state is restored. 11 | ## Pester AfterEach, AfterAll is not reliable when the session is constrained language or locked down. 12 | ## 13 | 14 | function Get-RandomFileName 15 | { 16 | [System.IO.Path]::GetFileNameWithoutExtension([IO.Path]::GetRandomFileName()) 17 | } 18 | 19 | if ($IsWindows) 20 | { 21 | $code = @' 22 | 23 | #region Using directives 24 | 25 | using System; 26 | using System.Management.Automation; 27 | 28 | #endregion 29 | 30 | /// Adds a new type to the Application Domain 31 | [Cmdlet("Invoke", "LanguageModeTestingSupportCmdlet")] 32 | public sealed class InvokeLanguageModeTestingSupportCmdlet : PSCmdlet 33 | { 34 | [Parameter()] 35 | public SwitchParameter EnableFullLanguageMode { get; set; } 36 | 37 | [Parameter()] 38 | public SwitchParameter SetLockdownMode { get; set; } 39 | 40 | [Parameter()] 41 | public SwitchParameter RevertLockdownMode { get; set; } 42 | 43 | protected override void BeginProcessing() 44 | { 45 | if (EnableFullLanguageMode) 46 | { 47 | SessionState.LanguageMode = PSLanguageMode.FullLanguage; 48 | } 49 | 50 | if (SetLockdownMode) 51 | { 52 | Environment.SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", EnvironmentVariableTarget.Machine); 53 | } 54 | 55 | if (RevertLockdownMode) 56 | { 57 | Environment.SetEnvironmentVariable("__PSLockdownPolicy", null, EnvironmentVariableTarget.Machine); 58 | } 59 | } 60 | } 61 | '@ 62 | 63 | if (-not (Get-Command Invoke-LanguageModeTestingSupportCmdlet -ErrorAction Ignore)) 64 | { 65 | $moduleName = Get-RandomFileName 66 | $moduleDirectory = join-path $TestDrive\Modules $moduleName 67 | if (-not (Test-Path $moduleDirectory)) 68 | { 69 | $null = New-Item -ItemType Directory $moduleDirectory -Force 70 | } 71 | 72 | try 73 | { 74 | Add-Type -TypeDefinition $code -OutputAssembly $moduleDirectory\TestCmdletForConstrainedLanguage.dll -ErrorAction Ignore 75 | } catch {} 76 | 77 | Import-Module -Name $moduleDirectory\TestCmdletForConstrainedLanguage.dll 78 | } 79 | } 80 | 81 | try 82 | { 83 | $defaultParamValues = $PSDefaultParameterValues.Clone() 84 | $PSDefaultParameterValues["it:Skip"] = !$IsWindows 85 | 86 | Describe "ThreadJob Constrained Language Tests" -Tags 'Feature','RequireAdminOnWindows' { 87 | 88 | BeforeAll { 89 | 90 | $sb = { $ExecutionContext.SessionState.LanguageMode } 91 | 92 | $scriptTrustedFilePath = Join-Path $TestDrive "ThJobTrusted_System32.ps1" 93 | @' 94 | Write-Output $ExecutionContext.SessionState.LanguageMode 95 | '@ | Out-File -FilePath $scriptTrustedFilePath 96 | 97 | $scriptUntrustedFilePath = Join-Path $TestDrive "ThJobUntrusted.ps1" 98 | @' 99 | Write-Output $ExecutionContext.SessionState.LanguageMode 100 | '@ | Out-File -FilePath $scriptUntrustedFilePath 101 | } 102 | 103 | AfterAll { 104 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 105 | } 106 | 107 | It "ThreadJob script must run in ConstrainedLanguage mode with system lock down" { 108 | 109 | try 110 | { 111 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 112 | Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode 113 | 114 | $results = Start-ThreadJob -ScriptBlock { $ExecutionContext.SessionState.LanguageMode } | Wait-Job | Receive-Job 115 | } 116 | finally 117 | { 118 | Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode 119 | } 120 | 121 | $results | Should -BeExactly "ConstrainedLanguage" 122 | } 123 | 124 | It "ThreadJob script block using variable must run in ConstrainedLanguage mode with system lock down" { 125 | 126 | try 127 | { 128 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 129 | Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode 130 | 131 | $results = Start-ThreadJob -ScriptBlock { & $using:sb } | Wait-Job | Receive-Job 132 | } 133 | finally 134 | { 135 | Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode 136 | } 137 | 138 | $results | Should -BeExactly "ConstrainedLanguage" 139 | } 140 | 141 | It "ThreadJob script block argument variable must run in ConstrainedLanguage mode with system lock down" { 142 | 143 | try 144 | { 145 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 146 | Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode 147 | 148 | $results = Start-ThreadJob -ScriptBlock { param ($sb) & $sb } -ArgumentList $sb | Wait-Job | Receive-Job 149 | } 150 | finally 151 | { 152 | Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode 153 | } 154 | 155 | $results | Should -BeExactly "ConstrainedLanguage" 156 | } 157 | 158 | It "ThreadJob script block piped variable must run in ConstrainedLanguage mode with system lock down" { 159 | 160 | try 161 | { 162 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 163 | Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode 164 | 165 | $results = $sb | Start-ThreadJob -ScriptBlock { $input | ForEach-Object { & $_ } } | Wait-Job | Receive-Job 166 | } 167 | finally 168 | { 169 | Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode 170 | } 171 | 172 | $results | Should -BeExactly "ConstrainedLanguage" 173 | } 174 | 175 | It "ThreadJob untrusted script file must run in ConstrainedLanguage mode with system lock down" { 176 | try 177 | { 178 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 179 | Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode 180 | 181 | $results = Start-ThreadJob -File $scriptUntrustedFilePath | Wait-Job | Receive-Job 182 | } 183 | finally 184 | { 185 | Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode 186 | } 187 | 188 | $results | Should -BeExactly "ConstrainedLanguage" 189 | } 190 | 191 | It "ThreadJob trusted script file must run in FullLanguage mode with system lock down" { 192 | try 193 | { 194 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 195 | Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode 196 | 197 | $results = Start-ThreadJob -File $scriptTrustedFilePath | Wait-Job | Receive-Job 198 | } 199 | finally 200 | { 201 | Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode 202 | } 203 | 204 | $results | Should -BeExactly "FullLanguage" 205 | } 206 | 207 | It "ThreadJob trusted script file *with* untrusted initialization script must run in ConstrainedLanguage mode with system lock down" { 208 | try 209 | { 210 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 211 | Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode 212 | 213 | $results = Start-ThreadJob -File $scriptTrustedFilePath -InitializationScript { "Hello" } | Wait-Job | Receive-Job 3>&1 214 | } 215 | finally 216 | { 217 | Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode 218 | } 219 | 220 | $results.Count | Should -BeExactly 3 -Because "Includes init script, file script, warning output" 221 | $results[0] | Should -BeExactly "Hello" -Because "This is the expected initialization script output" 222 | $results[1] | Should -BeExactly "ConstrainedLanguage" -Because "This is the expected script file language mode" 223 | } 224 | } 225 | } 226 | finally 227 | { 228 | if ($defaultParamValues -ne $null) 229 | { 230 | $Global:PSDefaultParameterValues = $defaultParamValues 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /test/ThreadJob.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | # Helper function to wait for job to reach a running or completed state 5 | # Job state can go to "Running" before the underlying runspace thread is running 6 | # so we always first wait 100 mSec before checking state. 7 | function Wait-ForJobRunning 8 | { 9 | param ( 10 | $job 11 | ) 12 | 13 | $iteration = 10 14 | Do 15 | { 16 | Start-Sleep -Milliseconds 100 17 | } 18 | Until (($job.State -match "Running|Completed|Failed") -or (--$iteration -eq 0)) 19 | 20 | if ($job.State -notmatch "Running|Completed|Failed") 21 | { 22 | throw ("Cannot start job '{0}'. Job state is '{1}'" -f $job,$job.State) 23 | } 24 | } 25 | 26 | Describe 'Basic ThreadJob Tests' -Tags 'CI' { 27 | 28 | BeforeAll { 29 | 30 | $scriptFilePath1 = Join-Path $testdrive "TestThreadJobFile1.ps1" 31 | @' 32 | for ($i=0; $i -lt 10; $i++) 33 | { 34 | Write-Output "Hello $i" 35 | } 36 | '@ > $scriptFilePath1 37 | 38 | $scriptFilePath2 = Join-Path $testdrive "TestThreadJobFile2.ps1" 39 | @' 40 | param ($arg1, $arg2) 41 | Write-Output $arg1 42 | Write-Output $arg2 43 | '@ > $scriptFilePath2 44 | 45 | $scriptFilePath3 = Join-Path $testdrive "TestThreadJobFile3.ps1" 46 | @' 47 | $input | foreach { 48 | Write-Output $_ 49 | } 50 | '@ > $scriptFilePath3 51 | 52 | $scriptFilePath4 = Join-Path $testdrive "TestThreadJobFile4.ps1" 53 | @' 54 | Write-Output $using:Var1 55 | Write-Output $($using:Array1)[2] 56 | Write-Output @(,$using:Array1) 57 | '@ > $scriptFilePath4 58 | 59 | $scriptFilePath5 = Join-Path $testdrive "TestThreadJobFile5.ps1" 60 | @' 61 | param ([string]$param1) 62 | Write-Output "$param1 $using:Var1 $using:Var2" 63 | '@ > $scriptFilePath5 64 | 65 | $WaitForCountFnScript = @' 66 | function Wait-ForExpectedRSCount 67 | { 68 | param ( 69 | $expectedRSCount 70 | ) 71 | 72 | $iteration = 20 73 | while ((@(Get-Runspace).Count -ne $expectedRSCount) -and ($iteration-- -gt 0)) 74 | { 75 | Start-Sleep -Milliseconds 100 76 | } 77 | } 78 | '@ 79 | } 80 | 81 | AfterEach { 82 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 83 | } 84 | 85 | It 'ThreadJob with ScriptBlock' { 86 | 87 | $job = Start-ThreadJob -ScriptBlock { "Hello" } 88 | $results = $job | Receive-Job -Wait 89 | $results | Should -Be "Hello" 90 | } 91 | 92 | It 'ThreadJob with ScriptBlock and Initialization script' { 93 | 94 | $job = Start-ThreadJob -ScriptBlock { "Goodbye" } -InitializationScript { "Hello" } 95 | $results = $job | Receive-Job -Wait 96 | $results[0] | Should -Be "Hello" 97 | $results[1] | Should -Be "Goodbye" 98 | } 99 | 100 | It 'ThreadJob with ScriptBlock and Argument list' { 101 | 102 | $job = Start-ThreadJob -ScriptBlock { param ($arg1, $arg2) $arg1; $arg2 } -ArgumentList @("Hello","Goodbye") 103 | $results = $job | Receive-Job -Wait 104 | $results[0] | Should -Be "Hello" 105 | $results[1] | Should -Be "Goodbye" 106 | } 107 | 108 | It 'ThreadJob with ScriptBlock and piped input' { 109 | 110 | $job = "Hello","Goodbye" | Start-ThreadJob -ScriptBlock { $input | ForEach-Object { $_ } } 111 | $results = $job | Receive-Job -Wait 112 | $results[0] | Should -Be "Hello" 113 | $results[1] | Should -Be "Goodbye" 114 | } 115 | 116 | It 'ThreadJob with ScriptBlock and Using variables' { 117 | 118 | $Var1 = "Hello" 119 | $Var2 = "Goodbye" 120 | $Var3 = 102 121 | $Var4 = 1..5 122 | $global:GVar1 = "GlobalVar" 123 | $job = Start-ThreadJob -ScriptBlock { 124 | Write-Output $using:Var1 125 | Write-Output $using:Var2 126 | Write-Output $using:Var3 127 | Write-Output ($using:Var4)[1] 128 | Write-Output @(,$using:Var4) 129 | Write-Output $using:GVar1 130 | } 131 | 132 | $results = $job | Receive-Job -Wait 133 | $results[0] | Should -Be $Var1 134 | $results[1] | Should -Be $Var2 135 | $results[2] | Should -Be $Var3 136 | $results[3] | Should -Be 2 137 | $results[4] | Should -Be $Var4 138 | $results[5] | Should -Be $global:GVar1 139 | } 140 | 141 | It 'ThreadJob with ScriptBlock and Using variables and argument list' { 142 | 143 | $Var1 = "Hello" 144 | $Var2 = 52 145 | $job = Start-ThreadJob -ScriptBlock { 146 | param ([string] $param1) 147 | 148 | "$using:Var1 $param1 $using:Var2" 149 | } -ArgumentList "There" 150 | 151 | $results = $job | Receive-Job -Wait 152 | $results | Should -Be "Hello There 52" 153 | } 154 | 155 | It 'ThreadJob with ScriptFile' { 156 | 157 | $job = Start-ThreadJob -FilePath $scriptFilePath1 158 | $results = $job | Receive-Job -Wait 159 | $results | Should -HaveCount 10 160 | $results[9] | Should -Be "Hello 9" 161 | } 162 | 163 | It 'ThreadJob with ScriptFile and Initialization script' { 164 | 165 | $job = Start-ThreadJob -FilePath $scriptFilePath1 -Initialization { "Goodbye" } 166 | $results = $job | Receive-Job -Wait 167 | $results | Should -HaveCount 11 168 | $results[0] | Should -Be "Goodbye" 169 | } 170 | 171 | It 'ThreadJob with ScriptFile and Argument list' { 172 | 173 | $job = Start-ThreadJob -FilePath $scriptFilePath2 -ArgumentList @("Hello","Goodbye") 174 | $results = $job | Receive-Job -Wait 175 | $results[0] | Should -Be "Hello" 176 | $results[1] | Should -Be "Goodbye" 177 | } 178 | 179 | It 'ThreadJob with ScriptFile and piped input' { 180 | 181 | $job = "Hello","Goodbye" | Start-ThreadJob -FilePath $scriptFilePath3 182 | $results = $job | Receive-Job -Wait 183 | $results[0] | Should -Be "Hello" 184 | $results[1] | Should -Be "Goodbye" 185 | } 186 | 187 | It 'ThreadJob with ScriptFile and Using variables' { 188 | 189 | $Var1 = "Hello!" 190 | $Array1 = 1..10 191 | 192 | $job = Start-ThreadJob -FilePath $scriptFilePath4 193 | $results = $job | Receive-Job -Wait 194 | $results[0] | Should -Be $Var1 195 | $results[1] | Should -Be 3 196 | $results[2] | Should -Be $Array1 197 | } 198 | 199 | It 'ThreadJob with ScriptFile and Using variables with argument list' { 200 | 201 | $Var1 = "There" 202 | $Var2 = 60 203 | $job = Start-ThreadJob -FilePath $scriptFilePath5 -ArgumentList "Hello" 204 | $results = $job | Receive-Job -Wait 205 | $results | Should -Be "Hello There 60" 206 | } 207 | 208 | It 'ThreadJob with terminating error' { 209 | 210 | $job = Start-ThreadJob -ScriptBlock { throw "MyError!" } 211 | $job | Wait-Job 212 | $job.JobStateInfo.Reason.Message | Should -Be "MyError!" 213 | } 214 | 215 | It 'ThreadJob and Error stream output' { 216 | 217 | $job = Start-ThreadJob -ScriptBlock { Write-Error "ErrorOut" } | Wait-Job 218 | $job.Error | Should -Be "ErrorOut" 219 | } 220 | 221 | It 'ThreadJob and Warning stream output' { 222 | 223 | $job = Start-ThreadJob -ScriptBlock { Write-Warning "WarningOut" } | Wait-Job 224 | $job.Warning | Should -Be "WarningOut" 225 | } 226 | 227 | It 'ThreadJob and Verbose stream output' { 228 | 229 | $job = Start-ThreadJob -ScriptBlock { $VerbosePreference = 'Continue'; Write-Verbose "VerboseOut" } | Wait-Job 230 | $job.Verbose | Should Match "VerboseOut" 231 | } 232 | 233 | It 'ThreadJob and Verbose stream output' { 234 | 235 | $job = Start-ThreadJob -ScriptBlock { $DebugPreference = 'Continue'; Write-Debug "DebugOut" } | Wait-Job 236 | $job.Debug | Should -Be "DebugOut" 237 | } 238 | 239 | It 'ThreadJob and Information stream output' { 240 | 241 | $job = Start-ThreadJob -ScriptBlock { Write-Information "InformationOutput" -InformationAction Continue } | Wait-Job 242 | $job.Information | Should -Be "InformationOutput" 243 | } 244 | 245 | It 'ThreadJob and Host stream output' { 246 | 247 | # Host stream data is automatically added to the Information stream 248 | $job = Start-ThreadJob -ScriptBlock { Write-Host "HostOutput" } | Wait-Job 249 | $job.Information | Should -Be "HostOutput" 250 | } 251 | 252 | It 'ThreadJob ThrottleLimit and Queue' { 253 | 254 | try 255 | { 256 | # Start four thread jobs with ThrottleLimit set to two 257 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 258 | $job1 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 2 259 | $job2 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } 260 | $job3 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } 261 | $job4 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } 262 | 263 | # Allow jobs to start 264 | Wait-ForJobRunning $job2 265 | 266 | Get-Job | Where-Object { ($_.PSJobTypeName -eq "ThreadJob") -and ($_.State -eq "Running") } | Should -HaveCount 2 267 | Get-Job | Where-Object { ($_.PSJobTypeName -eq "ThreadJob") -and ($_.State -eq "NotStarted") } | Should -HaveCount 2 268 | } 269 | finally 270 | { 271 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 272 | } 273 | 274 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Should -HaveCount 0 275 | } 276 | 277 | It 'ThreadJob Runspaces should be cleaned up at completion' { 278 | 279 | $script = $WaitForCountFnScript + @' 280 | 281 | try 282 | { 283 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 284 | $rsStartCount = @(Get-Runspace).Count 285 | 286 | # Start four thread jobs with ThrottleLimit set to two 287 | $Job1 = Start-ThreadJob -ScriptBlock { "Hello 1!" } -ThrottleLimit 5 288 | $job2 = Start-ThreadJob -ScriptBlock { "Hello 2!" } 289 | $job3 = Start-ThreadJob -ScriptBlock { "Hello 3!" } 290 | $job4 = Start-ThreadJob -ScriptBlock { "Hello 4!" } 291 | 292 | $null = $job1,$job2,$job3,$job4 | Wait-Job 293 | 294 | # Allow for runspace clean up to happen 295 | Wait-ForExpectedRSCount $rsStartCount 296 | 297 | Write-Output (@(Get-Runspace).Count -eq $rsStartCount) 298 | } 299 | finally 300 | { 301 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 302 | } 303 | '@ 304 | 305 | $result = & "$PSHOME/pwsh" -c $script 306 | $result | Should -BeExactly "True" 307 | } 308 | 309 | It 'ThreadJob Runspaces should be cleaned up after job removal' { 310 | 311 | $script = $WaitForCountFnScript + @' 312 | 313 | try { 314 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 315 | $rsStartCount = @(Get-Runspace).Count 316 | 317 | # Start four thread jobs with ThrottleLimit set to two 318 | $Job1 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 2 319 | $job2 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } 320 | $job3 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } 321 | $job4 = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } 322 | 323 | Wait-ForExpectedRSCount ($rsStartCount + 4) 324 | Write-Output (@(Get-Runspace).Count -eq ($rsStartCount + 4)) 325 | 326 | # Stop two jobs 327 | $job1 | Remove-Job -Force 328 | $job3 | Remove-Job -Force 329 | 330 | Wait-ForExpectedRSCount ($rsStartCount + 2) 331 | Write-Output (@(Get-Runspace).Count -eq ($rsStartCount + 2)) 332 | } 333 | finally 334 | { 335 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 336 | } 337 | 338 | Wait-ForExpectedRSCount $rsStartCount 339 | Write-Output (@(Get-Runspace).Count -eq $rsStartCount) 340 | '@ 341 | 342 | $result = & "$PSHOME/pwsh" -c $script 343 | $result | Should -BeExactly "True","True","True" 344 | } 345 | 346 | It 'ThreadJob jobs should work with Receive-Job -AutoRemoveJob' { 347 | 348 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 349 | 350 | $job1 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } -ThrottleLimit 5 351 | $job2 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } 352 | $job3 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } 353 | $job4 = Start-ThreadJob -ScriptBlock { 1..2 | ForEach-Object { Start-Sleep -Milliseconds 100; "Output $_" } } 354 | 355 | $null = $job1,$job2,$job3,$job4 | Receive-Job -Wait -AutoRemoveJob 356 | 357 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Should -HaveCount 0 358 | } 359 | 360 | It 'ThreadJob jobs should run in FullLanguage mode by default' { 361 | 362 | $result = Start-ThreadJob -ScriptBlock { $ExecutionContext.SessionState.LanguageMode } | Wait-Job | Receive-Job 363 | $result | Should -Be "FullLanguage" 364 | } 365 | 366 | It 'ThreadJob jobs should run in the current working directory' { 367 | 368 | $threadJobCurrentLocation = Start-ThreadJob -ScriptBlock { $pwd } | Wait-Job | Receive-Job 369 | $threadJobCurrentLocation.Path | Should -BeExactly $pwd.Path 370 | } 371 | } 372 | 373 | Describe 'Job2 class API tests' -Tags 'CI' { 374 | 375 | AfterEach { 376 | Get-Job | Where-Object PSJobTypeName -eq "ThreadJob" | Remove-Job -Force 377 | } 378 | 379 | It 'Verifies StopJob API' { 380 | 381 | $job = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 5 382 | Wait-ForJobRunning $job 383 | $job.StopJob($true, "No Reason") 384 | $job.JobStateInfo.State | Should -Be "Stopped" 385 | } 386 | 387 | It 'Verifies StopJobAsync API' { 388 | 389 | $job = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 5 390 | Wait-ForJobRunning $job 391 | $job.StopJobAsync($true, "No Reason") 392 | Wait-Job $job 393 | $job.JobStateInfo.State | Should -Be "Stopped" 394 | } 395 | 396 | It 'Verifies StartJobAsync API' { 397 | 398 | $jobRunning = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } -ThrottleLimit 1 399 | $jobNotRunning = Start-ThreadJob -ScriptBlock { Start-Sleep -Seconds 60 } 400 | 401 | $jobNotRunning.JobStateInfo.State | Should -Be "NotStarted" 402 | 403 | # StartJobAsync starts jobs synchronously for ThreadJob jobs 404 | $jobNotRunning.StartJobAsync() 405 | Wait-ForJobRunning $jobNotRunning 406 | $jobNotRunning.JobStateInfo.State | Should -Be "Running" 407 | } 408 | 409 | It 'Verifies JobSourceAdapter Get-Jobs' { 410 | 411 | $job = Start-ThreadJob -ScriptBlock { "Hello" } | Wait-Job 412 | 413 | $getJob = Get-Job -InstanceId $job.InstanceId 2> $null 414 | $getJob | Should -Be $job 415 | 416 | $getJob = Get-Job -Name $job.Name 2> $null 417 | $getJob | Should -Be $job 418 | 419 | $getJob = Get-Job -Command ' "hello" ' 2> $null 420 | $getJob | Should -Be $job 421 | 422 | $getJob = Get-Job -State $job.JobStateInfo.State 2> $null 423 | $getJob | Should -Be $job 424 | 425 | $getJob = Get-Job -Id $job.Id 2> $null 426 | $getJob | Should -Be $job 427 | 428 | # Get-Job -Filter is not supported 429 | $result = Get-Job -Filter @{Id = ($job.Id)} 3> $null 430 | $result | Should -BeNullOrEmpty 431 | } 432 | 433 | It 'Verifies terminating job error' { 434 | 435 | $job = Start-ThreadJob -ScriptBlock { throw "My Job Error!" } | Wait-Job 436 | $results = $job | Receive-Job 2>&1 437 | $results.ToString() | Should -Be "My Job Error!" 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /tools/helper.psm1: -------------------------------------------------------------------------------- 1 | 2 | $MinimalSDKVersion = '2.1.300' 3 | $IsWindowsEnv = [System.Environment]::OSVersion.Platform -eq "Win32NT" 4 | $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path 5 | $LocalDotnetDirPath = if ($IsWindowsEnv) { "$env:LocalAppData\Microsoft\dotnet" } else { "$env:HOME/.dotnet" } 6 | 7 | <# 8 | .SYNOPSIS 9 | Get the path of the currently running powershell executable. 10 | #> 11 | function Get-PSExePath 12 | { 13 | if (-not $Script:PSExePath) { 14 | $Script:PSExePath = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName 15 | } 16 | return $Script:PSExePath 17 | } 18 | 19 | <# 20 | .SYNOPSIS 21 | Find the dotnet SDK that meets the minimal version requirement. 22 | #> 23 | function Find-Dotnet 24 | { 25 | $dotnetFile = if ($IsWindowsEnv) { "dotnet.exe" } else { "dotnet" } 26 | $dotnetExePath = Join-Path -Path $LocalDotnetDirPath -ChildPath $dotnetFile 27 | 28 | # If dotnet is already in the PATH, check to see if that version of dotnet can find the required SDK. 29 | # This is "typically" the globally installed dotnet. 30 | $foundDotnetWithRightVersion = $false 31 | $dotnetInPath = Get-Command 'dotnet' -ErrorAction Ignore 32 | if ($dotnetInPath) { 33 | $foundDotnetWithRightVersion = Test-DotnetSDK $dotnetInPath.Source 34 | } 35 | 36 | if (-not $foundDotnetWithRightVersion) { 37 | if (Test-DotnetSDK $dotnetExePath) { 38 | Write-Warning "Can't find the dotnet SDK version $MinimalSDKVersion or higher, prepending '$LocalDotnetDirPath' to PATH." 39 | $env:PATH = $LocalDotnetDirPath + [IO.Path]::PathSeparator + $env:PATH 40 | } 41 | else { 42 | throw "Cannot find the dotnet SDK for .NET Core 2.1. Please specify '-Bootstrap' to install build dependencies." 43 | } 44 | } 45 | } 46 | 47 | <# 48 | .SYNOPSIS 49 | Check if the dotnet SDK meets the minimal version requirement. 50 | #> 51 | function Test-DotnetSDK 52 | { 53 | param($dotnetExePath) 54 | 55 | if (Test-Path $dotnetExePath) { 56 | $installedVersion = & $dotnetExePath --version 57 | return $installedVersion -ge $MinimalSDKVersion 58 | } 59 | return $false 60 | } 61 | 62 | <# 63 | .SYNOPSIS 64 | Install the dotnet SDK if we cannot find an existing one. 65 | #> 66 | function Install-Dotnet 67 | { 68 | [CmdletBinding()] 69 | param( 70 | [string]$Channel = 'release', 71 | [string]$Version = '2.1.505' 72 | ) 73 | 74 | try { 75 | Find-Dotnet 76 | return # Simply return if we find dotnet SDk with the correct version 77 | } catch { } 78 | 79 | $logMsg = if (Get-Command 'dotnet' -ErrorAction Ignore) { 80 | "dotent SDK is not present. Installing dotnet SDK." 81 | } else { 82 | "dotnet SDK out of date. Require '$MinimalSDKVersion' but found '$dotnetSDKVersion'. Updating dotnet." 83 | } 84 | Write-Log $logMsg -Warning 85 | 86 | $obtainUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain" 87 | 88 | try { 89 | Remove-Item $LocalDotnetDirPath -Recurse -Force -ErrorAction Ignore 90 | $installScript = if ($IsWindowsEnv) { "dotnet-install.ps1" } else { "dotnet-install.sh" } 91 | Invoke-WebRequest -Uri $obtainUrl/$installScript -OutFile $installScript 92 | 93 | if ($IsWindowsEnv) { 94 | & .\$installScript -Channel $Channel -Version $Version 95 | } else { 96 | bash ./$installScript -c $Channel -v $Version 97 | } 98 | } 99 | finally { 100 | Remove-Item $installScript -Force -ErrorAction Ignore 101 | } 102 | } 103 | 104 | <# 105 | .SYNOPSIS 106 | Write log message for the build. 107 | #> 108 | function Write-Log 109 | { 110 | param( 111 | [string] $Message, 112 | [switch] $Warning, 113 | [switch] $Indent 114 | ) 115 | 116 | $foregroundColor = if ($Warning) { "Yellow" } else { "Green" } 117 | $indentPrefix = if ($Indent) { " " } else { "" } 118 | Write-Host -ForegroundColor $foregroundColor "${indentPrefix}${Message}" 119 | } 120 | 121 | 122 | $KeyboardLayoutHelperCode = @' 123 | using System; 124 | using System.Collections.Generic; 125 | using System.Globalization; 126 | using System.Runtime.InteropServices; 127 | using System.Threading; 128 | 129 | public class KeyboardLayoutHelper 130 | { 131 | [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 132 | static extern IntPtr LoadKeyboardLayout(string pwszKLID, uint Flags); 133 | 134 | [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 135 | static extern IntPtr GetKeyboardLayout(uint idThread); 136 | 137 | [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 138 | static extern int GetKeyboardLayoutList(int nBuff, [Out] IntPtr[] lpList); 139 | 140 | // Used when setting the layout. 141 | [DllImport("user32.dll", CharSet = CharSet.Auto)] 142 | public static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam, int lParam); 143 | 144 | // Used for getting the layout. 145 | [DllImport("user32.dll", SetLastError = true)] 146 | static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); 147 | 148 | // Used in both getting and setting the layout 149 | [DllImport("user32.dll", SetLastError = true)] 150 | static extern IntPtr GetForegroundWindow(); 151 | 152 | const int WM_INPUTLANGCHANGEREQUEST = 0x0050; 153 | 154 | private static string GetLayoutNameFromHKL(IntPtr hkl) 155 | { 156 | var lcid = (int)((uint)hkl & 0xffff); 157 | return (new CultureInfo(lcid)).Name; 158 | } 159 | 160 | public static IEnumerable GetKeyboardLayouts() 161 | { 162 | int cnt = GetKeyboardLayoutList(0, null); 163 | var list = new IntPtr[cnt]; 164 | GetKeyboardLayoutList(list.Length, list); 165 | 166 | foreach (var layout in list) 167 | { 168 | yield return GetLayoutNameFromHKL(layout); 169 | } 170 | } 171 | 172 | public static string GetCurrentKeyboardLayout() 173 | { 174 | uint processId; 175 | IntPtr layout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), out processId)); 176 | return GetLayoutNameFromHKL(layout); 177 | } 178 | 179 | public static IntPtr SetKeyboardLayout(string lang) 180 | { 181 | var layoutId = (new CultureInfo(lang)).KeyboardLayoutId; 182 | var layout = LoadKeyboardLayout(layoutId.ToString("x8"), 0x80); 183 | // Hacky, but tests are probably running in a console app and the layout change 184 | // is ignored, so post the layout change to the foreground window. 185 | PostMessage(GetForegroundWindow(), WM_INPUTLANGCHANGEREQUEST, 0, layoutId); 186 | // Wait a bit until the layout has been changed. 187 | do { 188 | Thread.Sleep(100); 189 | } while (GetCurrentKeyboardLayout() != lang); 190 | return layout; 191 | } 192 | } 193 | '@ 194 | 195 | <# 196 | .SYNOPSIS 197 | Start to run the xUnit tests. 198 | #> 199 | function Start-TestRun 200 | { 201 | param( 202 | [string] 203 | $Configuration, 204 | 205 | [string] 206 | $Framework 207 | ) 208 | 209 | $testResultFolder = 'TestResults' 210 | 211 | function RunXunitTestsInNewProcess ([string] $Layout, [string] $OperatingSystem) 212 | { 213 | $filter = "FullyQualifiedName~Test.{0}_{1}" -f ($Layout -replace '-','_'), $OperatingSystem 214 | $testResultFile = "xUnitTestResults.{0}.xml" -f $Layout 215 | $testResultFile = Join-Path $testResultFolder $testResultFile 216 | 217 | $stdOutput, $stdError = @(New-TemporaryFile; New-TemporaryFile) 218 | $arguments = 'test', '--no-build', '-c', $Configuration, '-f', $Framework, '--filter', $filter, '--logger', "xunit;LogFilePath=$testResultFile" 219 | 220 | Start-Process -FilePath dotnet -Wait -RedirectStandardOutput $stdOutput -RedirectStandardError $stdError -ArgumentList $arguments 221 | Get-Content $stdOutput, $stdError 222 | Remove-Item $stdOutput, $stdError 223 | } 224 | 225 | try 226 | { 227 | $env:PSREADLINE_TESTRUN = 1 228 | Push-Location "$RepoRoot/test" 229 | 230 | if ($IsWindowsEnv) 231 | { 232 | if ($env:APPVEYOR -or $env:TF_BUILD) 233 | { 234 | # AppVeyor CI builder only has en-US keyboard layout installed. 235 | # We have to run tests from a new process because `GetCurrentKeyboardLayout` simply fails when called from 236 | # the `pwsh` process started by AppVeyor. Our xUnit tests depends on `GetCurrentKeyboardLayout` to tell if 237 | # a test case should run. 238 | RunXunitTestsInNewProcess -Layout 'en-US' -OperatingSystem 'Windows' 239 | } 240 | else 241 | { 242 | if (-not ("KeyboardLayoutHelper" -as [type])) 243 | { 244 | Add-Type $KeyboardLayoutHelperCode 245 | } 246 | 247 | try 248 | { 249 | # Remember the current keyboard layout, changes are system wide and restoring 250 | # is the nice thing to do. 251 | $savedLayout = [KeyboardLayoutHelper]::GetCurrentKeyboardLayout() 252 | 253 | # We want to run tests in as many layouts as possible. We have key info 254 | # data for layouts that might not be installed, and tests would fail 255 | # if we don't set the system wide layout to match the key data we'll use. 256 | $layouts = [KeyboardLayoutHelper]::GetKeyboardLayouts() 257 | Write-Log "Available layouts: $layouts" 258 | 259 | foreach ($layout in $layouts) 260 | { 261 | if (Test-Path "KeyInfo-${layout}-windows.json") 262 | { 263 | Write-Log "Testing $layout ..." 264 | $null = [KeyboardLayoutHelper]::SetKeyboardLayout($layout) 265 | 266 | # We have to use Start-Process so it creates a new window, because the keyboard 267 | # layout change won't be picked up by any processes running in the current conhost. 268 | RunXunitTestsInNewProcess -Layout $layout -OperatingSystem 'Windows' 269 | } 270 | } 271 | } 272 | finally 273 | { 274 | # Restore the original keyboard layout 275 | $null = [KeyboardLayoutHelper]::SetKeyboardLayout($savedLayout) 276 | } 277 | } 278 | } 279 | else 280 | { 281 | RunXunitTestsInNewProcess -Layout 'en-US' -OperatingSystem 'Linux' 282 | } 283 | 284 | # Check to see if there were any failures in xUnit tests, and throw exception to fail the build if so. 285 | Get-ChildItem $testResultFolder | Test-XUnitTestResults > $null 286 | } 287 | finally 288 | { 289 | Pop-Location 290 | Remove-Item env:PSREADLINE_TESTRUN 291 | } 292 | } 293 | 294 | <# 295 | .SYNOPSIS 296 | Check to see if the xUnit test run was successful. 297 | #> 298 | function Test-XUnitTestResults 299 | { 300 | param( 301 | [Parameter(Mandatory, ValueFromPipeline)] 302 | [string] $TestResultsFile 303 | ) 304 | 305 | Process 306 | { 307 | if (-not (Test-Path $TestResultsFile)) 308 | { 309 | throw "File not found $TestResultsFile" 310 | } 311 | 312 | try 313 | { 314 | $results = [xml] (Get-Content $TestResultsFile) 315 | } 316 | catch 317 | { 318 | throw "Cannot convert $TestResultsFile to xml : $($_.message)" 319 | } 320 | 321 | $failedTests = $results.assemblies.assembly.collection | Where-Object failed -gt 0 322 | 323 | if (-not $failedTests) 324 | { 325 | return $true 326 | } 327 | 328 | throw "$($failedTests.failed) tests failed" 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /tools/releaseBuild/releaseBuild.yml: -------------------------------------------------------------------------------- 1 | name: PSThreadJob-ModuleBuild-$(Build.BuildId) 2 | trigger: 3 | branches: 4 | include: 5 | - master 6 | - release* 7 | 8 | variables: 9 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 10 | POWERSHELL_TELEMETRY_OPTOUT: 1 11 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 12 | BUILD_CONFIGURATION: "Release" 13 | BUILD_FRAMEWORK: "Net461" 14 | 15 | # Set AzDevOps Agent to clean the machine after the end of the build 16 | resources: 17 | - repo: self 18 | clean: true 19 | 20 | jobs: 21 | - job: build_windows 22 | pool: Package ES CodeHub Lab E 23 | # vmimage: windows-2019 24 | 25 | steps: 26 | 27 | - checkout: self 28 | clean: true 29 | persistCredentials: true 30 | 31 | - task: PkgESSetupBuild@10 32 | displayName: 'Initialize build' 33 | inputs: 34 | # Do not create a release share. 35 | # Enabling this will cause failures! 36 | useDfs: false 37 | productName: PSThreadJob 38 | # Add branch name to build name (only for non-master) 39 | branchVersion: true 40 | disableWorkspace: true 41 | disableBuildTools: true 42 | disableNugetPack: true 43 | 44 | - powershell: | 45 | $(Build.SourcesDirectory)\build.ps1 -Bootstrap 46 | $(Build.SourcesDirectory)\build.ps1 -Configuration $(BUILD_CONFIGURATION) -Framework $(BUILD_FRAMEWORK) 47 | # Set target folder paths 48 | $vstsCommandString = "vso[task.setvariable variable=PSThreadJob]$(Build.SourcesDirectory)\PSThreadJob\bin\$(BUILD_CONFIGURATION)\PSThreadJob" 49 | Write-Host "sending " + $vstsCommandString 50 | Write-Host "##$vstsCommandString" 51 | $vstsCommandString = "vso[task.setvariable variable=Signed]$(Build.SourcesDirectory)\PSThreadJob\bin\$(BUILD_CONFIGURATION)\Signed" 52 | Write-Host "sending " + $vstsCommandString 53 | Write-Host "##$vstsCommandString" 54 | $modVersion = (Import-PowerShellDataFile -Path $(Build.SourcesDirectory)\PSThreadJob\ThreadJob.psd1).ModuleVersion 55 | $vstsCommandString = "vso[task.setvariable variable=ModVersion]$modVersion" 56 | Write-Host "sending " + $vstsCommandString 57 | Write-Host "##$vstsCommandString" 58 | displayName: Bootstrap & Build 59 | 60 | # Sign the module files 61 | - task: PkgESCodeSign@10 62 | displayName: 'CodeSign - module artifacts' 63 | env: 64 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 65 | inputs: 66 | signConfigXml: '$(Build.SourcesDirectory)\tools\releaseBuild\sign-module-files.xml' 67 | inPathRoot: '$(PSThreadJob)' 68 | outPathRoot: '$(Signed)' 69 | binVersion: Production 70 | binVersionOverride: '' 71 | condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) 72 | 73 | # Replace the *.psm1, *.ps1, *.psd1, *.dll files with the signed ones 74 | - powershell: | 75 | # Show the signed files 76 | Get-ChildItem -Path $(Signed) 77 | Copy-Item -Path $(Signed)\* -Destination $(PSThreadJob) -Force 78 | displayName: 'Replace unsigned files with signed ones' 79 | 80 | # Create catalog file from the signed modules files 81 | - powershell: | 82 | New-FileCatalog -CatalogFilePath $(PSThreadJob)\PSThreadJob.cat -Path $(PSThreadJob) -CatalogVersion 2.0 | ` 83 | ForEach-Object -MemberName FullName 84 | displayName: 'Create catalog file' 85 | 86 | # Sign the catalog file 87 | - task: PkgESCodeSign@10 88 | displayName: 'CodeSign - catalog file' 89 | env: 90 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 91 | inputs: 92 | signConfigXml: '$(Build.SourcesDirectory)\tools\releaseBuild\sign-catalog.xml' 93 | inPathRoot: '$(PSThreadJob)' 94 | outPathRoot: '$(Signed)' 95 | binVersion: Production 96 | binVersionOverride: '' 97 | condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) 98 | 99 | # Copy the signed catalog file over 100 | - powershell: | 101 | # Show the signed files 102 | Get-ChildItem -Path $(Signed) 103 | Copy-Item -Path $(Signed)\PSThreadJob.cat -Destination $(PSThreadJob) -Force 104 | displayName: 'Replace catalog file with the signed one' 105 | condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) 106 | 107 | # Verify the signatures 108 | - powershell: | 109 | $HasInvalidFiles = $false 110 | $WrongCert = @{} 111 | Get-ChildItem -Path $(PSThreadJob) -Recurse -Include "*.dll","*.ps*1*","*.cat" | ` 112 | Get-AuthenticodeSignature | ForEach-Object { 113 | $_ | Select-Object Path, Status 114 | if ($_.Status -ne 'Valid') { $HasInvalidFiles = $true } 115 | if ($_.SignerCertificate.Subject -notmatch 'CN=Microsoft Corporation.*') { 116 | $WrongCert.Add($_.Path, $_.SignerCertificate.Subject) 117 | } 118 | } 119 | 120 | if ($HasInvalidFiles) { throw "Authenticode verification failed. There is one or more invalid files." } 121 | if ($WrongCert.Count -gt 0) { 122 | $WrongCert 123 | throw "Certificate should have the subject starts with 'Microsoft Corporation'" 124 | } 125 | displayName: 'Verify the signed files' 126 | condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) 127 | 128 | - powershell: | 129 | $CatInfo = Test-FileCatalog -Path $(PSThreadJob) -CatalogFilePath $(PSThreadJob)\PSThreadJob.cat -Detailed 130 | $CatInfo | Format-List 131 | if ($CatInfo.Status -ne "Valid") { throw "Catalog file is invalid." } 132 | displayName: 'Verify the catalog file' 133 | condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) 134 | 135 | - powershell: | 136 | Get-ChildItem -Path $(PSThreadJob) 137 | Write-Host "##vso[artifact.upload containerfolder=PSThreadJob;artifactname=PSThreadJob]$(PSThreadJob)" 138 | displayName: 'Upload module artifacts' 139 | 140 | - template: templates/compliance.yml 141 | parameters: 142 | configuration: $(BUILD_CONFIGURATION) 143 | framework: $(BUILD_FRAMEWORK) 144 | -------------------------------------------------------------------------------- /tools/releaseBuild/sign-catalog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tools/releaseBuild/sign-module-files.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tools/releaseBuild/templates/compliance.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | configuration: "" 3 | framework: "" 4 | 5 | steps: 6 | 7 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-antimalware.AntiMalware@3 8 | displayName: 'Run Defender Scan' 9 | 10 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 11 | displayName: 'Run CredScan' 12 | inputs: 13 | debugMode: false 14 | continueOnError: true 15 | 16 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-binskim.BinSkim@3 17 | displayName: 'Run BinSkim ' 18 | inputs: 19 | InputType: Basic 20 | AnalyzeTarget: '$(Build.SourcesDirectory)\PSThreadJob\bin\${{ parameters.configuration }}\${{ parameters.framework }}\*.dll' 21 | AnalyzeSymPath: 'SRV*' 22 | AnalyzeVerbose: true 23 | AnalyzeHashes: true 24 | AnalyzeStatistics: true 25 | continueOnError: true 26 | 27 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@1 28 | displayName: 'Run PoliCheck' 29 | inputs: 30 | targetType: F 31 | optionsFC: 0 32 | optionsXS: 0 33 | optionsPE: '1|2|3|4' 34 | optionsHMENABLE: 0 35 | # optionsRulesDBPath: '$(Build.SourcesDirectory)\tools\terms\PowerShell-Terms-Rules.mdb' 36 | # optionsFTPATH: '$(Build.SourcesDirectory)\tools\terms\FileTypeSet.xml' 37 | toolVersion: 5.8.2.1 38 | continueOnError: true 39 | 40 | # - task: securedevelopmentteam.vss-secure-development-tools.build-task-apiscan.APIScan@1 41 | # displayName: 'Run APIScan' 42 | # inputs: 43 | # softwareFolder: '$(Build.SourcesDirectory)' 44 | # softwareName: PowerShell 45 | # softwareVersionNum: '$(ModVersion)' 46 | # isLargeApp: false 47 | # preserveTempFiles: true 48 | # continueOnError: true 49 | 50 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@2 51 | displayName: 'Publish Security Analysis Logs to Build Artifacts' 52 | continueOnError: true 53 | 54 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-uploadtotsa.TSAUpload@1 55 | displayName: 'TSA upload to Codebase: PSThreadJob_201912 Stamp: Azure' 56 | inputs: 57 | tsaStamp: $(TsaStamp) 58 | codeBaseName: $(CodeBaseName) 59 | tsaVersion: TsaV2 60 | uploadFortifySCA: false 61 | uploadFxCop: false 62 | uploadModernCop: false 63 | uploadPREfast: false 64 | uploadRoslyn: false 65 | uploadTSLint: false 66 | uploadAPIScan: false 67 | 68 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-report.SdtReport@1 69 | displayName: 'Create Security Analysis Report' 70 | inputs: 71 | TsvFile: false 72 | APIScan: true 73 | BinSkim: true 74 | CredScan: true 75 | PoliCheck: true 76 | PoliCheckBreakOn: Severity2Above 77 | 78 | - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 79 | displayName: 'Component Detection' 80 | inputs: 81 | sourceScanPath: '$(Build.SourcesDirectory)' 82 | snapshotForceEnabled: true 83 | --------------------------------------------------------------------------------