From 95132e3ca79b10b4bc514e6f6480d3646e22fed5 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 01:29:32 +0100 Subject: [PATCH 01/38] Adding PSData to module manifest --- module/PSParallel.psd1 | Bin 2100 -> 3140 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 626174c15396db2d391fdfbe865036ca060c6800..fa395ecc00358f09ea35cf97e12373cc4c3a49bf 100644 GIT binary patch delta 970 zcmbW0O-lk%6o%h6B0d(LBhUvf%uhLIx~!;2_gOn|455|LhF80 z&%M)xrUN0DJNMl4@t*gbbLV68bN4-y-aAxAj&gcbQ?8EmpgZKMYVtHtsLFILy>onp zwJRULP$RaZf5lA}S#!=5cCUsR$+6>KGU1^$sn6mhSj1+Y>iKY9Lx5c zaJ>h(r9M&wd;s|}x`5qfnZistj&3Q~#nZ=9M;~d6=!P`olX8oR zRAd&vdpYdAz#V zYWVb^)wdlDxRq4YxeB_be+8(<#AbP!Ie2;otxntuyta~vO}|p9%4{kl7cF^rl|)H- zoa6>{Y(rWU*Z*l&#g4&a3_0*zTJ0>%?=R>0$;SFI5&P@{@eNK-PkiSeY1iu|;5m0*8nu11|#?7}vsC08Ls0j{pDw From e338167e7e0d4bb4d390533cc2673b2c15c1013a Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 11:35:48 +0100 Subject: [PATCH 02/38] Adding tests for function capture and fixing bugs when no function provider is defined in InitialSessionState --- src/PSParallel/InvokeParallelCommand.cs | 14 +++++-- src/PSParallelTests/InvokeParallelTests.cs | 47 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index f495fcf..d196d2e 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -65,9 +65,17 @@ private static InitialSessionState GetSessionState(ScriptBlock scriptBlock, Sess private static IDictionary GetFunctions(SessionState sessionState) { - var baseObject = (Dictionary.ValueCollection) sessionState.InvokeProvider.Item.Get("function:")[0].BaseObject; - return baseObject.ToDictionary(f=>f.Name); - } + try + { + var functionDrive = sessionState.InvokeProvider.Item.Get("function:"); + var baseObject = (Dictionary.ValueCollection) functionDrive[0].BaseObject; + return baseObject.ToDictionary(f => f.Name); + } + catch (DriveNotFoundException) + { + return new Dictionary(); + } + } private static void CaptureFunctions(ScriptBlock scriptBlock, InitialSessionState initialSessionState, IDictionary functions, ISet processedFunctions) diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index 5f1e941..dd4d754 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -28,6 +28,7 @@ public InvokeParallelTests() new SessionStateCmdletEntry("Write-Information", typeof(WriteInformationCommand), null), new SessionStateCmdletEntry("Invoke-Parallel", typeof(InvokeParallelCommand), null), }); + iss.Providers.Add(new SessionStateProviderEntry("function", typeof(FunctionProvider), null)); m_runspacePool = RunspaceFactory.CreateRunspacePool(iss); m_runspacePool.SetMaxRunspaces(10); m_runspacePool.Open(); @@ -283,6 +284,52 @@ public void TestNoProgressOutput() } + [TestMethod] + public void TestFunctionCaptureOutput() + { + PowerShell ps = PowerShell.Create(); + ps.RunspacePool = m_runspacePool; + ps.AddScript(@" +function foo($x) {return $x * 2} +", false); + + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", ScriptBlock.Create("foo $_")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("NoProgress"); + + var input = new PSDataCollection { 1, 2, 3, 4, 5 }; + input.Complete(); + var output = ps.Invoke(input); + var sum = output.Aggregate(0, (a, b) => a + b); + Assert.AreEqual(30, sum); + } + + + + [TestMethod] + public void TestRecursiveFunctionCaptureOutput() + { + PowerShell ps = PowerShell.Create(); + ps.RunspacePool = m_runspacePool; + ps.AddScript(@" +function foo($x) {return 2 * $x} +function bar($x) {return 3 * (foo $x)} +", false); + + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", ScriptBlock.Create("bar $_")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("NoProgress"); + + var input = new PSDataCollection { 1, 2, 3, 4, 5 }; + input.Complete(); + var output = ps.Invoke(input); + var sum = output.Aggregate(0, (a, b) => a + b); + Assert.AreEqual(90, sum); + } public void Dispose() { From ce6fe6c90e7509ea9e7325d89f9d9166bec9dcc8 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 11:46:28 +0100 Subject: [PATCH 03/38] Cleanup --- src/PSParallel/InvokeParallelCommand.cs | 13 ++++++------- src/PSParallelTests/InvokeParallelTests.cs | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index d196d2e..3355e26 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -1,14 +1,14 @@ using System; -using System.CodeDom; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Threading; -using Microsoft.PowerShell.Commands; +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global +// ReSharper disable MemberCanBePrivate.Global namespace PSParallel { [Alias("ipa")] @@ -58,7 +58,6 @@ private static InitialSessionState GetSessionState(ScriptBlock scriptBlock, Sess // this will get invoked recursively var functions = GetFunctions(sessionState); - CaptureFunctions(scriptBlock, initialSessionState, functions, new HashSet()); return initialSessionState; } @@ -66,7 +65,7 @@ private static InitialSessionState GetSessionState(ScriptBlock scriptBlock, Sess private static IDictionary GetFunctions(SessionState sessionState) { try - { + { var functionDrive = sessionState.InvokeProvider.Item.Get("function:"); var baseObject = (Dictionary.ValueCollection) functionDrive[0].BaseObject; return baseObject.ToDictionary(f => f.Name); @@ -75,12 +74,12 @@ private static IDictionary GetFunctions(SessionState sessi { return new Dictionary(); } - } + } private static void CaptureFunctions(ScriptBlock scriptBlock, InitialSessionState initialSessionState, IDictionary functions, ISet processedFunctions) { - var commands = scriptBlock.Ast.FindAll((ast) => ast is CommandAst, true); + var commands = scriptBlock.Ast.FindAll(ast => ast is CommandAst, true); var nonProcessedCommandNames = commands.Cast() .Select(commandAst => commandAst.CommandElements[0].Extent.Text) diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index dd4d754..4be1f0c 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -17,7 +17,7 @@ public sealed class InvokeParallelTests : IDisposable public InvokeParallelTests() { var iss = InitialSessionState.Create(); - iss.LanguageMode = PSLanguageMode.FullLanguage; + iss.LanguageMode = PSLanguageMode.FullLanguage; iss.Commands.Add(new [] { new SessionStateCmdletEntry("Write-Error", typeof(WriteErrorCommand), null), @@ -27,7 +27,7 @@ public InvokeParallelTests() new SessionStateCmdletEntry("Write-Warning", typeof(WriteWarningCommand), null), new SessionStateCmdletEntry("Write-Information", typeof(WriteInformationCommand), null), new SessionStateCmdletEntry("Invoke-Parallel", typeof(InvokeParallelCommand), null), - }); + }); iss.Providers.Add(new SessionStateProviderEntry("function", typeof(FunctionProvider), null)); m_runspacePool = RunspaceFactory.CreateRunspacePool(iss); m_runspacePool.SetMaxRunspaces(10); @@ -288,7 +288,7 @@ public void TestNoProgressOutput() public void TestFunctionCaptureOutput() { PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = m_runspacePool; ps.AddScript(@" function foo($x) {return $x * 2} ", false); @@ -312,7 +312,7 @@ function foo($x) {return $x * 2} public void TestRecursiveFunctionCaptureOutput() { PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = m_runspacePool; ps.AddScript(@" function foo($x) {return 2 * $x} function bar($x) {return 3 * (foo $x)} From a824fa3b888850e20362bfc63b185fa536903778 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 11:49:18 +0100 Subject: [PATCH 04/38] Publishing version 1.5 with function capturing --- module/PSParallel.psd1 | Bin 2100 -> 2100 bytes scripts/Publish-ToGallery.ps1 | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 626174c15396db2d391fdfbe865036ca060c6800..ef9b6be3732316d50e5cbfb55bd1a687564d329c 100644 GIT binary patch delta 14 VcmdlYuti{l7bBzTW^cxqYyc%A1gHQ2 delta 14 VcmdlYuti{l7bBy|W^cxqYyc%41g8K1 diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index 33a1b26..196063a 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Adding authenticode signature." + ReleaseNote = "Capturing functions" ProjectUri = "https://github.com/powercode/PSParallel" } From 193572dbe53adda851d02ade74aec1a46ef2268f Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 12:43:17 +0100 Subject: [PATCH 05/38] Fixing example1 for Invoke-Parallel in help --- module/en-US/PSParallel.dll-Help.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/en-US/PSParallel.dll-Help.xml b/module/en-US/PSParallel.dll-Help.xml index 2c825fa..7ee740b 100644 --- a/module/en-US/PSParallel.dll-Help.xml +++ b/module/en-US/PSParallel.dll-Help.xml @@ -247,9 +247,10 @@ PS C:\> - 1..256 | Invoke-Parallel {$ip = 192.168.0.$_; $res = ping.exe -v4 -w20 $ip; [PSCustomObject] @{IP=$ip;Res=$res}} + (1..255).ForEach{"192.168.0.$_"} | Invoke-Parallel {$ip = $_; $res = ping.exe -4 -w 20 $_; [PSCustomObject] @{IP=$ip;Res=$res}} -ThrottleLimit 64 + This example pings all iP v4 addresses on a subnet, specifying Throttlelimit to 64, i.e. running up to 64 runspaces in parallel. From 289378746b060e953257c1bb675afa1db99c1078 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 13:26:51 +0100 Subject: [PATCH 06/38] Simplifying function and variable capture to not try to be clever. --- src/PSParallel/InvokeParallelCommand.cs | 93 +++++++--------------- src/PSParallelTests/InvokeParallelTests.cs | 7 +- 2 files changed, 36 insertions(+), 64 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 3355e26..54a75c9 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation; -using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Threading; @@ -50,100 +49,68 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable // Input is then captured in ProcessRecored and processed in EndProcessing private List m_input; - private static InitialSessionState GetSessionState(ScriptBlock scriptBlock, SessionState sessionState) + private static InitialSessionState GetSessionState(SessionState sessionState) { var initialSessionState = InitialSessionState.CreateDefault2(); - - CaptureVariables(scriptBlock, sessionState, initialSessionState); - // this will get invoked recursively - - var functions = GetFunctions(sessionState); - CaptureFunctions(scriptBlock, initialSessionState, functions, new HashSet()); + CaptureVariables(sessionState, initialSessionState); + CaptureFunctions(sessionState, initialSessionState); return initialSessionState; } - private static IDictionary GetFunctions(SessionState sessionState) + private static IEnumerable GetFunctions(SessionState sessionState) { try { var functionDrive = sessionState.InvokeProvider.Item.Get("function:"); var baseObject = (Dictionary.ValueCollection) functionDrive[0].BaseObject; - return baseObject.ToDictionary(f => f.Name); + return baseObject; } catch (DriveNotFoundException) { - return new Dictionary(); + return new FunctionInfo[] {}; } } - private static void CaptureFunctions(ScriptBlock scriptBlock, InitialSessionState initialSessionState, - IDictionary functions, ISet processedFunctions) + private static IEnumerable GetVariables(SessionState sessionState) { - var commands = scriptBlock.Ast.FindAll(ast => ast is CommandAst, true); - - var nonProcessedCommandNames = commands.Cast() - .Select(commandAst => commandAst.CommandElements[0].Extent.Text) - .Where(commandName => !processedFunctions.Contains(commandName)); - foreach (var commandName in nonProcessedCommandNames) + try { - - FunctionInfo functionInfo; - if (!functions.TryGetValue(commandName, out functionInfo)) - { - continue; - } - initialSessionState.Commands.Add(new SessionStateFunctionEntry(functionInfo.Name, functionInfo.Definition)); - processedFunctions.Add(commandName); - CaptureFunctions(functionInfo.ScriptBlock, initialSessionState, functions, processedFunctions); + string[] noTouchVariables = new[] {"null", "true", "false", "Error"}; + var variables = sessionState.InvokeProvider.Item.Get("Variable:"); + var psVariables = (IEnumerable) variables[0].BaseObject; + return psVariables.Where(p=>!noTouchVariables.Contains(p.Name)); + } + catch (DriveNotFoundException) + { + return new PSVariable[]{}; } } - - private static void CaptureVariables(ScriptBlock scriptBlock, SessionState sessionState, - InitialSessionState initialSessionState) + private static void CaptureFunctions(SessionState sessionState, InitialSessionState initialSessionState) { - var variables = scriptBlock.Ast.FindAll(ast => ast is VariableExpressionAst, true); - var varDict = new Dictionary(); - foreach (var ast in variables) - { - var v = (VariableExpressionAst) ast; - var variableName = v.VariablePath.UserPath; - if (variableName == "_" || varDict.ContainsKey(variableName)) - { - continue; - } - - var variable = sessionState.PSVariable.Get(variableName); - if (variable != null) - { - var ssve = new SessionStateVariableEntry(variable.Name, variable.Value, - variable.Description, variable.Options, variable.Attributes); - varDict.Add(variableName, ssve); - } + var functions = GetFunctions(sessionState); + foreach (var func in functions) { + initialSessionState.Commands.Add(new SessionStateFunctionEntry(func.Name, func.Definition)); } + } - var prefs = new[] - { - "ErrorActionPreference", "DebugPreference", "VerbosePreference", "WarningPreference", - "ProgressPreference", "InformationPreference", "ConfirmPreference", "WhatIfPreference" - }; - foreach (var pref in prefs) + private static void CaptureVariables(SessionState sessionState, InitialSessionState initialSessionState) + { + var variables = GetVariables(sessionState); + foreach(var variable in variables) { - var v = sessionState.PSVariable.Get(pref); - if (v != null) + var existing = initialSessionState.Variables[variable.Name].FirstOrDefault(); + if (existing != null && (existing.Options & (ScopedItemOptions.Constant | ScopedItemOptions.ReadOnly)) != ScopedItemOptions.None) { - var ssve = new SessionStateVariableEntry(v.Name, v.Value, - v.Description, v.Options, v.Attributes); - varDict.Add(v.Name, ssve); + continue; } + initialSessionState.Variables.Add(new SessionStateVariableEntry(variable.Name, variable.Value, variable.Description, variable.Options, variable.Attributes)); } - - initialSessionState.Variables.Add(varDict.Values); } protected override void BeginProcessing() { - m_initialSessionState = GetSessionState(ScriptBlock, SessionState); + m_initialSessionState = GetSessionState(SessionState); m_powershellPool = new PowershellPool(ThrottleLimit,m_initialSessionState, m_cancelationTokenSource.Token); m_powershellPool.Open(); if (!NoProgress) diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index 4be1f0c..e9194f7 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -28,7 +28,12 @@ public InvokeParallelTests() new SessionStateCmdletEntry("Write-Information", typeof(WriteInformationCommand), null), new SessionStateCmdletEntry("Invoke-Parallel", typeof(InvokeParallelCommand), null), }); - iss.Providers.Add(new SessionStateProviderEntry("function", typeof(FunctionProvider), null)); + iss.Providers.Add(new SessionStateProviderEntry("Function", typeof(FunctionProvider), null)); + iss.Providers.Add(new SessionStateProviderEntry("Variable", typeof(VariableProvider), null)); + iss.Variables.Add(new [] + { + new SessionStateVariableEntry("ErrorActionPreference", ActionPreference.Continue, "Dictates the action taken when an error message is delivered"), + }); m_runspacePool = RunspaceFactory.CreateRunspacePool(iss); m_runspacePool.SetMaxRunspaces(10); m_runspacePool.Open(); From f0e2c5fb4caa32b340de9b5492fa3eb16716b36e Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 13:29:00 +0100 Subject: [PATCH 07/38] Adding InitialSessionState parameter --- src/PSParallel/InvokeParallelCommand.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 54a75c9..6f8ec8e 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -30,6 +30,10 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [ValidateNotNullOrEmpty] public string ProgressActivity { get; set; } = "Invoke-Parallel"; + [Parameter] + [ValidateNotNull] + public InitialSessionState InitialSessionState { get; set; } + [Parameter] [ValidateRange(1,128)] public int ThrottleLimit { get; set; } = 32; @@ -110,7 +114,7 @@ private static void CaptureVariables(SessionState sessionState, InitialSessionSt protected override void BeginProcessing() { - m_initialSessionState = GetSessionState(SessionState); + m_initialSessionState = InitialSessionState ?? GetSessionState(SessionState); m_powershellPool = new PowershellPool(ThrottleLimit,m_initialSessionState, m_cancelationTokenSource.Token); m_powershellPool.Open(); if (!NoProgress) From 36226df403213efd6d97ec63c0cb586b86cdaaed Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 13:30:37 +0100 Subject: [PATCH 08/38] Publishing v1.6 --- module/PSParallel.psd1 | Bin 2100 -> 2100 bytes scripts/Publish-ToGallery.ps1 | 2 +- src/PSParallel/InvokeParallelCommand.cs | 8 ++++---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index ef9b6be3732316d50e5cbfb55bd1a687564d329c..04d69a738bb1f997b3e9916243a39615a926f0ae 100644 GIT binary patch delta 14 VcmdlYuti{l7bBzDW^cxqYyc%G1gQW3 delta 14 VcmdlYuti{l7bBzTW^cxqYyc%A1gHQ2 diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index 196063a..e9bb063 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Capturing functions" + ReleaseNote = "Adding InitialSessionState parameter. More robust function capture" ProjectUri = "https://github.com/powercode/PSParallel" } diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 6f8ec8e..f9af527 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -29,15 +29,15 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [Parameter(ParameterSetName = "Progress")] [ValidateNotNullOrEmpty] public string ProgressActivity { get; set; } = "Invoke-Parallel"; + + [Parameter] + [ValidateRange(1,128)] + public int ThrottleLimit { get; set; } = 32; [Parameter] [ValidateNotNull] public InitialSessionState InitialSessionState { get; set; } - [Parameter] - [ValidateRange(1,128)] - public int ThrottleLimit { get; set; } = 32; - [Parameter(ValueFromPipeline = true, Mandatory = true)] public PSObject InputObject { get; set; } From 36f15b7754f7da444150bc29bbe8cf1866aa6e83 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 13:40:12 +0100 Subject: [PATCH 09/38] Updating help for InitialSessionState --- PSParallel.sln | 1 + module/en-US/PSParallel.dll-Help.xml | 34 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/PSParallel.sln b/PSParallel.sln index 334a019..6c6636f 100644 --- a/PSParallel.sln +++ b/PSParallel.sln @@ -21,6 +21,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "module", "module", "{9C879DF2-D1E2-4143-A95A-2374F8650F48}" ProjectSection(SolutionItems) = preProject module\en-US\about_PSParallel.Help.txt = module\en-US\about_PSParallel.Help.txt + module\en-US\PSParallel.dll-Help.xml = module\en-US\PSParallel.dll-Help.xml module\PSParallel.psd1 = module\PSParallel.psd1 EndProjectSection EndProject diff --git a/module/en-US/PSParallel.dll-Help.xml b/module/en-US/PSParallel.dll-Help.xml index 7ee740b..c0ca823 100644 --- a/module/en-US/PSParallel.dll-Help.xml +++ b/module/en-US/PSParallel.dll-Help.xml @@ -73,6 +73,18 @@ Int32 + + InitialSessionState + + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. + +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. + + + InitialSessionState + + + InputObject @@ -95,6 +107,17 @@ + + InitialSessionState + + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. + +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. + + InitialSessionState + + + ThrottleLimit @@ -189,6 +212,17 @@ + + InitialSessionState + + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. + + + InitialSessionState + + + InputObject From e4b922d103278a516aabdaf77a24990f1407ba42 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 23 Nov 2015 16:57:48 +0100 Subject: [PATCH 10/38] Starting work on better cancellation --- src/PSParallel/InvokeParallelCommand.cs | 3 +- src/PSParallel/PowerShellPoolMember.cs | 41 ++++++++++++++++++++----- src/PSParallel/PowershellPool.cs | 27 +++++++++++++--- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index f9af527..2ccbd49 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -139,7 +139,8 @@ protected override void ProcessRecord() protected override void EndProcessing() { - try { + try + { if (!NoProgress) { m_progressManager.TotalCount = m_input.Count; diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index ce7bf95..0bc2eab 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -29,10 +29,16 @@ private void PowerShellOnInvocationStateChanged(object sender, PSInvocationState case PSInvocationState.Stopped: case PSInvocationState.Completed: case PSInvocationState.Failed: - ReturnPowerShell(m_powerShell); - CreatePowerShell(); + var ps = m_powerShell; + if(ps != null) + { + lock(ps) { + ReturnPowerShell(ps); + m_powerShell = null; + } + CreatePowerShell(); + } m_pool.ReportCompletion(this); - break; } } @@ -52,7 +58,7 @@ private void ReturnPowerShell(PowerShell powershell) UnhookStreamEvents(powershell.Streams); powershell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; m_output.DataAdded -= OutputOnDataAdded; - powershell.Dispose(); + powershell.Dispose(); ; } @@ -89,13 +95,14 @@ public void BeginInvoke(ScriptBlock scriptblock, PSObject inputObject) public void Dispose() { - if (m_powerShell != null) + var ps = m_powerShell; + if (ps != null) { - UnhookStreamEvents(m_powerShell.Streams); + UnhookStreamEvents(ps.Streams); + ps.Dispose(); } m_output.Dispose(); m_input.Dispose(); - m_powerShell?.Dispose(); } private void OutputOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) @@ -140,5 +147,25 @@ private void VerboseOnDataAdded(object sender, DataAddedEventArgs dataAddedEvent var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; m_poolStreams.Verbose.Add(record); } + + public void Stop() + { + m_powerShell?.BeginStop(OnStopped, this); + } + + private void OnStopped(IAsyncResult ar) + { + var poolMember = (PowerShellPoolMember)ar.AsyncState; + var ps = poolMember.PowerShell; + if (ps == null) + { + return; + } + lock (ps) + { + ps.EndStop(ar); + poolMember.m_powerShell = null; + } + } } } \ No newline at end of file diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 6031c3a..c087fc4 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -39,10 +39,16 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can public void AddInput(ScriptBlock scriptblock,PSObject inputObject) { - var powerShell = WaitForAvailablePowershell(); - powerShell.BeginInvoke(scriptblock, inputObject); - Interlocked.Increment(ref m_busyCount); - } + try { + var powerShell = WaitForAvailablePowershell(); + powerShell.BeginInvoke(scriptblock, inputObject); + Interlocked.Increment(ref m_busyCount); + } + catch(OperationCanceledException) + { + Stop(); + } + } public void Open() { @@ -93,7 +99,18 @@ public void ReportCompletion(PowerShellPoolMember poolmember) { Interlocked.Decrement(ref m_busyCount); Interlocked.Increment(ref m_processedCount); - m_availablePoolMembers.Add(poolmember); + if(poolmember.PowerShell != null) + { + m_availablePoolMembers.Add(poolmember); + } + } + + private void Stop() + { + foreach (var poolMember in m_availablePoolMembers.GetConsumingEnumerable()) + { + poolMember.Stop(); + } } } } \ No newline at end of file From 8f7f10538014d1daac7f54bb1f72f6e78672cd28 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 24 Nov 2015 09:43:42 +0100 Subject: [PATCH 11/38] More robust cancellation --- src/PSParallel/InvokeParallelCommand.cs | 18 +++++---- src/PSParallel/PowerShellPoolMember.cs | 47 ++++++++++------------ src/PSParallel/PowershellPool.cs | 30 +++++++------- src/PSParallelTests/InvokeParallelTests.cs | 1 - 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 2ccbd49..ba4aa64 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -29,7 +29,7 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [Parameter(ParameterSetName = "Progress")] [ValidateNotNullOrEmpty] public string ProgressActivity { get; set; } = "Invoke-Parallel"; - + [Parameter] [ValidateRange(1,128)] public int ThrottleLimit { get; set; } = 32; @@ -45,8 +45,7 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable public SwitchParameter NoProgress { get; set; } private readonly CancellationTokenSource m_cancelationTokenSource = new CancellationTokenSource(); - private PowershellPool m_powershellPool; - private InitialSessionState m_initialSessionState; + private PowershellPool m_powershellPool; private ProgressManager m_progressManager; // this is only used when NoProgress is not specified @@ -56,7 +55,7 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable private static InitialSessionState GetSessionState(SessionState sessionState) { var initialSessionState = InitialSessionState.CreateDefault2(); - CaptureVariables(sessionState, initialSessionState); + CaptureVariables(sessionState, initialSessionState); CaptureFunctions(sessionState, initialSessionState); return initialSessionState; } @@ -114,8 +113,8 @@ private static void CaptureVariables(SessionState sessionState, InitialSessionSt protected override void BeginProcessing() { - m_initialSessionState = InitialSessionState ?? GetSessionState(SessionState); - m_powershellPool = new PowershellPool(ThrottleLimit,m_initialSessionState, m_cancelationTokenSource.Token); + var iss = InitialSessionState ?? GetSessionState(SessionState); + m_powershellPool = new PowershellPool(ThrottleLimit, iss, m_cancelationTokenSource.Token); m_powershellPool.Open(); if (!NoProgress) { @@ -176,10 +175,15 @@ protected override void EndProcessing() protected override void StopProcessing() { m_cancelationTokenSource.Cancel(); + m_powershellPool.Stop(); } private void WriteOutputs() { + if (m_cancelationTokenSource.IsCancellationRequested) + { + return; + } var streams = m_powershellPool.Streams; foreach (var o in streams.Output.ReadAll()) { @@ -218,7 +222,7 @@ private void WriteOutputs() public void Dispose() { - m_powershellPool.Dispose(); + m_powershellPool.Dispose(); m_cancelationTokenSource.Dispose(); } diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 0bc2eab..5718898 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -1,6 +1,5 @@ using System; using System.Management.Automation; -using System.Threading; namespace PSParallel { @@ -8,8 +7,8 @@ class PowerShellPoolMember : IDisposable { private readonly PowershellPool m_pool; private readonly PowerShellPoolStreams m_poolStreams; - private PowerShell m_powerShell; - public PowerShell PowerShell => m_powerShell; + private PowerShell m_powerShell; + public PowerShell PowerShell => m_powerShell; private readonly PSDataCollection m_input =new PSDataCollection(); private PSDataCollection m_output; @@ -25,20 +24,15 @@ private void PowerShellOnInvocationStateChanged(object sender, PSInvocationState { switch (psInvocationStateChangedEventArgs.InvocationStateInfo.State) { - case PSInvocationState.Stopped: + ReleasePowerShell(); + m_pool.ReportStopped(this); + break; case PSInvocationState.Completed: case PSInvocationState.Failed: - var ps = m_powerShell; - if(ps != null) - { - lock(ps) { - ReturnPowerShell(ps); - m_powerShell = null; - } - CreatePowerShell(); - } - m_pool.ReportCompletion(this); + ReleasePowerShell(); + CreatePowerShell(); + m_pool.ReportAvailable(this); break; } } @@ -53,12 +47,13 @@ private void CreatePowerShell() m_output.DataAdded += OutputOnDataAdded; } - private void ReturnPowerShell(PowerShell powershell) + private void ReleasePowerShell() { - UnhookStreamEvents(powershell.Streams); - powershell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; + UnhookStreamEvents(m_powerShell.Streams); + m_powerShell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; m_output.DataAdded -= OutputOnDataAdded; - powershell.Dispose(); ; + m_powerShell.Dispose(); + m_powerShell = null; } @@ -150,22 +145,22 @@ private void VerboseOnDataAdded(object sender, DataAddedEventArgs dataAddedEvent public void Stop() { - m_powerShell?.BeginStop(OnStopped, this); + if(m_powerShell.InvocationStateInfo.State != PSInvocationState.Stopped) + { + UnhookStreamEvents(m_powerShell.Streams); + m_powerShell.BeginStop(OnStopped, null); + } } private void OnStopped(IAsyncResult ar) { - var poolMember = (PowerShellPoolMember)ar.AsyncState; - var ps = poolMember.PowerShell; + var ps = m_powerShell; if (ps == null) { return; } - lock (ps) - { - ps.EndStop(ar); - poolMember.m_powerShell = null; - } + ps.EndStop(ar); + m_powerShell = null; } } } \ No newline at end of file diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index c087fc4..8ea267b 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -40,15 +40,14 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can public void AddInput(ScriptBlock scriptblock,PSObject inputObject) { try { - var powerShell = WaitForAvailablePowershell(); - powerShell.BeginInvoke(scriptblock, inputObject); + var powerShell = WaitForAvailablePowershell(); Interlocked.Increment(ref m_busyCount); + powerShell.BeginInvoke(scriptblock, inputObject); } catch(OperationCanceledException) { - Stop(); } - } + } public void Open() { @@ -87,30 +86,33 @@ private PowerShellPoolMember WaitForAvailablePowershell() public void Dispose() { - foreach (var pm in m_poolMembers) - { - pm.Dispose(); - } Streams.Dispose(); m_availablePoolMembers.Dispose(); } - public void ReportCompletion(PowerShellPoolMember poolmember) + public void ReportAvailable(PowerShellPoolMember poolmember) { Interlocked.Decrement(ref m_busyCount); Interlocked.Increment(ref m_processedCount); - if(poolmember.PowerShell != null) + if(!m_cancellationToken.IsCancellationRequested) { m_availablePoolMembers.Add(poolmember); } } - private void Stop() + public void ReportStopped(PowerShellPoolMember powerShellPoolMember) { - foreach (var poolMember in m_availablePoolMembers.GetConsumingEnumerable()) + Interlocked.Decrement(ref m_busyCount); + } + + public void Stop() { - poolMember.Stop(); + m_availablePoolMembers.CompleteAdding(); + foreach (var poolMember in m_poolMembers) + { + poolMember.Stop(); + } + WaitForAllPowershellCompleted(5000); } } - } } \ No newline at end of file diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index e9194f7..f102c81 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; From afc53138132794ec07d07ad9d54ad0b90f79f3e3 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 24 Nov 2015 09:45:26 +0100 Subject: [PATCH 12/38] Fixing copying of localized files in install script --- scripts/Install.ps1 | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/scripts/Install.ps1 b/scripts/Install.ps1 index ff1c97f..b1ca26e 100644 --- a/scripts/Install.ps1 +++ b/scripts/Install.ps1 @@ -4,7 +4,7 @@ $rootDir = Split-Path (Split-Path $MyInvocation.MyCommand.Path) if ('' -eq $InstallDirectory) { - $personalModules = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath WindowsPowerShell\Modules + $personalModules = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath WindowsPowerShell\Modules\ if (($env:PSModulePath -split ';') -notcontains $personalModules) { Write-Warning "$personalModules is not in `$env:PSModulePath" @@ -25,14 +25,28 @@ if (!(Test-Path $InstallDirectory)) $moduleFileList = @( - 'PSParallel.psd1' - 'en-US\PSParallel.dll-Help.xml' - 'en-US\about_PSParallel.Help.txt' - + 'PSParallel.psd1' ) $binaryFileList = 'src\PsParallel\bin\Release\PSParallel.dll' +$localizations = @{ + 'en-us' = @( + 'PSParallel.dll-Help.xml' + 'about_PSParallel.Help.txt' + ) +} - +foreach($kv in $localizations.GetEnumerator()) +{ + $lang = $kv.Name + if(-not (Test-Path $InstallDirectory\$lang)) + { + $null = MkDir $InstallDirectory\$lang + } + foreach($v in $kv.Value){ + $locPath = Join-Path $lang $v + Copy-Item $rootDir\module\$locPath -Destination $InstallDirectory\$locPath + } +} $binaryFileList | foreach { Copy-Item "$rootDir\$_" -Destination $InstallDirectory } $moduleFileList | foreach {Copy-Item "$rootdir\module\$_" -Destination $InstallDirectory\$_ } @@ -41,6 +55,3 @@ Get-ChildItem -Recurse -Path $InstallDirectory $cert = Get-Item Cert:\CurrentUser\My\98D6087848D1213F20149ADFE698473429A9B15D Get-ChildItem -File $InstallDirectory | Set-AuthenticodeSignature -Certificate $cert - - - \ No newline at end of file From 151d3c1eb271772d19cd05c7e334eeae750d2098 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 24 Nov 2015 09:54:17 +0100 Subject: [PATCH 13/38] whitespace --- src/PSParallel/PowershellPool.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 8ea267b..d809d6f 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -106,13 +106,13 @@ public void ReportStopped(PowerShellPoolMember powerShellPoolMember) } public void Stop() + { + m_availablePoolMembers.CompleteAdding(); + foreach (var poolMember in m_poolMembers) { - m_availablePoolMembers.CompleteAdding(); - foreach (var poolMember in m_poolMembers) - { - poolMember.Stop(); - } - WaitForAllPowershellCompleted(5000); + poolMember.Stop(); } + WaitForAllPowershellCompleted(5000); } + } } \ No newline at end of file From 0e00aa37598b2150a29eb8b9190ca9b0a8c95d7e Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 24 Nov 2015 09:58:59 +0100 Subject: [PATCH 14/38] Publishing v 1.7 --- module/PSParallel.psd1 | Bin 2100 -> 2100 bytes scripts/Install.ps1 | 2 +- scripts/Publish-ToGallery.ps1 | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 04d69a738bb1f997b3e9916243a39615a926f0ae..52014e980ca6087ed523997932a38b3d7576e2a3 100644 GIT binary patch delta 14 VcmdlYuti{l7bBzjW^cxqYyc%M1gZc4 delta 14 VcmdlYuti{l7bBzDW^cxqYyc%G1gQW3 diff --git a/scripts/Install.ps1 b/scripts/Install.ps1 index b1ca26e..b2b38c1 100644 --- a/scripts/Install.ps1 +++ b/scripts/Install.ps1 @@ -54,4 +54,4 @@ $moduleFileList | foreach {Copy-Item "$rootdir\module\$_" -Destination $Install Get-ChildItem -Recurse -Path $InstallDirectory $cert = Get-Item Cert:\CurrentUser\My\98D6087848D1213F20149ADFE698473429A9B15D -Get-ChildItem -File $InstallDirectory | Set-AuthenticodeSignature -Certificate $cert +Get-ChildItem -File $InstallDirectory -Include *.dll,*.psd1 | Set-AuthenticodeSignature -Certificate $cert diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index e9bb063..d593445 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Adding InitialSessionState parameter. More robust function capture" + ReleaseNote = "More robust cancellation" ProjectUri = "https://github.com/powercode/PSParallel" } From 5d8e9ffdf5be542e8b7b4d78ac7df5e60072af26 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 24 Nov 2015 10:28:40 +0100 Subject: [PATCH 15/38] Updating readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd66059..c653793 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Install-Module PSParallel (1..255).Foreach{"192.168.0.$_"} | Invoke-Parallel { [PSCustomObject] @{IP=$_;Result=ping.exe -4 -a -w 20 $_}} ``` -Variables are captured from the parent session but functions are not. +Variables and functions are captured from the parent session. ##Throttling To control the degree of parallelism, i.e. the number of concurrent runspaces, use the -ThrottleLimit parameter From d17822b4234afdee99c82e0708e40663a185d80b Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 24 Nov 2015 16:14:21 +0100 Subject: [PATCH 16/38] Adding importModule parameter --- module/PSParallel.psd1 | Bin 2100 -> 2100 bytes module/en-US/PSParallel.dll-Help.xml | 370 +++++++++++++++++------- scripts/Publish-ToGallery.ps1 | 2 +- src/PSParallel/InvokeParallelCommand.cs | 31 +- 4 files changed, 294 insertions(+), 109 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 52014e980ca6087ed523997932a38b3d7576e2a3..b92da8458ffbf3d5201419fdaa738602e7b29c31 100644 GIT binary patch delta 14 VcmdlYuti{l7bBy^W^cxqYyc%S1gii5 delta 14 VcmdlYuti{l7bBzjW^cxqYyc%M1gZc4 diff --git a/module/en-US/PSParallel.dll-Help.xml b/module/en-US/PSParallel.dll-Help.xml index c0ca823..8b1bf0a 100644 --- a/module/en-US/PSParallel.dll-Help.xml +++ b/module/en-US/PSParallel.dll-Help.xml @@ -1,14 +1,10 @@ - - - - + + + - + Invoke-Parallel @@ -25,74 +21,80 @@ Th - + Invoke-Parallel ScriptBlock - - + Specifies the operation that is performed on each input object. Enter a script block that describes the operation. ScriptBlock - - + - + ParentProgressId Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. Int32 - - - + ProgressId - Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than one progress bar in a single command. If the progress bars do not have different IDs, they are superimposed instead of being displayed in a series. + Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than + one progress bar in a single command. If the progress bars do not have different IDs, they are superimposed instead of +being displayed in a series. Int32 - - - + ProgressActivity - Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose progress is being reported. + Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose pr +ogress is being reported. String - - ThrottleLimit - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this +parameter or enter a value of 0, the default value, 16, is used. Int32 - - InitialSessionState + + ImportModule - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. - -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. - + Specify the names of the modules to import into the runspaces running the scriptblock to process. - InitialSessionState + String[] - + InputObject - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable that contains the objects, or type a command or expression that gets the objects. + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. PSObject - - + + + InformationAction + + + + ActionPreference + + + InformationVariable + + + + String @@ -100,68 +102,210 @@ By default, InitialSessionState.Create2() is used and the functions and variable ScriptBlock - - + Specifies the operation that is performed on each input object. Enter a script block that describes the operation. ScriptBlock - - + + + + ParentProgressId + + Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. + + Int32 + + + ProgressId + + Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than + one progress bar in a single command. If the progress bars do not have different IDs, they are superimposed instead of +being displayed in a series. + + Int32 + + + ProgressActivity + + Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose pr +ogress is being reported. + + String + + + ThrottleLimit + + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this +parameter or enter a value of 0, the default value, 16, is used. + + Int32 + - + InitialSessionState - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. - -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc a +vailable to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then importe +d. InitialSessionState - - + + + InputObject + + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. + + PSObject + + + InformationAction + + + + ActionPreference + + + InformationVariable + + + + String + + + + Invoke-Parallel + + ScriptBlock + + Specifies the operation that is performed on each input object. Enter a script block that describes the operation. + + ScriptBlock + ThrottleLimit - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this +parameter or enter a value of 0, the default value, 16, is used. Int32 - + + InitialSessionState + + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc a +vailable to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then importe +d. + + InitialSessionState + + InputObject - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable that contains the objects, or type a command or expression that gets the objects. + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. PSObject + + + NoProgress + + Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. + + SwitchParameter + + + InformationAction + + + + ActionPreference + + + InformationVariable + + + + String + + + + Invoke-Parallel + + ScriptBlock + + Specifies the operation that is performed on each input object. Enter a script block that describes the operation. + + ScriptBlock + + + + ThrottleLimit + + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this +parameter or enter a value of 0, the default value, 16, is used. + + Int32 + + + + ImportModule + + Specify the names of the modules to import into the runspaces running the scriptblock to process. + + String[] - + + InputObject + + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. + + PSObject + + NoProgress Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. SwitchParameter - + + + InformationAction + + + + ActionPreference + + + InformationVariable + + + + String - + ScriptBlock - - + Specifies the operation that is performed on each input object. Enter a script block that describes the operation. ScriptBlock ScriptBlock - - + - + ParentProgressId Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. @@ -171,39 +315,40 @@ By default, InitialSessionState.Create2() is used and the functions and variable Int32 - - + - + ProgressId - Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than one progress bar in a single command. If the progress bars do not have different IDs, they are superimposed instead of being displayed in a series. + Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than + one progress bar in a single command. If the progress bars do not have different IDs, they are superimposed instead of +being displayed in a series. Int32 Int32 - - + - + ProgressActivity - Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose progress is being reported. + Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose pr +ogress is being reported. String String - - + ThrottleLimit - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this +parameter or enter a value of 0, the default value, 16, is used. Int32 @@ -212,31 +357,72 @@ By default, InitialSessionState.Create2() is used and the functions and variable - - InitialSessionState + + ImportModule - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. - + Specify the names of the modules to import into the runspaces running the scriptblock to process. - InitialSessionState + String[] + + String[] + + - + InputObject - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable that contains the objects, or type a command or expression that gets the objects. + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. PSObject PSObject - - + - + + InformationAction + + + + ActionPreference + + ActionPreference + + + + + + InformationVariable + + + + String + + String + + + + + + InitialSessionState + + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc a +vailable to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then importe +d. + + InitialSessionState + + InitialSessionState + + + + + NoProgress Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. @@ -250,11 +436,10 @@ By default, InitialSessionState.Create2() is used and the functions and variable - + - System.Management.Automation.PSObject - + System.Management.Automation.PSObject @@ -262,20 +447,8 @@ By default, InitialSessionState.Create2() is used and the functions and variable - - - - - System.Object - - - - - - - - + -------------------------- EXAMPLE 1 -------------------------- @@ -283,12 +456,11 @@ By default, InitialSessionState.Create2() is used and the functions and variable (1..255).ForEach{"192.168.0.$_"} | Invoke-Parallel {$ip = $_; $res = ping.exe -4 -w 20 $_; [PSCustomObject] @{IP=$ip;Res=$res}} -ThrottleLimit 64 - - This example pings all iP v4 addresses on a subnet, specifying Throttlelimit to 64, i.e. running up to 64 runspaces in parallel. - + This example pings all iP v4 addresses on a subnet, specifying Throttlelimit to 64, i.e. running up to 64 runspaces in p +arallel. - + \ No newline at end of file diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index d593445..3f2b43a 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "More robust cancellation" + ReleaseNote = "Adding ImportModule parameter" ProjectUri = "https://github.com/powercode/PSParallel" } diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index ba4aa64..581f023 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -11,22 +11,25 @@ namespace PSParallel { [Alias("ipa")] - [Cmdlet("Invoke", "Parallel", DefaultParameterSetName = "Progress")] + [Cmdlet("Invoke", "Parallel", DefaultParameterSetName = "ProgressSessionStateParams")] public sealed class InvokeParallelCommand : PSCmdlet, IDisposable { [Parameter(Mandatory = true, Position = 0)] public ScriptBlock ScriptBlock { get; set; } [Alias("ppi")] - [Parameter(ParameterSetName = "Progress")] + [Parameter(ParameterSetName = "ProgressInitialSessionState")] + [Parameter(ParameterSetName = "ProgressSessionStateParams")] public int ParentProgressId { get; set; } = -1; [Alias("pi")] - [Parameter(ParameterSetName = "Progress")] + [Parameter(ParameterSetName = "ProgressInitialSessionState")] + [Parameter(ParameterSetName = "ProgressSessionStateParams")] public int ProgressId { get; set; } = 1000; [Alias("pa")] - [Parameter(ParameterSetName = "Progress")] + [Parameter(ParameterSetName = "ProgressInitialSessionState")] + [Parameter(ParameterSetName = "ProgressSessionStateParams")] [ValidateNotNullOrEmpty] public string ProgressActivity { get; set; } = "Invoke-Parallel"; @@ -34,14 +37,20 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [ValidateRange(1,128)] public int ThrottleLimit { get; set; } = 32; - [Parameter] + [Parameter(ParameterSetName = "ProgressInitialSessionState", Mandatory = true)] + [Parameter(ParameterSetName = "NoProgressInitialSessionState", Mandatory = true)] [ValidateNotNull] public InitialSessionState InitialSessionState { get; set; } + [Parameter(ParameterSetName = "ProgressSessionStateParams")] + [Parameter(ParameterSetName = "NoProgressSessionStateParams")] + public string[] ImportModule { get; set; } + [Parameter(ValueFromPipeline = true, Mandatory = true)] public PSObject InputObject { get; set; } - - [Parameter(ParameterSetName = "NoProgress")] + + [Parameter(ParameterSetName = "NoProgressInitialSessionState")] + [Parameter(ParameterSetName = "NoProgressSessionStateParams")] public SwitchParameter NoProgress { get; set; } private readonly CancellationTokenSource m_cancelationTokenSource = new CancellationTokenSource(); @@ -52,11 +61,15 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable // Input is then captured in ProcessRecored and processed in EndProcessing private List m_input; - private static InitialSessionState GetSessionState(SessionState sessionState) + private static InitialSessionState GetSessionState(SessionState sessionState, string[] modulesToImport) { var initialSessionState = InitialSessionState.CreateDefault2(); CaptureVariables(sessionState, initialSessionState); CaptureFunctions(sessionState, initialSessionState); + if (modulesToImport != null) + { + initialSessionState.ImportPSModule(modulesToImport); + } return initialSessionState; } @@ -113,7 +126,7 @@ private static void CaptureVariables(SessionState sessionState, InitialSessionSt protected override void BeginProcessing() { - var iss = InitialSessionState ?? GetSessionState(SessionState); + var iss = InitialSessionState ?? GetSessionState(SessionState, ImportModule); m_powershellPool = new PowershellPool(ThrottleLimit, iss, m_cancelationTokenSource.Token); m_powershellPool.Open(); if (!NoProgress) From c15428002fe5bd51558e8fe698a270f37db11c37 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 24 Nov 2015 21:35:16 +0100 Subject: [PATCH 17/38] Adding PSTags to module manifest --- module/PSParallel.psd1 | Bin 2100 -> 3140 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index b92da8458ffbf3d5201419fdaa738602e7b29c31..7cfc8ec6b0bee0a838b19ab0bd0711709ff9e76d 100644 GIT binary patch delta 970 zcmbW0O-lk%6o%h6B0d(LBhUvf%uhLIx~!;2_gOn|455|LhF80 z&%M)xrUN0DJNMl4@t*gbbLV68bN4-y-aAxAj&gcbQ?8EmpgZKMYVtHtsLFILy>onp zwJRULP$RaZf5lA}S#!=5cCUsR$+6>KGU1^$sn6mhSj1+Y>iKY9Lx5c zaJ>h(r9M&wd;s|}x`5qfnZistj&3Q~#nZ=9M;~d6=!P`olX8oR zRAd&vdpYdAz#V zYWVb^)wdlDxRq4YxeB_be+8(<#AbP!Ie2;otxntuyta~vO}|p9%4{kl7cF^rl|)H- zoa6>{Y(rWU*Z*l&#g4&a3_0*zTJ0>%?=R>0$;SFI5&P@{@eNK-PkiSeY1iu|;5m0*8nu11|#?7}vsC08Ls0j{pDw From 6668ab6c3ac976ae377c3ee84bd3424d32c357c3 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 25 Nov 2015 00:06:08 +0100 Subject: [PATCH 18/38] Fixing memory leak and publishing v1.9 --- module/PSParallel.psd1 | Bin 3140 -> 3140 bytes scripts/Publish-ToGallery.ps1 | 2 +- src/PSParallel/PowershellPool.cs | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 7cfc8ec6b0bee0a838b19ab0bd0711709ff9e76d..c74b395e5253807ce88c1b114dea7b4c5617fa5d 100644 GIT binary patch delta 14 VcmX>iaYSN+7bBzPW^cxyTmUD(1n>X= delta 14 VcmX>iaYSN+7bBy^W^cxyTmUDz1n&R< diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index 3f2b43a..b837090 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Adding ImportModule parameter" + ReleaseNote = "Fixing big memory leak" ProjectUri = "https://github.com/powercode/PSParallel" } diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index d809d6f..0074340 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -88,6 +88,7 @@ public void Dispose() { Streams.Dispose(); m_availablePoolMembers.Dispose(); + m_runspacePool.Dispose(); } public void ReportAvailable(PowerShellPoolMember poolmember) From 790b83ef2a914c5eb4437cf0ae7451c3ca1dfc69 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 25 Nov 2015 01:36:16 +0100 Subject: [PATCH 19/38] Adding ImportVariable,ImportFunction parameters. Publishing v2 to gallery --- module/PSParallel.psd1 | Bin 3140 -> 3140 bytes module/en-US/PSParallel.dll-Help.xml | 314 +++++++------- scripts/Publish-ToGallery.ps1 | 2 +- src/PSParallel/InvokeParallelCommand.cs | 91 ++-- src/PSParallelTests/InvokeParallelTests.cs | 466 ++++++++++++--------- 5 files changed, 488 insertions(+), 385 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index c74b395e5253807ce88c1b114dea7b4c5617fa5d..c827da22b4bb767a9b5f0e791b4ebde35fddee99 100644 GIT binary patch delta 18 ZcmX>iaYSN+8zZX`gC2vyW^cxyTmUt>1xEk? delta 18 ZcmX>iaYSN+8zZYBgC2wBW^cxyTmUuY1y29~ diff --git a/module/en-US/PSParallel.dll-Help.xml b/module/en-US/PSParallel.dll-Help.xml index 8b1bf0a..7fd1b32 100644 --- a/module/en-US/PSParallel.dll-Help.xml +++ b/module/en-US/PSParallel.dll-Help.xml @@ -1,7 +1,11 @@ - - + + + @@ -18,28 +22,31 @@ - Th + The cmdlet uses a RunspacePool an and invokes the provied scriptblock once for each input. + +To control the environment of the scriptblock, the ImportModule, ImportVariable and ImportFunction parameters can be used. + + Invoke-Parallel - + ScriptBlock Specifies the operation that is performed on each input object. Enter a script block that describes the operation. ScriptBlock - - + ParentProgressId Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. Int32 - + ProgressId Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than @@ -48,7 +55,7 @@ being displayed in a series. Int32 - + ProgressActivity Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose pr @@ -56,21 +63,40 @@ ogress is being reported. String - + ThrottleLimit - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. Int32 - - + ImportModule Specify the names of the modules to import into the runspaces running the scriptblock to process. String[] + + + ImportVariable + + Specifies the variables to import into the Runspaces running the specified ScriptBlock. + +If not specified, all variables are imported. + + String[] + + + + + ImportFunction + + Specifies the functions to import into the Runspaces running the specified ScriptBlock. + +It this variables isn't specified, all functions are imported. + + String[] @@ -82,39 +108,24 @@ at contains the objects, or type a command or expression that gets the objects.< PSObject - - InformationAction - - - - ActionPreference - - - InformationVariable - - - - String - Invoke-Parallel - + ScriptBlock Specifies the operation that is performed on each input object. Enter a script block that describes the operation. ScriptBlock - - + ParentProgressId Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. Int32 - + ProgressId Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than @@ -123,7 +134,7 @@ being displayed in a series. Int32 - + ProgressActivity Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose pr @@ -131,24 +142,25 @@ ogress is being reported. String - + ThrottleLimit - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. Int32 - - + InitialSessionState - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc a -vailable to the ScriptBlock. -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then importe -d. + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc +available to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then +imported. InitialSessionState + + InputObject @@ -158,142 +170,110 @@ at contains the objects, or type a command or expression that gets the objects.< PSObject - - InformationAction - - - - ActionPreference - - - InformationVariable - - - - String - Invoke-Parallel - + ScriptBlock Specifies the operation that is performed on each input object. Enter a script block that describes the operation. - ScriptBlock - + ScriptBlock - + ThrottleLimit - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. - Int32 - + Int32 - + InitialSessionState - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc a -vailable to the ScriptBlock. -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then importe -d. + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc +available to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then +imported. - InitialSessionState - - - InputObject - - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th -at contains the objects, or type a command or expression that gets the objects. - - PSObject + InitialSessionState + + - + NoProgress Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. - SwitchParameter + SwitchParameter - - InformationAction - - - - ActionPreference - - - InformationVariable + + InputObject - + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. - String + PSObject Invoke-Parallel - + ScriptBlock Specifies the operation that is performed on each input object. Enter a script block that describes the operation. - ScriptBlock - + ScriptBlock - - ThrottleLimit + + InputObject - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this -parameter or enter a value of 0, the default value, 16, is used. + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. - Int32 - + PSObject - + ImportModule Specify the names of the modules to import into the runspaces running the scriptblock to process. - String[] + String[] + + + ImportVariable + + Specifies the variables to import into the Runspaces running the specified ScriptBlock. + +If not specified, all variables are imported. + + String[] - - InputObject + + ImportFunction - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th -at contains the objects, or type a command or expression that gets the objects. + Specifies the functions to import into the Runspaces running the specified ScriptBlock. + +It this variables isn't specified, all functions are imported. - PSObject + String[] + + - + NoProgress Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. - SwitchParameter - - - InformationAction - - - - ActionPreference - - - InformationVariable - - - - String + SwitchParameter - + ScriptBlock Specifies the operation that is performed on each input object. Enter a script block that describes the operation. @@ -305,7 +285,7 @@ at contains the objects, or type a command or expression that gets the objects.< - + ParentProgressId Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. @@ -317,7 +297,7 @@ at contains the objects, or type a command or expression that gets the objects.< - + ProgressId Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than @@ -331,7 +311,7 @@ being displayed in a series. - + ProgressActivity Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose pr @@ -344,10 +324,10 @@ ogress is being reported. - + ThrottleLimit - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. Int32 @@ -357,7 +337,7 @@ parameter or enter a value of 0, the default value, 16, is used. - + ImportModule Specify the names of the modules to import into the runspaces running the scriptblock to process. @@ -367,57 +347,47 @@ parameter or enter a value of 0, the default value, 16, is used. String[] - - - - - InputObject - - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th -at contains the objects, or type a command or expression that gets the objects. - - PSObject - - PSObject - - - - InformationAction + + ImportVariable - + Specifies the variables to import into the Runspaces running the specified ScriptBlock. + +If not specified, all variables are imported. - ActionPreference + String[] - ActionPreference + String[] - + + - - InformationVariable + + ImportFunction - + Specifies the functions to import into the Runspaces running the specified ScriptBlock. + +It this variables isn't specified, all functions are imported. - String + String[] - String + String[] - + + - - InitialSessionState + + InputObject - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc a -vailable to the ScriptBlock. -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then importe -d. + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. - InitialSessionState + PSObject - InitialSessionState + PSObject @@ -434,6 +404,22 @@ d. + + InitialSessionState + + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc +available to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then +imported. + + InitialSessionState + + InitialSessionState + + + + + @@ -444,6 +430,12 @@ d. + + + + + + @@ -462,5 +454,5 @@ arallel. - + \ No newline at end of file diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index b837090..8951184 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Fixing big memory leak" + ReleaseNote = "Fixing parameter set bug" ProjectUri = "https://github.com/powercode/PSParallel" } diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 581f023..9817886 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -11,25 +11,26 @@ namespace PSParallel { [Alias("ipa")] - [Cmdlet("Invoke", "Parallel", DefaultParameterSetName = "ProgressSessionStateParams")] + [Cmdlet("Invoke", "Parallel", DefaultParameterSetName = "SessionStateParams")] public sealed class InvokeParallelCommand : PSCmdlet, IDisposable { [Parameter(Mandatory = true, Position = 0)] public ScriptBlock ScriptBlock { get; set; } [Alias("ppi")] - [Parameter(ParameterSetName = "ProgressInitialSessionState")] - [Parameter(ParameterSetName = "ProgressSessionStateParams")] + [Parameter(ParameterSetName = "InitialSessionState")] + [Parameter(ParameterSetName = "SessionStateParams")] + [Parameter(ParameterSetName = "")] public int ParentProgressId { get; set; } = -1; [Alias("pi")] - [Parameter(ParameterSetName = "ProgressInitialSessionState")] - [Parameter(ParameterSetName = "ProgressSessionStateParams")] + [Parameter(ParameterSetName = "InitialSessionState")] + [Parameter(ParameterSetName = "SessionStateParams")] public int ProgressId { get; set; } = 1000; [Alias("pa")] - [Parameter(ParameterSetName = "ProgressInitialSessionState")] - [Parameter(ParameterSetName = "ProgressSessionStateParams")] + [Parameter(ParameterSetName = "InitialSessionState")] + [Parameter(ParameterSetName = "SessionStateParams")] [ValidateNotNullOrEmpty] public string ProgressActivity { get; set; } = "Invoke-Parallel"; @@ -37,35 +38,41 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [ValidateRange(1,128)] public int ThrottleLimit { get; set; } = 32; - [Parameter(ParameterSetName = "ProgressInitialSessionState", Mandatory = true)] - [Parameter(ParameterSetName = "NoProgressInitialSessionState", Mandatory = true)] + [Parameter(ParameterSetName = "InitialSessionState", Mandatory = true)] [ValidateNotNull] public InitialSessionState InitialSessionState { get; set; } - [Parameter(ParameterSetName = "ProgressSessionStateParams")] - [Parameter(ParameterSetName = "NoProgressSessionStateParams")] + [Parameter(ParameterSetName = "SessionStateParams")] + [ValidateNotNull] public string[] ImportModule { get; set; } + [Parameter(ParameterSetName = "SessionStateParams")] + [ValidateNotNull] + public string[] ImportVariable{ get; set; } + + [Parameter(ParameterSetName = "SessionStateParams")] + [ValidateNotNull] + public string[] ImportFunction{ get; set; } + [Parameter(ValueFromPipeline = true, Mandatory = true)] public PSObject InputObject { get; set; } - - [Parameter(ParameterSetName = "NoProgressInitialSessionState")] - [Parameter(ParameterSetName = "NoProgressSessionStateParams")] + + [Parameter] public SwitchParameter NoProgress { get; set; } private readonly CancellationTokenSource m_cancelationTokenSource = new CancellationTokenSource(); - private PowershellPool m_powershellPool; + private PowershellPool m_powershellPool; private ProgressManager m_progressManager; // this is only used when NoProgress is not specified // Input is then captured in ProcessRecored and processed in EndProcessing private List m_input; - private static InitialSessionState GetSessionState(SessionState sessionState, string[] modulesToImport) + private static InitialSessionState GetSessionState(SessionState sessionState, string[] modulesToImport, string[] variablesToImport, string[] functionsToImport) { var initialSessionState = InitialSessionState.CreateDefault2(); - CaptureVariables(sessionState, initialSessionState); - CaptureFunctions(sessionState, initialSessionState); + CaptureVariables(sessionState, initialSessionState, variablesToImport); + CaptureFunctions(sessionState, initialSessionState, functionsToImport); if (modulesToImport != null) { initialSessionState.ImportPSModule(modulesToImport); @@ -73,13 +80,13 @@ private static InitialSessionState GetSessionState(SessionState sessionState, st return initialSessionState; } - private static IEnumerable GetFunctions(SessionState sessionState) + private static IEnumerable GetFunctions(SessionState sessionState, string[] functionsToImport) { try { var functionDrive = sessionState.InvokeProvider.Item.Get("function:"); var baseObject = (Dictionary.ValueCollection) functionDrive[0].BaseObject; - return baseObject; + return functionsToImport == null ? baseObject : baseObject.Where(f=>functionsToImport.Contains(f.Name, StringComparer.OrdinalIgnoreCase)); } catch (DriveNotFoundException) { @@ -87,14 +94,15 @@ private static IEnumerable GetFunctions(SessionState sessionState) } } - private static IEnumerable GetVariables(SessionState sessionState) + private static IEnumerable GetVariables(SessionState sessionState, string[] variablesToImport) { try { - string[] noTouchVariables = new[] {"null", "true", "false", "Error"}; + string[] noTouchVariables = {"null", "true", "false", "Error"}; var variables = sessionState.InvokeProvider.Item.Get("Variable:"); var psVariables = (IEnumerable) variables[0].BaseObject; - return psVariables.Where(p=>!noTouchVariables.Contains(p.Name)); + return psVariables.Where(p=>!noTouchVariables.Contains(p.Name) && + (variablesToImport == null || variablesToImport.Contains(p.Name, StringComparer.OrdinalIgnoreCase))); } catch (DriveNotFoundException) { @@ -102,17 +110,17 @@ private static IEnumerable GetVariables(SessionState sessionState) } } - private static void CaptureFunctions(SessionState sessionState, InitialSessionState initialSessionState) + private static void CaptureFunctions(SessionState sessionState, InitialSessionState initialSessionState, string[] functionsToImport) { - var functions = GetFunctions(sessionState); + var functions = GetFunctions(sessionState, functionsToImport); foreach (var func in functions) { initialSessionState.Commands.Add(new SessionStateFunctionEntry(func.Name, func.Definition)); } } - private static void CaptureVariables(SessionState sessionState, InitialSessionState initialSessionState) + private static void CaptureVariables(SessionState sessionState, InitialSessionState initialSessionState, string[] variablesToImport) { - var variables = GetVariables(sessionState); + var variables = GetVariables(sessionState, variablesToImport); foreach(var variable in variables) { var existing = initialSessionState.Variables[variable.Name].FirstOrDefault(); @@ -124,9 +132,24 @@ private static void CaptureVariables(SessionState sessionState, InitialSessionSt } } + private void ValidateParameters() + { + if (NoProgress) + { + var boundParameters = MyInvocation.BoundParameters; + foreach(var p in new[]{nameof(ProgressActivity), nameof(ParentProgressId), nameof(ProgressId)}) + { + if (!boundParameters.ContainsKey(p)) continue; + var argumentException = new ArgumentException($"'{p}' must not be specified together with 'NoProgress'", p); + ThrowTerminatingError(new ErrorRecord(argumentException, "InvalidProgressParam", ErrorCategory.InvalidArgument, p)); + } + } + } + protected override void BeginProcessing() { - var iss = InitialSessionState ?? GetSessionState(SessionState, ImportModule); + ValidateParameters(); + var iss = InitialSessionState ?? GetSessionState(SessionState, ImportModule, ImportVariable, ImportFunction); m_powershellPool = new PowershellPool(ThrottleLimit, iss, m_cancelationTokenSource.Token); m_powershellPool.Open(); if (!NoProgress) @@ -135,6 +158,7 @@ protected override void BeginProcessing() m_input = new List(500); } } + protected override void ProcessRecord() { @@ -165,9 +189,12 @@ protected override void EndProcessing() } } while(!m_powershellPool.WaitForAllPowershellCompleted(100)) - { - var pr = m_progressManager.GetCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", m_powershellPool.ProcessedCount); - WriteProgress(pr); + { + if(!NoProgress) + { + var pr = m_progressManager.GetCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", m_powershellPool.ProcessedCount); + WriteProgress(pr); + } if (Stopping) { return; @@ -235,7 +262,7 @@ private void WriteOutputs() public void Dispose() { - m_powershellPool.Dispose(); + m_powershellPool?.Dispose(); m_cancelationTokenSource.Dispose(); } diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index f102c81..b20a100 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -40,274 +40,306 @@ public InvokeParallelTests() [TestMethod] public void TestOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - - ps.AddCommand("Invoke-Parallel") - .AddParameter("ScriptBlock", ScriptBlock.Create("$_* 2")) - .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection {1,2,3,4,5}; - input.Complete(); - var output = ps.Invoke(input); - var sum = output.Aggregate(0, (a, b) => a + b); - Assert.AreEqual(30, sum); + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + + ps.AddCommand("Invoke-Parallel") + .AddParameter("ScriptBlock", ScriptBlock.Create("$_* 2")) + .AddParameter("ThrottleLimit", 1); + var input = new PSDataCollection {1,2,3,4,5}; + input.Complete(); + var output = ps.Invoke(input); + var sum = output.Aggregate(0, (a, b) => a + b); + Assert.AreEqual(30, sum); + } } [TestMethod] public void TestParallelOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - - ps.AddCommand("Invoke-Parallel") - .AddParameter("ScriptBlock", ScriptBlock.Create("$_* 2")) - .AddParameter("ThrottleLimit", 10); - var input = new PSDataCollection(Enumerable.Range(1,1000)); - input.Complete(); - var output = ps.Invoke(input); - var sum = output.Aggregate(0, (a, b) => a + b); - Assert.AreEqual(1001000, sum); + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + + ps.AddCommand("Invoke-Parallel") + .AddParameter("ScriptBlock", ScriptBlock.Create("$_* 2")) + .AddParameter("ThrottleLimit", 10); + var input = new PSDataCollection(Enumerable.Range(1, 1000)); + input.Complete(); + var output = ps.Invoke(input); + var sum = output.Aggregate(0, (a, b) => a + b); + Assert.AreEqual(1001000, sum); + } } [TestMethod] public void TestVerboseOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("$VerbosePreference='Continue'", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel",false) + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("$VerbosePreference='Continue'", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Verbose $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); - var vrb = ps.Streams.Verbose.ReadAll(); - Assert.IsTrue(vrb.Any(v=> v.Message == "1"), "Some verbose message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); + var vrb = ps.Streams.Verbose.ReadAll(); + Assert.IsTrue(vrb.Any(v => v.Message == "1"), "Some verbose message should be '1'"); + } } [TestMethod] public void TestNoVerboseOutputWithoutPreference() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Verbose $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); - var vrb = ps.Streams.Verbose.ReadAll(); - Assert.IsFalse(vrb.Any(v => v.Message == "1"), "No verbose message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); + var vrb = ps.Streams.Verbose.ReadAll(); + Assert.IsFalse(vrb.Any(v => v.Message == "1"), "No verbose message should be '1'"); + } } [TestMethod] public void TestDebugOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("$DebugPreference='Continue'", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("$DebugPreference='Continue'", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Debug $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); - var dbg = ps.Streams.Debug.ReadAll(); - Assert.IsTrue(dbg.Any(d => d.Message == "1"), "Some debug message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); + var dbg = ps.Streams.Debug.ReadAll(); + Assert.IsTrue(dbg.Any(d => d.Message == "1"), "Some debug message should be '1'"); + } } [TestMethod] public void TestNoDebugOutputWithoutPreference() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Debug $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - var dbg = ps.Streams.Debug.ReadAll(); - Assert.IsFalse(dbg.Any(d => d.Message == "1"), "No debug message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + var dbg = ps.Streams.Debug.ReadAll(); + Assert.IsFalse(dbg.Any(d => d.Message == "1"), "No debug message should be '1'"); + } } [TestMethod] public void TestWarningOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("$WarningPreference='Continue'", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) + + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("$WarningPreference='Continue'", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Warning $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - var wrn = ps.Streams.Warning.ReadAll(); - Assert.IsTrue(wrn.Any(w => w.Message == "1"), "Some warning message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + var wrn = ps.Streams.Warning.ReadAll(); + Assert.IsTrue(wrn.Any(w => w.Message == "1"), "Some warning message should be '1'"); + } } [TestMethod] public void TestNoWarningOutputWithoutPreference() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("$WarningPreference='SilentlyContinue'", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("$WarningPreference='SilentlyContinue'", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Warning $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - var wrn = ps.Streams.Warning.ReadAll(); - Assert.IsFalse(wrn.Any(w => w.Message == "1"), "No warning message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + var wrn = ps.Streams.Warning.ReadAll(); + Assert.IsFalse(wrn.Any(w => w.Message == "1"), "No warning message should be '1'"); + } } [TestMethod] public void TestErrorOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("$ErrorActionPreference='Continue'", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("$ErrorActionPreference='Continue'", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Error -Message $_ -TargetObject $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - var err = ps.Streams.Error.ReadAll(); - Assert.IsTrue(err.Any(e => e.Exception.Message == "1"), "Some warning message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + var err = ps.Streams.Error.ReadAll(); + Assert.IsTrue(err.Any(e => e.Exception.Message == "1"), "Some warning message should be '1'"); + } } [TestMethod] public void TestNoErrorOutputWithoutPreference() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("$ErrorActionPreference='SilentlyContinue'", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("$ErrorActionPreference='SilentlyContinue'", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Error -message $_ -TargetObject $_")) .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - var err = ps.Streams.Error.ReadAll(); - Assert.IsFalse(err.Any(e => e.Exception.Message == "1"), "No Error message should be '1'"); + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + var err = ps.Streams.Error.ReadAll(); + Assert.IsFalse(err.Any(e => e.Exception.Message == "1"), "No Error message should be '1'"); + } } [TestMethod] public void TestBinaryExpressionVariableCapture() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("[int]$x=10", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("$x -eq 10")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("InputObject", 1); - - var result = ps.Invoke().First(); - Assert.IsTrue(result); + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("[int]$x=10", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", ScriptBlock.Create("$x -eq 10")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("InputObject", 1); + + var result = ps.Invoke().First(); + Assert.IsTrue(result); + } } [TestMethod] public void TestAssingmentExpressionVariableCapture() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript("[int]$x=10;", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("$y = $x * 5; $y")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("InputObject", 1); - - var result = ps.Invoke().First(); - Assert.AreEqual(50, result); + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("[int]$x=10;", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", ScriptBlock.Create("$y = $x * 5; $y")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("InputObject", 1); + + var result = ps.Invoke().First(); + Assert.AreEqual(50, result); + } } [TestMethod] public void TestProgressOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Progress -activity 'Test' -Status 'Status' -currentoperation $_")) - .AddParameter("ThrottleLimit", 1); - - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - var progress = ps.Streams.Progress.ReadAll(); - Assert.AreEqual(11, progress.Count(pr=>pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", + ScriptBlock.Create("Write-Progress -activity 'Test' -Status 'Status' -currentoperation $_")) + .AddParameter("ThrottleLimit", 1); + + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + var progress = ps.Streams.Progress.ReadAll(); + Assert.AreEqual(11, progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); + } } [TestMethod] public void TestNoProgressOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Progress -activity 'Test' -Status 'Status' -currentoperation $_")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress"); - - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - ps.Invoke(input); - var progress = ps.Streams.Progress.ReadAll(); - Assert.IsFalse( progress.Any(pr=>pr.Activity == "Invoke-Parallel")); - Assert.AreEqual(5, progress.Count(pr=>pr.Activity == "Test")); + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", + ScriptBlock.Create("Write-Progress -activity 'Test' -Status 'Status' -currentoperation $_")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("NoProgress"); + + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + ps.Invoke(input); + var progress = ps.Streams.Progress.ReadAll(); + Assert.IsFalse(progress.Any(pr => pr.Activity == "Invoke-Parallel")); + Assert.AreEqual(5, progress.Count(pr => pr.Activity == "Test")); + } } [TestMethod] public void TestFunctionCaptureOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript(@" + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript(@" function foo($x) {return $x * 2} ", false); - - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("foo $_")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress"); - - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - var output = ps.Invoke(input); - var sum = output.Aggregate(0, (a, b) => a + b); - Assert.AreEqual(30, sum); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", ScriptBlock.Create("foo $_")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("NoProgress"); + + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + var output = ps.Invoke(input); + var sum = output.Aggregate(0, (a, b) => a + b); + Assert.AreEqual(30, sum); + } } @@ -315,24 +347,76 @@ function foo($x) {return $x * 2} [TestMethod] public void TestRecursiveFunctionCaptureOutput() { - PowerShell ps = PowerShell.Create(); - ps.RunspacePool = m_runspacePool; - ps.AddScript(@" + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript(@" function foo($x) {return 2 * $x} function bar($x) {return 3 * (foo $x)} ", false); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("bar $_")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress"); - - var input = new PSDataCollection { 1, 2, 3, 4, 5 }; - input.Complete(); - var output = ps.Invoke(input); - var sum = output.Aggregate(0, (a, b) => a + b); - Assert.AreEqual(90, sum); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", ScriptBlock.Create("bar $_")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("NoProgress"); + + var input = new PSDataCollection {1, 2, 3, 4, 5}; + input.Complete(); + var output = ps.Invoke(input); + var sum = output.Aggregate(0, (a, b) => a + b); + Assert.AreEqual(90, sum); + } + } + + [TestMethod] + public void TestLimitingVariables() + { + using (PowerShell ps = PowerShell.Create()) { + ps.RunspacePool = m_runspacePool; + ps.AddScript(@" +$x = 1 +$y = 2 + ", false); + + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ImportVariable", 'x') + .AddParameter("ScriptBlock", ScriptBlock.Create("$x,$y")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("NoProgress") + .AddParameter("InputObject", 1); + + var output = ps.Invoke(); + int x = (int) output[0].BaseObject; + Assert.AreEqual(1,x); + Assert.IsNull(output[1]); + } + } + + [TestMethod] + public void TestLimitingFunctions() + { + using (PowerShell ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript(@" +function f{1} +function g{} + ", false).Invoke(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ImportFunction", 'f') + .AddParameter("ScriptBlock", ScriptBlock.Create("f;g")) + .AddParameter("ThrottleLimit", 1) + .AddParameter("NoProgress") + .AddParameter("InputObject", 1); + + var output = ps.Invoke(); + int x = (int)output[0].BaseObject; + Assert.AreEqual(1, x); + Assert.IsTrue(ps.HadErrors); + } } public void Dispose() From 913c9fbf60c78705e044dc868800cc42f454f9ac Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 25 Nov 2015 02:55:05 +0100 Subject: [PATCH 20/38] Adding argumentcompleter. Publishing v2.1 --- module/PSParallel.psd1 | Bin 3140 -> 3140 bytes scripts/Publish-ToGallery.ps1 | 2 +- src/PSParallel/ImportArgumentCompleter.cs | 75 ++++++++++++++++++++++ src/PSParallel/InvokeParallelCommand.cs | 7 ++ src/PSParallel/PSParallel.csproj | 5 ++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/PSParallel/ImportArgumentCompleter.cs diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index c827da22b4bb767a9b5f0e791b4ebde35fddee99..a5820aa3e52a9c6e63f65e50a36c8aac9ae162b1 100644 GIT binary patch delta 14 VcmX>iaYSN+7bBzLW^cxyTmUDJ1n2+& delta 14 VcmX>iaYSN+7bBy=W^cxyTmUDD1m^$% diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index 8951184..32ee565 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Fixing parameter set bug" + ReleaseNote = "Adding argumentcompleter" ProjectUri = "https://github.com/powercode/PSParallel" } diff --git a/src/PSParallel/ImportArgumentCompleter.cs b/src/PSParallel/ImportArgumentCompleter.cs new file mode 100644 index 0000000..117f73d --- /dev/null +++ b/src/PSParallel/ImportArgumentCompleter.cs @@ -0,0 +1,75 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using Microsoft.PowerShell.Commands; + +namespace PSParallel +{ + public class ImportArgumentCompleter : IArgumentCompleter + { + private static readonly CompletionResult[] EmptyCompletion = new CompletionResult[0]; + + public IEnumerable CompleteArgument(string commandName, string parameterName, string wordToComplete, + CommandAst commandAst, IDictionary fakeBoundParameters) + { + var fakeParam = fakeBoundParameters[parameterName]; + var paramList = new List(); + if (fakeParam.GetType().IsArray) + { + paramList.AddRange(from i in (object[]) fakeParam select i.ToString()); + } + else + { + paramList.Add(fakeParam.ToString()); + } + using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + switch (parameterName) + { + case "ImportModule": + { + powerShell + .AddCommand(new CmdletInfo("Get-Module", typeof(GetModuleCommand))) + .AddParameter("Name", wordToComplete + "*"); + return from mod in powerShell.Invoke() + where !paramList.Contains(mod.Name) + select new CompletionResult(mod.Name, mod.Name, CompletionResultType.ParameterValue, mod.Description.OrIfEmpty(mod.Name)); + } + case "ImportVariable": + { + powerShell + .AddCommand(new CmdletInfo("Get-Variable", typeof(GetVariableCommand))) + .AddParameter("Name", wordToComplete + "*"); + return from varInfo in powerShell.Invoke() + where !paramList.Contains(varInfo.Name) + select new CompletionResult(varInfo.Name, varInfo.Name, CompletionResultType.ParameterValue, varInfo.Description.OrIfEmpty(varInfo.Name) ); + } + case "ImportFunction": + { + powerShell + .AddCommand(new CmdletInfo("Get-Item", typeof(GetVariableCommand))) + .AddParameter("Path", $"function:{wordToComplete}*"); + + return + from fi in powerShell.Invoke>().First() + where !paramList.Contains(fi.Name) + select new CompletionResult(fi.Name, fi.Name, CompletionResultType.ParameterValue, fi.Description.OrIfEmpty(fi.Name)); + } + } + } + + return EmptyCompletion; + } + + } + + static class StringExtension + { + public static string OrIfEmpty(this string s, string other) + { + return string.IsNullOrEmpty(s) ? other : s; + } + } +} \ No newline at end of file diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 9817886..de46b4c 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -4,6 +4,7 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Threading; +using Microsoft.PowerShell.Commands; // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global @@ -44,14 +45,20 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [Parameter(ParameterSetName = "SessionStateParams")] [ValidateNotNull] + [Alias("im")] + [ArgumentCompleter(typeof(ImportArgumentCompleter))] public string[] ImportModule { get; set; } [Parameter(ParameterSetName = "SessionStateParams")] [ValidateNotNull] + [ArgumentCompleter(typeof(ImportArgumentCompleter))] + [Alias("iv")] public string[] ImportVariable{ get; set; } [Parameter(ParameterSetName = "SessionStateParams")] [ValidateNotNull] + [ArgumentCompleter(typeof(ImportArgumentCompleter))] + [Alias("if")] public string[] ImportFunction{ get; set; } [Parameter(ValueFromPipeline = true, Mandatory = true)] diff --git a/src/PSParallel/PSParallel.csproj b/src/PSParallel/PSParallel.csproj index 75319f0..be39c80 100644 --- a/src/PSParallel/PSParallel.csproj +++ b/src/PSParallel/PSParallel.csproj @@ -33,6 +33,10 @@ false + + False + ..\..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.PowerShell.Commands.Utility\v4.0_3.0.0.0__31bf3856ad364e35\Microsoft.PowerShell.Commands.Utility.dll + @@ -42,6 +46,7 @@ + From b744107c506d5e2e48f97dbad0ef49c9febecfd2 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 25 Nov 2015 03:09:40 +0100 Subject: [PATCH 21/38] Accepting null for InitialSessionState. Will cause an empty iss to be used. --- module/PSParallel.psd1 | Bin 3140 -> 3144 bytes module/en-US/PSParallel.dll-Help.xml | 165 ++++++------------------ src/PSParallel/InvokeParallelCommand.cs | 26 +++- 3 files changed, 59 insertions(+), 132 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index a5820aa3e52a9c6e63f65e50a36c8aac9ae162b1..706706dd70ce33e7228f8debf3f432627e5590e1 100644 GIT binary patch delta 16 XcmX>iaYAB)4haYSN+598(l#vfb&AxZ?m diff --git a/module/en-US/PSParallel.dll-Help.xml b/module/en-US/PSParallel.dll-Help.xml index 7fd1b32..c0d0bb2 100644 --- a/module/en-US/PSParallel.dll-Help.xml +++ b/module/en-US/PSParallel.dll-Help.xml @@ -4,7 +4,7 @@ @@ -24,9 +24,8 @@ The cmdlet uses a RunspacePool an and invokes the provied scriptblock once for each input. -To control the environment of the scriptblock, the ImportModule, ImportVariable and ImportFunction parameters can be used. - - +To control the environment of the scriptblock, the ImportModule, ImportVariable and ImportFunction parameters can be use +d. @@ -71,14 +70,14 @@ parameter or enter a value of 0, the default value, 16, is used. Int32 - + ImportModule Specify the names of the modules to import into the runspaces running the scriptblock to process. String[] - + ImportVariable Specifies the variables to import into the Runspaces running the specified ScriptBlock. @@ -86,19 +85,15 @@ parameter or enter a value of 0, the default value, 16, is used. If not specified, all variables are imported. String[] - - - + ImportFunction Specifies the functions to import into the Runspaces running the specified ScriptBlock. -It this variables isn't specified, all functions are imported. +It this variables isn't specified, all functions are imported. String[] - - InputObject @@ -108,6 +103,15 @@ at contains the objects, or type a command or expression that gets the objects.< PSObject + + NoProgress + + Will not show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. + + SwitchParameter + + + Invoke-Parallel @@ -150,17 +154,15 @@ parameter or enter a value of 0, the default value, 16, is used. Int32 - + InitialSessionState - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. InitialSessionState - - InputObject @@ -170,105 +172,15 @@ at contains the objects, or type a command or expression that gets the objects.< PSObject - - - Invoke-Parallel - - ScriptBlock - - Specifies the operation that is performed on each input object. Enter a script block that describes the operation. - - ScriptBlock - - - ThrottleLimit - - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this -parameter or enter a value of 0, the default value, 16, is used. - - Int32 - - - InitialSessionState - - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc -available to the ScriptBlock. -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then -imported. - - InitialSessionState - - - - + NoProgress - Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. - - SwitchParameter - - - InputObject - - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th -at contains the objects, or type a command or expression that gets the objects. - - PSObject - - - - Invoke-Parallel - - ScriptBlock - - Specifies the operation that is performed on each input object. Enter a script block that describes the operation. - - ScriptBlock - - - InputObject - - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th -at contains the objects, or type a command or expression that gets the objects. - - PSObject - - - ImportModule - - Specify the names of the modules to import into the runspaces running the scriptblock to process. - - String[] - - - ImportVariable - - Specifies the variables to import into the Runspaces running the specified ScriptBlock. - -If not specified, all variables are imported. - - String[] - - - - - ImportFunction - - Specifies the functions to import into the Runspaces running the specified ScriptBlock. - -It this variables isn't specified, all functions are imported. + Will not show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. - String[] + SwitchParameter - - NoProgress - - Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. - - SwitchParameter - @@ -337,7 +249,7 @@ parameter or enter a value of 0, the default value, 16, is used. - + ImportModule Specify the names of the modules to import into the runspaces running the scriptblock to process. @@ -349,7 +261,7 @@ parameter or enter a value of 0, the default value, 16, is used. - + ImportVariable Specifies the variables to import into the Runspaces running the specified ScriptBlock. @@ -361,23 +273,21 @@ If not specified, all variables are imported. String[] - - + - + ImportFunction Specifies the functions to import into the Runspaces running the specified ScriptBlock. -It this variables isn't specified, all functions are imported. +It this variables isn't specified, all functions are imported. String[] String[] - - + InputObject @@ -392,24 +302,25 @@ at contains the objects, or type a command or expression that gets the objects.< - + NoProgress - Will now show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. + Will not show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. SwitchParameter SwitchParameter - + + - + InitialSessionState - The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc available to the ScriptBlock. -By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then imported. InitialSessionState @@ -417,8 +328,7 @@ imported. InitialSessionState - - + @@ -436,6 +346,9 @@ imported. + + + diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index de46b4c..cabce6d 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -39,26 +39,27 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [ValidateRange(1,128)] public int ThrottleLimit { get; set; } = 32; - [Parameter(ParameterSetName = "InitialSessionState", Mandatory = true)] - [ValidateNotNull] + [Parameter(ParameterSetName = "InitialSessionState", Mandatory = true)] + [AllowNull] + [Alias("iss")] public InitialSessionState InitialSessionState { get; set; } [Parameter(ParameterSetName = "SessionStateParams")] [ValidateNotNull] - [Alias("im")] + [Alias("imo")] [ArgumentCompleter(typeof(ImportArgumentCompleter))] public string[] ImportModule { get; set; } [Parameter(ParameterSetName = "SessionStateParams")] [ValidateNotNull] [ArgumentCompleter(typeof(ImportArgumentCompleter))] - [Alias("iv")] + [Alias("iva")] public string[] ImportVariable{ get; set; } [Parameter(ParameterSetName = "SessionStateParams")] [ValidateNotNull] [ArgumentCompleter(typeof(ImportArgumentCompleter))] - [Alias("if")] + [Alias("ifu")] public string[] ImportFunction{ get; set; } [Parameter(ValueFromPipeline = true, Mandatory = true)] @@ -153,10 +154,23 @@ private void ValidateParameters() } } + InitialSessionState GetSessionState() + { + if (MyInvocation.BoundParameters.ContainsKey(nameof(InitialSessionState))) + { + if (InitialSessionState == null) + { + return InitialSessionState.Create(); + } + return InitialSessionState; + } + return GetSessionState(SessionState, ImportModule, ImportVariable, ImportFunction); + } + protected override void BeginProcessing() { ValidateParameters(); - var iss = InitialSessionState ?? GetSessionState(SessionState, ImportModule, ImportVariable, ImportFunction); + var iss = GetSessionState(); m_powershellPool = new PowershellPool(ThrottleLimit, iss, m_cancelationTokenSource.Token); m_powershellPool.Open(); if (!NoProgress) From 4d54c850b112bdacabeea5373f2018f7cbcd84d1 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 25 Nov 2015 10:37:11 +0100 Subject: [PATCH 22/38] Minor bugfixes --- module/PSParallel.psd1 | Bin 3144 -> 3144 bytes scripts/Install.ps1 | 49 ++++++++++------------ scripts/Publish-ToGallery.ps1 | 2 +- src/PSParallel/ImportArgumentCompleter.cs | 23 +++++----- src/PSParallel/InvokeParallelCommand.cs | 2 +- src/PSParallel/PowershellPool.cs | 2 +- src/PSParallel/Properties/AssemblyInfo.cs | 3 +- 7 files changed, 38 insertions(+), 43 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 706706dd70ce33e7228f8debf3f432627e5590e1..6d764325ac8a6bf8eb4514ebb1c87114a2d71e5d 100644 GIT binary patch delta 14 VcmX>haYAB)A0wmjW`D+?TmUFH1o{8~ delta 14 VcmX>haYAB)A0wmDW`D+?TmUFB1o;2} diff --git a/scripts/Install.ps1 b/scripts/Install.ps1 index b2b38c1..5f90041 100644 --- a/scripts/Install.ps1 +++ b/scripts/Install.ps1 @@ -18,39 +18,32 @@ if ('' -eq $InstallDirectory) $InstallDirectory = Join-Path -Path $personalModules -ChildPath PSParallel } -if (!(Test-Path $InstallDirectory)) +if(-not (Test-Path $InstallDirectory)) { - $null = mkdir $InstallDirectory + $null = mkdir $InstallDirectory } - -$moduleFileList = @( - 'PSParallel.psd1' -) -$binaryFileList = 'src\PsParallel\bin\Release\PSParallel.dll' -$localizations = @{ - 'en-us' = @( - 'PSParallel.dll-Help.xml' - 'about_PSParallel.Help.txt' - ) -} - -foreach($kv in $localizations.GetEnumerator()) -{ - $lang = $kv.Name - if(-not (Test-Path $InstallDirectory\$lang)) - { - $null = MkDir $InstallDirectory\$lang - } - foreach($v in $kv.Value){ - $locPath = Join-Path $lang $v - Copy-Item $rootDir\module\$locPath -Destination $InstallDirectory\$locPath - } +@( + 'module\PSParallel.psd1' + 'src\PsParallel\bin\Release\PSParallel.dll' +).Foreach{Copy-Item "$rootdir\$_" -Destination $InstallDirectory } + +$lang = @('en-us') + +$lang.Foreach{ + $lang = $_ + $langDir = "$InstallDirectory\$lang" + if(-not (Test-Path $langDir)) + { + $null = MkDir $langDir + } + + @( + 'PSParallel.dll-Help.xml' + 'about_PSParallel.Help.txt' + ).Foreach{Copy-Item "$rootDir\module\$lang\$_" -Destination $langDir} } -$binaryFileList | foreach { Copy-Item "$rootDir\$_" -Destination $InstallDirectory } -$moduleFileList | foreach {Copy-Item "$rootdir\module\$_" -Destination $InstallDirectory\$_ } - Get-ChildItem -Recurse -Path $InstallDirectory $cert = Get-Item Cert:\CurrentUser\My\98D6087848D1213F20149ADFE698473429A9B15D diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index 32ee565..243f1af 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -4,7 +4,7 @@ $p = @{ LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Adding argumentcompleter" + ReleaseNote = "Minor bugfixes" ProjectUri = "https://github.com/powercode/PSParallel" } diff --git a/src/PSParallel/ImportArgumentCompleter.cs b/src/PSParallel/ImportArgumentCompleter.cs index 117f73d..147d102 100644 --- a/src/PSParallel/ImportArgumentCompleter.cs +++ b/src/PSParallel/ImportArgumentCompleter.cs @@ -9,20 +9,23 @@ namespace PSParallel { public class ImportArgumentCompleter : IArgumentCompleter { - private static readonly CompletionResult[] EmptyCompletion = new CompletionResult[0]; - + private static readonly CompletionResult[] EmptyCompletion = new CompletionResult[0]; + public IEnumerable CompleteArgument(string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) { - var fakeParam = fakeBoundParameters[parameterName]; var paramList = new List(); - if (fakeParam.GetType().IsArray) - { - paramList.AddRange(from i in (object[]) fakeParam select i.ToString()); - } - else - { - paramList.Add(fakeParam.ToString()); + var fakeParam = fakeBoundParameters[parameterName]; + if(fakeParam != null) + { + if (fakeParam.GetType().IsArray) + { + paramList.AddRange(from i in (object[]) fakeParam select i.ToString()); + } + else + { + paramList.Add(fakeParam.ToString()); + } } using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) { diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index cabce6d..29197da 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -236,7 +236,7 @@ protected override void EndProcessing() protected override void StopProcessing() { m_cancelationTokenSource.Cancel(); - m_powershellPool.Stop(); + m_powershellPool?.Stop(); } private void WriteOutputs() diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 0074340..da1d4b1 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -88,7 +88,7 @@ public void Dispose() { Streams.Dispose(); m_availablePoolMembers.Dispose(); - m_runspacePool.Dispose(); + m_runspacePool?.Dispose(); } public void ReportAvailable(PowerShellPoolMember poolmember) diff --git a/src/PSParallel/Properties/AssemblyInfo.cs b/src/PSParallel/Properties/AssemblyInfo.cs index 3a8773d..4c739e7 100644 --- a/src/PSParallel/Properties/AssemblyInfo.cs +++ b/src/PSParallel/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -33,4 +32,4 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("2.1.3.0")] From dab8971488a7578e80ac69ddf5055cde7bb24154 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 20 Jan 2016 23:04:17 +0100 Subject: [PATCH 23/38] TryAdd/TryTake for better robustness and responsiveness. Hierarchical progress --- module/PSParallel.psd1 | Bin 3144 -> 3508 bytes src/PSParallel/InvokeParallelCommand.cs | 14 ++++-- src/PSParallel/PowerShellPoolMember.cs | 21 +++++++-- src/PSParallel/PowershellPool.cs | 55 +++++++++++++--------- src/PSParallel/ProgressManager.cs | 36 +++++++++++++- src/PSParallel/Properties/AssemblyInfo.cs | 4 +- 6 files changed, 97 insertions(+), 33 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 6d764325ac8a6bf8eb4514ebb1c87114a2d71e5d..3303f3bf7c9d35960c4b49c21bf230bd92477496 100644 GIT binary patch delta 362 zcmZ9I&k6xi7{$*B`IAyo%4T*pR?1RY5esBtsV)XnVwy47c>-C?3wQ*@&I@=f=li12 z?Yn=@J>T!1?z40+U%Qr7i%g<$8(XosRlk`;KN(%p`Qs1{RxE>na}OJAxdyP&U^P)e z4jHuI;=o#CLDl7bfbi|x^Ss59x=$rkj!n*7m5@CFJo2aN7h0Rn4V@mGDrU5KDl8Nr zXfci0;uCE!(SVE;om0g^hrk(82lg4=t9~5(F*-^%*wN*Z@Zm`OHG3tpqi@SQh7S3L Z6&0V+enZ<-zlY?j@N;B+>(L{7dI4^CLeKyJ delta 29 lcmdlYeL`Y`7bB}7gC2wNW`9OO7DnUEiLCx?lVx~k0049;2T%Y2 diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 29197da..b48ba3e 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -185,8 +186,10 @@ protected override void ProcessRecord() { if(NoProgress) { - m_powershellPool.AddInput(ScriptBlock, InputObject); - WriteOutputs(); + while (!m_powershellPool.TryAddInput(ScriptBlock, InputObject)) + { + WriteOutputs(); + } } else { @@ -205,8 +208,10 @@ protected override void EndProcessing() { var pr = m_progressManager.GetCurrentProgressRecord($"Starting processing of {i}", m_powershellPool.ProcessedCount); WriteProgress(pr); - m_powershellPool.AddInput(ScriptBlock, i); - WriteOutputs(); + while (!m_powershellPool.TryAddInput(ScriptBlock, i)) + { + WriteOutputs(); + } } } while(!m_powershellPool.WaitForAllPowershellCompleted(100)) @@ -241,6 +246,7 @@ protected override void StopProcessing() private void WriteOutputs() { + Debug.WriteLine("Processing output"); if (m_cancelationTokenSource.IsCancellationRequested) { return; diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 5718898..1ae9816 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -6,18 +6,25 @@ namespace PSParallel class PowerShellPoolMember : IDisposable { private readonly PowershellPool m_pool; + private readonly int m_index; private readonly PowerShellPoolStreams m_poolStreams; private PowerShell m_powerShell; public PowerShell PowerShell => m_powerShell; + public int Index => m_index ; + private readonly PSDataCollection m_input =new PSDataCollection(); private PSDataCollection m_output; + private ProgressProjector m_progressProjector; + public ProgressProjector ProgressProjector => m_progressProjector; - public PowerShellPoolMember(PowershellPool pool) + public PowerShellPoolMember(PowershellPool pool, int index) { m_pool = pool; + m_index = index; m_poolStreams = m_pool.Streams; - m_input.Complete(); + m_input.Complete(); CreatePowerShell(); + m_progressProjector = new ProgressProjector(); } private void PowerShellOnInvocationStateChanged(object sender, PSInvocationStateChangedEventArgs psInvocationStateChangedEventArgs) @@ -81,10 +88,13 @@ private void UnhookStreamEvents(PSDataStreams streams) public void BeginInvoke(ScriptBlock scriptblock, PSObject inputObject) { - string command = $"param($_,$PSItem){scriptblock}"; + m_progressProjector.Start(); + string command = $"param($_,$PSItem, $PSPArallelIndex,$PSParallelProgressId){scriptblock}"; m_powerShell.AddScript(command) .AddParameter("_", inputObject) - .AddParameter("PSItem", inputObject); + .AddParameter("PSItem", inputObject) + .AddParameter("PSParallelIndex", m_index) + .AddParameter("PSParallelProgressId", m_index+1000); m_powerShell.BeginInvoke(m_input, m_output); } @@ -115,7 +125,8 @@ private void InformationOnDataAdded(object sender, DataAddedEventArgs dataAddedE private void ProgressOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { - var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; + var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; + m_progressProjector.ReportProgress(record.PercentComplete); m_poolStreams.Progress.Add(record); } diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index da1d4b1..3ccc906 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Contracts; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -8,14 +9,14 @@ namespace PSParallel { - class PowershellPool : IDisposable + sealed class PowershellPool : IDisposable { private int m_busyCount; private int m_processedCount; private readonly CancellationToken m_cancellationToken; private readonly RunspacePool m_runspacePool; private readonly List m_poolMembers; - private readonly BlockingCollection m_availablePoolMembers = new BlockingCollection(new ConcurrentStack ()); + private readonly BlockingCollection m_availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); public int ProcessedCount => m_processedCount; @@ -26,9 +27,9 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can m_processedCount = 0; m_cancellationToken = cancellationToken; - for (int i = 0; i < poolSize; i++) + for (var i = 0; i < poolSize; i++) { - var powerShellPoolMember = new PowerShellPoolMember(this); + var powerShellPoolMember = new PowerShellPoolMember(this, i+1); m_poolMembers.Add(powerShellPoolMember); m_availablePoolMembers.Add(powerShellPoolMember); } @@ -37,16 +38,17 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can m_runspacePool.SetMaxRunspaces(poolSize); } - public void AddInput(ScriptBlock scriptblock,PSObject inputObject) - { - try { - var powerShell = WaitForAvailablePowershell(); - Interlocked.Increment(ref m_busyCount); - powerShell.BeginInvoke(scriptblock, inputObject); - } - catch(OperationCanceledException) + public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) + { + PowerShellPoolMember poolMember; + if(!TryWaitForAvailablePowershell(100, out poolMember)) { + return false; } + + Interlocked.Increment(ref m_busyCount); + poolMember.BeginInvoke(scriptblock, inputObject); + return true; } public void Open() @@ -76,11 +78,19 @@ public bool WaitForAllPowershellCompleted(int timeoutMilliseconds) return false; } - private PowerShellPoolMember WaitForAvailablePowershell() - { - var poolmember = m_availablePoolMembers.Take(m_cancellationToken); - poolmember.PowerShell.RunspacePool = m_runspacePool; - return poolmember; + private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolMember poolMember) + { + if(!m_availablePoolMembers.TryTake(out poolMember, milliseconds, m_cancellationToken)) + { + m_cancellationToken.ThrowIfCancellationRequested(); + Debug.WriteLine($"WaitForAvailablePowershell - TryTake failed"); + poolMember = null; + return false; + } + + poolMember.PowerShell.RunspacePool = m_runspacePool; + Debug.WriteLine($"WaitForAvailablePowershell - Busy: {m_busyCount} _processed {m_processedCount}, member = {poolMember.Index}"); + return true; } @@ -95,10 +105,13 @@ public void ReportAvailable(PowerShellPoolMember poolmember) { Interlocked.Decrement(ref m_busyCount); Interlocked.Increment(ref m_processedCount); - if(!m_cancellationToken.IsCancellationRequested) - { - m_availablePoolMembers.Add(poolmember); - } + while (!m_availablePoolMembers.TryAdd(poolmember, 1000, m_cancellationToken)) + { + m_cancellationToken.ThrowIfCancellationRequested(); + Debug.WriteLine($"WaitForAvailablePowershell - TryAdd failed"); + } + Debug.WriteLine($"ReportAvailable - Busy: {m_busyCount} _processed {m_processedCount}, member = {poolmember.Index}"); + } public void ReportStopped(PowerShellPoolMember powerShellPoolMember) diff --git a/src/PSParallel/ProgressManager.cs b/src/PSParallel/ProgressManager.cs index c3b1d00..ebb380b 100644 --- a/src/PSParallel/ProgressManager.cs +++ b/src/PSParallel/ProgressManager.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.Management.Automation; namespace PSParallel @@ -53,4 +54,37 @@ public ProgressRecord Completed() private int GetPercentComplete(int count) => count*100/TotalCount; public int ActivityId => m_progressRecord.ActivityId; } + + + class ProgressProjector + { + private readonly Stopwatch m_stopWatch; + private int m_percentComplete; + public ProgressProjector() + { + m_stopWatch = new Stopwatch(); + m_percentComplete = -1; + } + + public void ReportProgress(int percentComplete) + { + m_percentComplete = percentComplete; + } + + public bool IsValid => m_percentComplete > 0 && m_stopWatch.IsRunning; + public TimeSpan Elapsed => m_stopWatch.Elapsed; + + public TimeSpan ProjectedTotalTime => new TimeSpan(Elapsed.Ticks * 100 / m_percentComplete); + + public void Start() + { + m_stopWatch.Start(); + m_percentComplete = 0; + } + + public void Stop() + { + m_stopWatch.Stop(); + } + } } diff --git a/src/PSParallel/Properties/AssemblyInfo.cs b/src/PSParallel/Properties/AssemblyInfo.cs index 4c739e7..79cea25 100644 --- a/src/PSParallel/Properties/AssemblyInfo.cs +++ b/src/PSParallel/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PSParallell")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,4 +32,4 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("2.1.3.0")] +[assembly: AssemblyFileVersion("2.2.0.0")] From e433a6cbc36fd5e5dcafd6b190487b8de0dc279e Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 21 Jan 2016 01:01:15 +0100 Subject: [PATCH 24/38] Fixing issues with reuse of runspaces Grouped progress Variables PSParallelIndex and PSParallelProgressId available in the parallel runspaces Simplified interface by removing parameters to import things from invoking runspace. Use InitialSessionState instead. --- module/PSParallel.psd1 | Bin 3508 -> 3742 bytes module/en-US/PSParallel.dll-Help.xml | 130 +--------------------- scripts/Install.ps1 | 19 +++- scripts/Publish-ToGallery.ps1 | 5 - scripts/dbg.ps1 | 63 +++++++++++ src/PSParallel/ImportArgumentCompleter.cs | 78 ------------- src/PSParallel/InvokeParallelCommand.cs | 77 +++++-------- src/PSParallel/PSParallel.csproj | 1 - src/PSParallel/PowerShellPoolMember.cs | 12 +- src/PSParallel/PowershellPool.cs | 23 +++- src/PSParallel/ProgressManager.cs | 40 ++++--- 11 files changed, 162 insertions(+), 286 deletions(-) create mode 100644 scripts/dbg.ps1 delete mode 100644 src/PSParallel/ImportArgumentCompleter.cs diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 3303f3bf7c9d35960c4b49c21bf230bd92477496..e98ffd69c3940cea964382e0b47f0c619309d23f 100644 GIT binary patch delta 278 zcmX|+K?=e!5JkUW5yYNAh3?#m3lHGdb#ZUiR)bAjl2~xnLv-WHjTaE~fc{BU!X%SF zKW`@Q@qPN-2EFAg2_A!XG3qxl6f`R2JTpd$10tRU9CJf0!Jb)!9j@FBbBz+U_Z+EI zbQp$arF&zkI(md7?2>iowbS>NI5MhbLqpfDrtSQP8eBMn^>ArFr)$G|iV#DrSyMzR iw#^Y!wOopXf;UH12I;ug=Lx?hjNcN<3@fyg@b&>tk~KsC delta 24 ecmbOyyG43K0xP5G=0w)T?5qk5Ksfm`?`i;Ch6g+V diff --git a/module/en-US/PSParallel.dll-Help.xml b/module/en-US/PSParallel.dll-Help.xml index c0d0bb2..648ef9c 100644 --- a/module/en-US/PSParallel.dll-Help.xml +++ b/module/en-US/PSParallel.dll-Help.xml @@ -66,90 +66,6 @@ ogress is being reported. ThrottleLimit Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this -parameter or enter a value of 0, the default value, 16, is used. - - Int32 - - - ImportModule - - Specify the names of the modules to import into the runspaces running the scriptblock to process. - - String[] - - - ImportVariable - - Specifies the variables to import into the Runspaces running the specified ScriptBlock. - -If not specified, all variables are imported. - - String[] - - - ImportFunction - - Specifies the functions to import into the Runspaces running the specified ScriptBlock. - -It this variables isn't specified, all functions are imported. - - String[] - - - InputObject - - Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th -at contains the objects, or type a command or expression that gets the objects. - - PSObject - - - NoProgress - - Will not show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. - - SwitchParameter - - - - - - Invoke-Parallel - - ScriptBlock - - Specifies the operation that is performed on each input object. Enter a script block that describes the operation. - - ScriptBlock - - - ParentProgressId - - Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. - - Int32 - - - ProgressId - - Specifies an ID that distinguishes each progress bar from the others. Use this parameter when you are creating more than - one progress bar in a single command. If the progress bars do not have different IDs, they are superimposed instead of -being displayed in a series. - - Int32 - - - ProgressActivity - - Specifies the first line of progress text in the heading above the status bar. This text describes the activity whose pr -ogress is being reported. - - String - - - ThrottleLimit - - Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 16, is used. Int32 @@ -178,8 +94,7 @@ at contains the objects, or type a command or expression that gets the objects.< Will not show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. SwitchParameter - - + @@ -249,46 +164,6 @@ parameter or enter a value of 0, the default value, 16, is used. - - ImportModule - - Specify the names of the modules to import into the runspaces running the scriptblock to process. - - String[] - - String[] - - - - - - ImportVariable - - Specifies the variables to import into the Runspaces running the specified ScriptBlock. - -If not specified, all variables are imported. - - String[] - - String[] - - - - - - ImportFunction - - Specifies the functions to import into the Runspaces running the specified ScriptBlock. - -It this variables isn't specified, all functions are imported. - - String[] - - String[] - - - - InputObject @@ -312,8 +187,7 @@ at contains the objects, or type a command or expression that gets the objects.< SwitchParameter - - + InitialSessionState diff --git a/scripts/Install.ps1 b/scripts/Install.ps1 index 5f90041..3e9bce4 100644 --- a/scripts/Install.ps1 +++ b/scripts/Install.ps1 @@ -1,6 +1,14 @@ -param([string]$InstallDirectory) +$manPath = Get-ChildItem -recurse $PSScriptRoot/../module -include *.psd1 | select -first 1 +$man = Test-ModuleManifest $manPath -$rootDir = Split-Path (Split-Path $MyInvocation.MyCommand.Path) +$name = $man.Name +[string]$version = $man.Version +$moduleSourceDir = "$PSScriptRoot/$name" +$moduleDir = "~/documents/WindowsPowerShell/Modules/$name/$version/" + +[string]$rootDir = Resolve-Path $PSSCriptRoot/.. + +$InstallDirectory = $moduleDir if ('' -eq $InstallDirectory) { @@ -46,5 +54,8 @@ $lang.Foreach{ Get-ChildItem -Recurse -Path $InstallDirectory -$cert = Get-Item Cert:\CurrentUser\My\98D6087848D1213F20149ADFE698473429A9B15D -Get-ChildItem -File $InstallDirectory -Include *.dll,*.psd1 | Set-AuthenticodeSignature -Certificate $cert +$cert =Get-ChildItem cert:\CurrentUser\My -CodeSigningCert +if($cert) +{ + Get-ChildItem -File $InstallDirectory -Include *.dll,*.psd1 -Recurse | Set-AuthenticodeSignature -Certificate $cert -TimestampServer http://timestamp.verisign.com/scripts/timstamp.dll +} diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index 243f1af..09616bd 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -1,11 +1,6 @@ $p = @{ Name = "PSParallel" NuGetApiKey = $NuGetApiKey - LicenseUri = "https://github.com/powercode/PSParallel/blob/master/LICENSE" - IconUri = "https://github.com/powercode/PSParallel/blob/master/images/PSParallel_icon.png" - Tag = "Parallel","Runspace","Invoke","Foreach" - ReleaseNote = "Minor bugfixes" - ProjectUri = "https://github.com/powercode/PSParallel" } Publish-Module @p diff --git a/scripts/dbg.ps1 b/scripts/dbg.ps1 new file mode 100644 index 0000000..4699be4 --- /dev/null +++ b/scripts/dbg.ps1 @@ -0,0 +1,63 @@ +param( + [int] $ThrottleLimit = 3, + [int] $Milliseconds = 500 +) + +function new-philosopher { + param($name, [string[]] $treats) + [PSCustomObject] @{ + Name = $name + Treats = $treats + } +} + +function new-philosopher { + param($name, [string[]] $treats) + [PSCustomObject] @{ + Name = $name + Treats = $treats + } +} + +$philosopherData = @( + new-philosopher 'Immanuel Kant' 'was a real pissant','who where very rarely stable' + new-philosopher 'Heidegger' 'was a boozy beggar', 'Who could think you under the table' + new-philosopher 'David Hume' 'could out-consume Schopenhauer and Hegel' + new-philosopher 'Wittgenstein' 'was a beery swine', 'Who was just as sloshed as Schlegel' + new-philosopher 'John Stuart Mill' 'of his own free will', 'On half a pint of shandy was particularly ill' + new-philosopher 'Nietzsche' 'There''s nothing Nietzsche couldn''t teach ya', 'Bout the raising of the wrist.' + new-philosopher 'Plato' 'they say, could stick it away', 'Half a crate of whiskey every day' + new-philosopher 'Aristotle' 'was a bugger for the bottle' + new-philosopher 'Hobbes' 'was fond of his dram' + new-philosopher 'Rene Descartes' 'was a drunken fart:', 'I drink, therefore I am' + new-philosopher 'Socrates' 'is particularly missed','A lovely little thinker but a bugger when he''s pissed!' + ) + + +1..100 | invoke-parallel -Throttle $ThrottleLimit -ProgressActivity "Parallel Philosofers" { + + + + $pd = $philosopherData[($_ -1)% $philosopherData.Count] + + 1..100 | foreach { + $op = switch($_ % 8) + { + 0 { 'sleeping' } + 1 { 'drinking' } + 2 { 'drinking' } + 3 { 'thinking' } + 4 { 'drinking' } + 5 { 'drinking' } + 6 { 'eating' } + 7 { 'drinking' } + } + + $status = $pd.Treats[$_ % $pd.Treats.Length] + + $name = $pd.Name + $currentOperation = "$name is currently $op" + Write-Progress -id $PSParallelProgressId -percent $_ -activity $pd.Name -Status $status -CurrentOperation $currentOperation + Start-Sleep -milliseconds ($Milliseconds + 100 * (Get-Random -Minimum 3 -Maximum 7)) + } +} \ No newline at end of file diff --git a/src/PSParallel/ImportArgumentCompleter.cs b/src/PSParallel/ImportArgumentCompleter.cs deleted file mode 100644 index 147d102..0000000 --- a/src/PSParallel/ImportArgumentCompleter.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Language; -using Microsoft.PowerShell.Commands; - -namespace PSParallel -{ - public class ImportArgumentCompleter : IArgumentCompleter - { - private static readonly CompletionResult[] EmptyCompletion = new CompletionResult[0]; - - public IEnumerable CompleteArgument(string commandName, string parameterName, string wordToComplete, - CommandAst commandAst, IDictionary fakeBoundParameters) - { - var paramList = new List(); - var fakeParam = fakeBoundParameters[parameterName]; - if(fakeParam != null) - { - if (fakeParam.GetType().IsArray) - { - paramList.AddRange(from i in (object[]) fakeParam select i.ToString()); - } - else - { - paramList.Add(fakeParam.ToString()); - } - } - using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - switch (parameterName) - { - case "ImportModule": - { - powerShell - .AddCommand(new CmdletInfo("Get-Module", typeof(GetModuleCommand))) - .AddParameter("Name", wordToComplete + "*"); - return from mod in powerShell.Invoke() - where !paramList.Contains(mod.Name) - select new CompletionResult(mod.Name, mod.Name, CompletionResultType.ParameterValue, mod.Description.OrIfEmpty(mod.Name)); - } - case "ImportVariable": - { - powerShell - .AddCommand(new CmdletInfo("Get-Variable", typeof(GetVariableCommand))) - .AddParameter("Name", wordToComplete + "*"); - return from varInfo in powerShell.Invoke() - where !paramList.Contains(varInfo.Name) - select new CompletionResult(varInfo.Name, varInfo.Name, CompletionResultType.ParameterValue, varInfo.Description.OrIfEmpty(varInfo.Name) ); - } - case "ImportFunction": - { - powerShell - .AddCommand(new CmdletInfo("Get-Item", typeof(GetVariableCommand))) - .AddParameter("Path", $"function:{wordToComplete}*"); - - return - from fi in powerShell.Invoke>().First() - where !paramList.Contains(fi.Name) - select new CompletionResult(fi.Name, fi.Name, CompletionResultType.ParameterValue, fi.Description.OrIfEmpty(fi.Name)); - } - } - } - - return EmptyCompletion; - } - - } - - static class StringExtension - { - public static string OrIfEmpty(this string s, string other) - { - return string.IsNullOrEmpty(s) ? other : s; - } - } -} \ No newline at end of file diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index b48ba3e..3ff4b2a 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -5,7 +5,6 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Threading; -using Microsoft.PowerShell.Commands; // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global @@ -44,25 +43,7 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [AllowNull] [Alias("iss")] public InitialSessionState InitialSessionState { get; set; } - - [Parameter(ParameterSetName = "SessionStateParams")] - [ValidateNotNull] - [Alias("imo")] - [ArgumentCompleter(typeof(ImportArgumentCompleter))] - public string[] ImportModule { get; set; } - - [Parameter(ParameterSetName = "SessionStateParams")] - [ValidateNotNull] - [ArgumentCompleter(typeof(ImportArgumentCompleter))] - [Alias("iva")] - public string[] ImportVariable{ get; set; } - - [Parameter(ParameterSetName = "SessionStateParams")] - [ValidateNotNull] - [ArgumentCompleter(typeof(ImportArgumentCompleter))] - [Alias("ifu")] - public string[] ImportFunction{ get; set; } - + [Parameter(ValueFromPipeline = true, Mandatory = true)] public PSObject InputObject { get; set; } @@ -77,25 +58,21 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable // Input is then captured in ProcessRecored and processed in EndProcessing private List m_input; - private static InitialSessionState GetSessionState(SessionState sessionState, string[] modulesToImport, string[] variablesToImport, string[] functionsToImport) + private static InitialSessionState GetSessionState(SessionState sessionState) { var initialSessionState = InitialSessionState.CreateDefault2(); - CaptureVariables(sessionState, initialSessionState, variablesToImport); - CaptureFunctions(sessionState, initialSessionState, functionsToImport); - if (modulesToImport != null) - { - initialSessionState.ImportPSModule(modulesToImport); - } + CaptureVariables(sessionState, initialSessionState); + CaptureFunctions(sessionState, initialSessionState); return initialSessionState; } - private static IEnumerable GetFunctions(SessionState sessionState, string[] functionsToImport) + private static IEnumerable GetFunctions(SessionState sessionState) { try { var functionDrive = sessionState.InvokeProvider.Item.Get("function:"); - var baseObject = (Dictionary.ValueCollection) functionDrive[0].BaseObject; - return functionsToImport == null ? baseObject : baseObject.Where(f=>functionsToImport.Contains(f.Name, StringComparer.OrdinalIgnoreCase)); + return (Dictionary.ValueCollection) functionDrive[0].BaseObject; + } catch (DriveNotFoundException) { @@ -103,15 +80,14 @@ private static IEnumerable GetFunctions(SessionState sessionState, } } - private static IEnumerable GetVariables(SessionState sessionState, string[] variablesToImport) + private static IEnumerable GetVariables(SessionState sessionState) { try { string[] noTouchVariables = {"null", "true", "false", "Error"}; var variables = sessionState.InvokeProvider.Item.Get("Variable:"); var psVariables = (IEnumerable) variables[0].BaseObject; - return psVariables.Where(p=>!noTouchVariables.Contains(p.Name) && - (variablesToImport == null || variablesToImport.Contains(p.Name, StringComparer.OrdinalIgnoreCase))); + return psVariables.Where(p=>!noTouchVariables.Contains(p.Name)); } catch (DriveNotFoundException) { @@ -119,17 +95,17 @@ private static IEnumerable GetVariables(SessionState sessionState, s } } - private static void CaptureFunctions(SessionState sessionState, InitialSessionState initialSessionState, string[] functionsToImport) + private static void CaptureFunctions(SessionState sessionState, InitialSessionState initialSessionState) { - var functions = GetFunctions(sessionState, functionsToImport); + var functions = GetFunctions(sessionState); foreach (var func in functions) { initialSessionState.Commands.Add(new SessionStateFunctionEntry(func.Name, func.Definition)); } } - private static void CaptureVariables(SessionState sessionState, InitialSessionState initialSessionState, string[] variablesToImport) + private static void CaptureVariables(SessionState sessionState, InitialSessionState initialSessionState) { - var variables = GetVariables(sessionState, variablesToImport); + var variables = GetVariables(sessionState); foreach(var variable in variables) { var existing = initialSessionState.Variables[variable.Name].FirstOrDefault(); @@ -165,7 +141,7 @@ InitialSessionState GetSessionState() } return InitialSessionState; } - return GetSessionState(SessionState, ImportModule, ImportVariable, ImportFunction); + return GetSessionState(SessionState); } protected override void BeginProcessing() @@ -206,8 +182,9 @@ protected override void EndProcessing() m_progressManager.TotalCount = m_input.Count; foreach (var i in m_input) { - var pr = m_progressManager.GetCurrentProgressRecord($"Starting processing of {i}", m_powershellPool.ProcessedCount); - WriteProgress(pr); + var processed = m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount(); + m_progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", processed); + WriteProgress(m_progressManager.ProgressRecord); while (!m_powershellPool.TryAddInput(ScriptBlock, i)) { WriteOutputs(); @@ -218,8 +195,8 @@ protected override void EndProcessing() { if(!NoProgress) { - var pr = m_progressManager.GetCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", m_powershellPool.ProcessedCount); - WriteProgress(pr); + m_progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", m_powershellPool.ProcessedCount); + WriteProgress(m_progressManager.ProgressRecord); } if (Stopping) { @@ -277,13 +254,19 @@ private void WriteOutputs() { WriteVerbose(v.Message); } - foreach (var p in streams.Progress.ReadAll()) + var progressCount = streams.Progress.Count; + if (progressCount > 0) { - if(!NoProgress) + foreach (var p in streams.Progress.ReadAll()) { - p.ParentActivityId = m_progressManager.ActivityId; - } - WriteProgress(p); + if(!NoProgress) + { + p.ParentActivityId = m_progressManager.ActivityId; + } + WriteProgress(p); + } + m_progressManager.UpdateCurrentProgressRecord(m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount()); + WriteProgress(m_progressManager.ProgressRecord); } } diff --git a/src/PSParallel/PSParallel.csproj b/src/PSParallel/PSParallel.csproj index be39c80..5a72cfb 100644 --- a/src/PSParallel/PSParallel.csproj +++ b/src/PSParallel/PSParallel.csproj @@ -46,7 +46,6 @@ - diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 1ae9816..45e0726 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -14,8 +14,9 @@ class PowerShellPoolMember : IDisposable private readonly PSDataCollection m_input =new PSDataCollection(); private PSDataCollection m_output; - private ProgressProjector m_progressProjector; - public ProgressProjector ProgressProjector => m_progressProjector; + private int m_percentComplete; + public int PercentComplete => m_percentComplete; + public PowerShellPoolMember(PowershellPool pool, int index) { @@ -23,8 +24,7 @@ public PowerShellPoolMember(PowershellPool pool, int index) m_index = index; m_poolStreams = m_pool.Streams; m_input.Complete(); - CreatePowerShell(); - m_progressProjector = new ProgressProjector(); + CreatePowerShell(); } private void PowerShellOnInvocationStateChanged(object sender, PSInvocationStateChangedEventArgs psInvocationStateChangedEventArgs) @@ -88,7 +88,7 @@ private void UnhookStreamEvents(PSDataStreams streams) public void BeginInvoke(ScriptBlock scriptblock, PSObject inputObject) { - m_progressProjector.Start(); + m_percentComplete = 0; string command = $"param($_,$PSItem, $PSPArallelIndex,$PSParallelProgressId){scriptblock}"; m_powerShell.AddScript(command) .AddParameter("_", inputObject) @@ -126,7 +126,7 @@ private void InformationOnDataAdded(object sender, DataAddedEventArgs dataAddedE private void ProgressOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_progressProjector.ReportProgress(record.PercentComplete); + m_percentComplete = record.PercentComplete; m_poolStreams.Progress.Add(record); } diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 3ccc906..8a34cb3 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Contracts; +using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Threading; @@ -19,7 +20,7 @@ sealed class PowershellPool : IDisposable private readonly BlockingCollection m_availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); - public int ProcessedCount => m_processedCount; + public int ProcessedCount => m_processedCount; public PowershellPool(int poolSize, InitialSessionState initialSessionState, CancellationToken cancellationToken) { @@ -38,6 +39,26 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can m_runspacePool.SetMaxRunspaces(poolSize); } + public int GetPartiallyProcessedCount() + { + var totalPercentComplete = 0; + var count = m_poolMembers.Count; + for (int i = 0; i < count; ++i) + { + var percentComplete = m_poolMembers[i].PercentComplete; + if (percentComplete < 0) + { + percentComplete = 0; + } + else if(percentComplete > 100) + { + percentComplete = 100; + } + totalPercentComplete += percentComplete; + } + return totalPercentComplete / 100; + } + public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) { PowerShellPoolMember poolMember; diff --git a/src/PSParallel/ProgressManager.cs b/src/PSParallel/ProgressManager.cs index ebb380b..ee7ca32 100644 --- a/src/PSParallel/ProgressManager.cs +++ b/src/PSParallel/ProgressManager.cs @@ -7,8 +7,8 @@ namespace PSParallel class ProgressManager { public int TotalCount { get; set; } - private readonly ProgressRecord m_progressRecord; - private readonly Stopwatch m_stopwatch; + private ProgressRecord m_progressRecord; + private readonly Stopwatch m_stopwatch; public ProgressManager(int activityId, string activity, string statusDescription, int parentActivityId = -1, int totalCount = 0) { @@ -17,30 +17,34 @@ public ProgressManager(int activityId, string activity, string statusDescription m_progressRecord = new ProgressRecord(activityId, activity, statusDescription) {ParentActivityId = parentActivityId}; } - public ProgressRecord GetCurrentProgressRecord(string currentOperation, int count) + + public void UpdateCurrentProgressRecord(int count) { - if(!m_stopwatch.IsRunning && TotalCount > 0) + if (!m_stopwatch.IsRunning && TotalCount > 0) { m_stopwatch.Start(); - } - m_progressRecord.RecordType = ProgressRecordType.Processing; - if(TotalCount > 0) + } + m_progressRecord.RecordType = ProgressRecordType.Processing; + if (TotalCount > 0) { var percentComplete = GetPercentComplete(count); if (percentComplete != m_progressRecord.PercentComplete) { m_progressRecord.PercentComplete = percentComplete; m_progressRecord.SecondsRemaining = GetSecondsRemaining(count); - } - m_progressRecord.CurrentOperation = $"({count}/{TotalCount}) {currentOperation}"; - } - else - { - m_progressRecord.CurrentOperation = currentOperation; - } - return m_progressRecord; + } + } + } + + public void UpdateCurrentProgressRecord(string currentOperation, int count) + { + UpdateCurrentProgressRecord(count); + + m_progressRecord.CurrentOperation = TotalCount > 0 ? $"({count}/{TotalCount}) {currentOperation}" : currentOperation; } + public ProgressRecord ProgressRecord => m_progressRecord; + public ProgressRecord Completed() { @@ -67,7 +71,11 @@ public ProgressProjector() } public void ReportProgress(int percentComplete) - { + { + if (percentComplete > 100) + { + percentComplete = 100; + } m_percentComplete = percentComplete; } From 15421227f69b98010c8f1b5d0c6978a642aca9ec Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 21 Jan 2016 09:11:29 +0100 Subject: [PATCH 25/38] Fixing nullreference issue with -NoProgress --- module/PSParallel.psd1 | Bin 3742 -> 3888 bytes module/en-US/PSParallel.dll-Help.xml | 56 +++++++++++++++++++++--- scripts/Publish-ToGallery.ps1 | 9 +++- src/PSParallel/InvokeParallelCommand.cs | 31 +++++++------ 4 files changed, 73 insertions(+), 23 deletions(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index e98ffd69c3940cea964382e0b47f0c619309d23f..0d9e41339b60c68610d6d4e959729355838e42ba 100644 GIT binary patch delta 137 zcmbOyyFqS)A0wmTW`D-%T%1M>dJG_JIQb#BY`OwNIYTBx2}1^h0)sAtA45Jv07DUw zOb3#w48=gG0A-ajHz7=V@L*)ybN4mdq8#>Y<|eS Gmk9vMmK&M? delta 25 hcmdlWH&1qhALHZ{R)NiNj2&E)zi>-zp1`w!2>^9N2-E-o diff --git a/module/en-US/PSParallel.dll-Help.xml b/module/en-US/PSParallel.dll-Help.xml index 648ef9c..931a439 100644 --- a/module/en-US/PSParallel.dll-Help.xml +++ b/module/en-US/PSParallel.dll-Help.xml @@ -88,13 +88,48 @@ at contains the objects, or type a command or expression that gets the objects.< PSObject - + + + Invoke-Parallel + + ScriptBlock + + Specifies the operation that is performed on each input object. Enter a script block that describes the operation. + + ScriptBlock + + + ThrottleLimit + + Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this +parameter or enter a value of 0, the default value, 16, is used. + + Int32 + + + InputObject + + Specifies the input objects. Invoke-Parallel runs the script block on each input object in parallel. Enter a variable th +at contains the objects, or type a command or expression that gets the objects. + + PSObject + + NoProgress Will not show progress from Invoke-Progress. Progress from the scriptblock will still be displayed. - SwitchParameter - + SwitchParameter + + + InitialSessionState + + The session state used by the runspaces when invoking ScriptBlock. This provides the functions, variables, drives, etc +available to the ScriptBlock. +By default, InitialSessionState.Create2() is used and the functions and variables from the current scope is then +imported. + + InitialSessionState @@ -204,6 +239,14 @@ imported. + + Parameter8 + + + + + + @@ -233,10 +276,11 @@ imported. PS C:\> - (1..255).ForEach{"192.168.0.$_"} | Invoke-Parallel {$ip = $_; $res = ping.exe -4 -w 20 $_; [PSCustomObject] @{IP=$ip;Res=$res}} -ThrottleLimit 64 + (1..255).ForEach{"192.168.0.$_"} | + Invoke-Parallel {$ip = [IPAddress]$_; $res = ping.exe -n 1 -4 -w 20 -a $_; [PSCustomObject] @{IP=$ip;Res=$res}} -ThrottleLimit 64 - This example pings all iP v4 addresses on a subnet, specifying Throttlelimit to 64, i.e. running up to 64 runspaces in p -arallel. + This example pings all iP v4 addresses on a subnet, specifying Throttlelimit to 64, i.e. running up to 64 runspaces +in parallel. diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index 09616bd..aca7d72 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -1,6 +1,13 @@ +$manPath = Get-ChildItem -recurse $PSScriptRoot/../module -include *.psd1 | select -first 1 +$man = Test-ModuleManifest $manPath + +$name = $man.Name +[string]$version = $man.Version + $p = @{ - Name = "PSParallel" + Name = $name NuGetApiKey = $NuGetApiKey + RequiredVersion = $version } Publish-Module @p diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 3ff4b2a..40b8281 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -12,26 +12,22 @@ namespace PSParallel { [Alias("ipa")] - [Cmdlet("Invoke", "Parallel", DefaultParameterSetName = "SessionStateParams")] + [Cmdlet("Invoke", "Parallel", DefaultParameterSetName = "Progress")] public sealed class InvokeParallelCommand : PSCmdlet, IDisposable { [Parameter(Mandatory = true, Position = 0)] public ScriptBlock ScriptBlock { get; set; } - [Alias("ppi")] - [Parameter(ParameterSetName = "InitialSessionState")] - [Parameter(ParameterSetName = "SessionStateParams")] - [Parameter(ParameterSetName = "")] + [Parameter(ParameterSetName = "Progress")] + [Alias("ppi")] public int ParentProgressId { get; set; } = -1; - [Alias("pi")] - [Parameter(ParameterSetName = "InitialSessionState")] - [Parameter(ParameterSetName = "SessionStateParams")] + [Parameter(ParameterSetName = "Progress")] + [Alias("pi")] public int ProgressId { get; set; } = 1000; - [Alias("pa")] - [Parameter(ParameterSetName = "InitialSessionState")] - [Parameter(ParameterSetName = "SessionStateParams")] + [Parameter(ParameterSetName = "Progress")] + [Alias("pa")] [ValidateNotNullOrEmpty] public string ProgressActivity { get; set; } = "Invoke-Parallel"; @@ -39,7 +35,7 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [ValidateRange(1,128)] public int ThrottleLimit { get; set; } = 32; - [Parameter(ParameterSetName = "InitialSessionState", Mandatory = true)] + [Parameter] [AllowNull] [Alias("iss")] public InitialSessionState InitialSessionState { get; set; } @@ -47,7 +43,7 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [Parameter(ValueFromPipeline = true, Mandatory = true)] public PSObject InputObject { get; set; } - [Parameter] + [Parameter(ParameterSetName = "NoProgress")] public SwitchParameter NoProgress { get; set; } private readonly CancellationTokenSource m_cancelationTokenSource = new CancellationTokenSource(); @@ -264,9 +260,12 @@ private void WriteOutputs() p.ParentActivityId = m_progressManager.ActivityId; } WriteProgress(p); - } - m_progressManager.UpdateCurrentProgressRecord(m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount()); - WriteProgress(m_progressManager.ProgressRecord); + } + if(!NoProgress) + { + m_progressManager.UpdateCurrentProgressRecord(m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount()); + WriteProgress(m_progressManager.ProgressRecord); + } } } From a65a5c5eac055c2d5f18a075ec4e40334d94e9b7 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Fri, 29 Jan 2016 00:42:52 +0100 Subject: [PATCH 26/38] Progress changes with each poolmember updating the pool with progress changes to keep track of partial item progress --- src/PSParallel/InvokeParallelCommand.cs | 5 ++- src/PSParallel/PowerShellPoolMember.cs | 4 ++- src/PSParallel/PowershellPool.cs | 31 ++++++------------ src/PSParallel/ProgressManager.cs | 42 ++----------------------- 4 files changed, 17 insertions(+), 65 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 40b8281..7cce2ff 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -178,8 +178,7 @@ protected override void EndProcessing() m_progressManager.TotalCount = m_input.Count; foreach (var i in m_input) { - var processed = m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount(); - m_progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", processed); + m_progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", m_powershellPool.ProcessedCount); WriteProgress(m_progressManager.ProgressRecord); while (!m_powershellPool.TryAddInput(ScriptBlock, i)) { @@ -263,7 +262,7 @@ private void WriteOutputs() } if(!NoProgress) { - m_progressManager.UpdateCurrentProgressRecord(m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount()); + m_progressManager.UpdateCurrentProgressRecord(m_powershellPool.ProcessedCount); WriteProgress(m_progressManager.ProgressRecord); } } diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 45e0726..90a65e0 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -125,9 +125,11 @@ private void InformationOnDataAdded(object sender, DataAddedEventArgs dataAddedE private void ProgressOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { - var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; + var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; + var change = record.PercentComplete - m_percentComplete; m_percentComplete = record.PercentComplete; m_poolStreams.Progress.Add(record); + m_pool.AddProgressChange(change); } private void ErrorOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 8a34cb3..397809e 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -19,8 +19,11 @@ sealed class PowershellPool : IDisposable private readonly List m_poolMembers; private readonly BlockingCollection m_availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); + private int m_totalPercentComplete; - public int ProcessedCount => m_processedCount; + public int ProcessedCount => m_processedCount + PartiallyProcessedCount; + + private int PartiallyProcessedCount => m_totalPercentComplete / 100; public PowershellPool(int poolSize, InitialSessionState initialSessionState, CancellationToken cancellationToken) { @@ -39,26 +42,6 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can m_runspacePool.SetMaxRunspaces(poolSize); } - public int GetPartiallyProcessedCount() - { - var totalPercentComplete = 0; - var count = m_poolMembers.Count; - for (int i = 0; i < count; ++i) - { - var percentComplete = m_poolMembers[i].PercentComplete; - if (percentComplete < 0) - { - percentComplete = 0; - } - else if(percentComplete > 100) - { - percentComplete = 100; - } - totalPercentComplete += percentComplete; - } - return totalPercentComplete / 100; - } - public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) { PowerShellPoolMember poolMember; @@ -126,6 +109,7 @@ public void ReportAvailable(PowerShellPoolMember poolmember) { Interlocked.Decrement(ref m_busyCount); Interlocked.Increment(ref m_processedCount); + Interlocked.Add(ref m_totalPercentComplete, poolmember.PercentComplete); while (!m_availablePoolMembers.TryAdd(poolmember, 1000, m_cancellationToken)) { m_cancellationToken.ThrowIfCancellationRequested(); @@ -149,5 +133,10 @@ public void Stop() } WaitForAllPowershellCompleted(5000); } + + public void AddProgressChange(int change) + { + Interlocked.Add(ref m_totalPercentComplete, change); + } } } \ No newline at end of file diff --git a/src/PSParallel/ProgressManager.cs b/src/PSParallel/ProgressManager.cs index ee7ca32..eb3a1ac 100644 --- a/src/PSParallel/ProgressManager.cs +++ b/src/PSParallel/ProgressManager.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Management.Automation; namespace PSParallel @@ -7,7 +6,7 @@ namespace PSParallel class ProgressManager { public int TotalCount { get; set; } - private ProgressRecord m_progressRecord; + private readonly ProgressRecord m_progressRecord; private readonly Stopwatch m_stopwatch; public ProgressManager(int activityId, string activity, string statusDescription, int parentActivityId = -1, int totalCount = 0) @@ -58,41 +57,4 @@ public ProgressRecord Completed() private int GetPercentComplete(int count) => count*100/TotalCount; public int ActivityId => m_progressRecord.ActivityId; } - - - class ProgressProjector - { - private readonly Stopwatch m_stopWatch; - private int m_percentComplete; - public ProgressProjector() - { - m_stopWatch = new Stopwatch(); - m_percentComplete = -1; - } - - public void ReportProgress(int percentComplete) - { - if (percentComplete > 100) - { - percentComplete = 100; - } - m_percentComplete = percentComplete; - } - - public bool IsValid => m_percentComplete > 0 && m_stopWatch.IsRunning; - public TimeSpan Elapsed => m_stopWatch.Elapsed; - - public TimeSpan ProjectedTotalTime => new TimeSpan(Elapsed.Ticks * 100 / m_percentComplete); - - public void Start() - { - m_stopWatch.Start(); - m_percentComplete = 0; - } - - public void Stop() - { - m_stopWatch.Stop(); - } - } } From bee3aece2cb492664df074d46bb5ea49994e1c37 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Fri, 29 Jan 2016 00:44:14 +0100 Subject: [PATCH 27/38] Skipping hungarian m for members --- src/PSParallel/InvokeParallelCommand.cs | 56 ++++++------- src/PSParallel/PowerShellPoolMember.cs | 96 +++++++++++----------- src/PSParallel/PowershellPool.cs | 72 ++++++++-------- src/PSParallel/ProgressManager.cs | 34 ++++---- src/PSParallelTests/InvokeParallelTests.cs | 46 +++++------ 5 files changed, 152 insertions(+), 152 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 7cce2ff..f097a9c 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -46,13 +46,13 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable [Parameter(ParameterSetName = "NoProgress")] public SwitchParameter NoProgress { get; set; } - private readonly CancellationTokenSource m_cancelationTokenSource = new CancellationTokenSource(); - private PowershellPool m_powershellPool; - private ProgressManager m_progressManager; + private readonly CancellationTokenSource _cancelationTokenSource = new CancellationTokenSource(); + private PowershellPool _powershellPool; + private ProgressManager _progressManager; // this is only used when NoProgress is not specified // Input is then captured in ProcessRecored and processed in EndProcessing - private List m_input; + private List _input; private static InitialSessionState GetSessionState(SessionState sessionState) { @@ -144,12 +144,12 @@ protected override void BeginProcessing() { ValidateParameters(); var iss = GetSessionState(); - m_powershellPool = new PowershellPool(ThrottleLimit, iss, m_cancelationTokenSource.Token); - m_powershellPool.Open(); + _powershellPool = new PowershellPool(ThrottleLimit, iss, _cancelationTokenSource.Token); + _powershellPool.Open(); if (!NoProgress) { - m_progressManager = new ProgressManager(ProgressId, ProgressActivity, $"Processing with {ThrottleLimit} workers", ParentProgressId); - m_input = new List(500); + _progressManager = new ProgressManager(ProgressId, ProgressActivity, $"Processing with {ThrottleLimit} workers", ParentProgressId); + _input = new List(500); } } @@ -158,14 +158,14 @@ protected override void ProcessRecord() { if(NoProgress) { - while (!m_powershellPool.TryAddInput(ScriptBlock, InputObject)) + while (!_powershellPool.TryAddInput(ScriptBlock, InputObject)) { WriteOutputs(); } } else { - m_input.Add(InputObject); + _input.Add(InputObject); } } @@ -175,23 +175,23 @@ protected override void EndProcessing() { if (!NoProgress) { - m_progressManager.TotalCount = m_input.Count; - foreach (var i in m_input) + _progressManager.TotalCount = _input.Count; + foreach (var i in _input) { - m_progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", m_powershellPool.ProcessedCount); - WriteProgress(m_progressManager.ProgressRecord); - while (!m_powershellPool.TryAddInput(ScriptBlock, i)) + _progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", _powershellPool.ProcessedCount); + WriteProgress(_progressManager.ProgressRecord); + while (!_powershellPool.TryAddInput(ScriptBlock, i)) { WriteOutputs(); } } } - while(!m_powershellPool.WaitForAllPowershellCompleted(100)) + while(!_powershellPool.WaitForAllPowershellCompleted(100)) { if(!NoProgress) { - m_progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", m_powershellPool.ProcessedCount); - WriteProgress(m_progressManager.ProgressRecord); + _progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", _powershellPool.ProcessedCount); + WriteProgress(_progressManager.ProgressRecord); } if (Stopping) { @@ -205,25 +205,25 @@ protected override void EndProcessing() { if(!NoProgress) { - WriteProgress(m_progressManager.Completed()); + WriteProgress(_progressManager.Completed()); } } } protected override void StopProcessing() { - m_cancelationTokenSource.Cancel(); - m_powershellPool?.Stop(); + _cancelationTokenSource.Cancel(); + _powershellPool?.Stop(); } private void WriteOutputs() { Debug.WriteLine("Processing output"); - if (m_cancelationTokenSource.IsCancellationRequested) + if (_cancelationTokenSource.IsCancellationRequested) { return; } - var streams = m_powershellPool.Streams; + var streams = _powershellPool.Streams; foreach (var o in streams.Output.ReadAll()) { WriteObject(o, false); @@ -256,22 +256,22 @@ private void WriteOutputs() { if(!NoProgress) { - p.ParentActivityId = m_progressManager.ActivityId; + p.ParentActivityId = _progressManager.ActivityId; } WriteProgress(p); } if(!NoProgress) { - m_progressManager.UpdateCurrentProgressRecord(m_powershellPool.ProcessedCount); - WriteProgress(m_progressManager.ProgressRecord); + _progressManager.UpdateCurrentProgressRecord(_powershellPool.ProcessedCount); + WriteProgress(_progressManager.ProgressRecord); } } } public void Dispose() { - m_powershellPool?.Dispose(); - m_cancelationTokenSource.Dispose(); + _powershellPool?.Dispose(); + _cancelationTokenSource.Dispose(); } } diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 90a65e0..cb9752c 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -5,25 +5,25 @@ namespace PSParallel { class PowerShellPoolMember : IDisposable { - private readonly PowershellPool m_pool; - private readonly int m_index; - private readonly PowerShellPoolStreams m_poolStreams; - private PowerShell m_powerShell; - public PowerShell PowerShell => m_powerShell; - public int Index => m_index ; - - private readonly PSDataCollection m_input =new PSDataCollection(); - private PSDataCollection m_output; - private int m_percentComplete; - public int PercentComplete => m_percentComplete; + private readonly PowershellPool _pool; + private readonly int _index; + private readonly PowerShellPoolStreams _poolStreams; + private PowerShell _powerShell; + public PowerShell PowerShell => _powerShell; + public int Index => _index ; + + private readonly PSDataCollection _input =new PSDataCollection(); + private PSDataCollection _output; + private int _percentComplete; + public int PercentComplete => _percentComplete; public PowerShellPoolMember(PowershellPool pool, int index) { - m_pool = pool; - m_index = index; - m_poolStreams = m_pool.Streams; - m_input.Complete(); + _pool = pool; + _index = index; + _poolStreams = _pool.Streams; + _input.Complete(); CreatePowerShell(); } @@ -33,13 +33,13 @@ private void PowerShellOnInvocationStateChanged(object sender, PSInvocationState { case PSInvocationState.Stopped: ReleasePowerShell(); - m_pool.ReportStopped(this); + _pool.ReportStopped(this); break; case PSInvocationState.Completed: case PSInvocationState.Failed: ReleasePowerShell(); CreatePowerShell(); - m_pool.ReportAvailable(this); + _pool.ReportAvailable(this); break; } } @@ -49,18 +49,18 @@ private void CreatePowerShell() var powerShell = PowerShell.Create(); HookStreamEvents(powerShell.Streams); powerShell.InvocationStateChanged += PowerShellOnInvocationStateChanged; - m_powerShell = powerShell; - m_output = new PSDataCollection(); - m_output.DataAdded += OutputOnDataAdded; + _powerShell = powerShell; + _output = new PSDataCollection(); + _output.DataAdded += OutputOnDataAdded; } private void ReleasePowerShell() { - UnhookStreamEvents(m_powerShell.Streams); - m_powerShell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; - m_output.DataAdded -= OutputOnDataAdded; - m_powerShell.Dispose(); - m_powerShell = null; + UnhookStreamEvents(_powerShell.Streams); + _powerShell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; + _output.DataAdded -= OutputOnDataAdded; + _powerShell.Dispose(); + _powerShell = null; } @@ -88,92 +88,92 @@ private void UnhookStreamEvents(PSDataStreams streams) public void BeginInvoke(ScriptBlock scriptblock, PSObject inputObject) { - m_percentComplete = 0; + _percentComplete = 0; string command = $"param($_,$PSItem, $PSPArallelIndex,$PSParallelProgressId){scriptblock}"; - m_powerShell.AddScript(command) + _powerShell.AddScript(command) .AddParameter("_", inputObject) .AddParameter("PSItem", inputObject) - .AddParameter("PSParallelIndex", m_index) - .AddParameter("PSParallelProgressId", m_index+1000); - m_powerShell.BeginInvoke(m_input, m_output); + .AddParameter("PSParallelIndex", _index) + .AddParameter("PSParallelProgressId", _index+1000); + _powerShell.BeginInvoke(_input, _output); } public void Dispose() { - var ps = m_powerShell; + var ps = _powerShell; if (ps != null) { UnhookStreamEvents(ps.Streams); ps.Dispose(); } - m_output.Dispose(); - m_input.Dispose(); + _output.Dispose(); + _input.Dispose(); } private void OutputOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var item = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Output.Add(item); + _poolStreams.Output.Add(item); } private void InformationOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var ir = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Information.Add(ir); + _poolStreams.Information.Add(ir); } private void ProgressOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - var change = record.PercentComplete - m_percentComplete; - m_percentComplete = record.PercentComplete; - m_poolStreams.Progress.Add(record); - m_pool.AddProgressChange(change); + var change = record.PercentComplete - _percentComplete; + _percentComplete = record.PercentComplete; + _poolStreams.Progress.Add(record); + _pool.AddProgressChange(change); } private void ErrorOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Error.Add(record); + _poolStreams.Error.Add(record); } private void DebugOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Debug.Add(record); + _poolStreams.Debug.Add(record); } private void WarningOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Warning.Add(record); + _poolStreams.Warning.Add(record); } private void VerboseOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Verbose.Add(record); + _poolStreams.Verbose.Add(record); } public void Stop() { - if(m_powerShell.InvocationStateInfo.State != PSInvocationState.Stopped) + if(_powerShell.InvocationStateInfo.State != PSInvocationState.Stopped) { - UnhookStreamEvents(m_powerShell.Streams); - m_powerShell.BeginStop(OnStopped, null); + UnhookStreamEvents(_powerShell.Streams); + _powerShell.BeginStop(OnStopped, null); } } private void OnStopped(IAsyncResult ar) { - var ps = m_powerShell; + var ps = _powerShell; if (ps == null) { return; } ps.EndStop(ar); - m_powerShell = null; + _powerShell = null; } } } \ No newline at end of file diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 397809e..490302a 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -12,34 +12,34 @@ namespace PSParallel { sealed class PowershellPool : IDisposable { - private int m_busyCount; - private int m_processedCount; - private readonly CancellationToken m_cancellationToken; - private readonly RunspacePool m_runspacePool; - private readonly List m_poolMembers; - private readonly BlockingCollection m_availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); + private int _busyCount; + private int _processedCount; + private readonly CancellationToken _cancellationToken; + private readonly RunspacePool _runspacePool; + private readonly List _poolMembers; + private readonly BlockingCollection _availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); - private int m_totalPercentComplete; + private int _totalPercentComplete; - public int ProcessedCount => m_processedCount + PartiallyProcessedCount; + public int ProcessedCount => _processedCount + PartiallyProcessedCount; - private int PartiallyProcessedCount => m_totalPercentComplete / 100; + private int PartiallyProcessedCount => _totalPercentComplete / 100; public PowershellPool(int poolSize, InitialSessionState initialSessionState, CancellationToken cancellationToken) { - m_poolMembers= new List(poolSize); - m_processedCount = 0; - m_cancellationToken = cancellationToken; + _poolMembers= new List(poolSize); + _processedCount = 0; + _cancellationToken = cancellationToken; for (var i = 0; i < poolSize; i++) { var powerShellPoolMember = new PowerShellPoolMember(this, i+1); - m_poolMembers.Add(powerShellPoolMember); - m_availablePoolMembers.Add(powerShellPoolMember); + _poolMembers.Add(powerShellPoolMember); + _availablePoolMembers.Add(powerShellPoolMember); } - m_runspacePool = RunspaceFactory.CreateRunspacePool(initialSessionState); - m_runspacePool.SetMaxRunspaces(poolSize); + _runspacePool = RunspaceFactory.CreateRunspacePool(initialSessionState); + _runspacePool.SetMaxRunspaces(poolSize); } public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) @@ -50,14 +50,14 @@ public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) return false; } - Interlocked.Increment(ref m_busyCount); + Interlocked.Increment(ref _busyCount); poolMember.BeginInvoke(scriptblock, inputObject); return true; } public void Open() { - m_runspacePool.Open(); + _runspacePool.Open(); } public bool WaitForAllPowershellCompleted(int timeoutMilliseconds) @@ -68,11 +68,11 @@ public bool WaitForAllPowershellCompleted(int timeoutMilliseconds) while (currendTicks - startTicks < timeoutMilliseconds) { currendTicks = Environment.TickCount; - if (m_cancellationToken.IsCancellationRequested) + if (_cancellationToken.IsCancellationRequested) { return false; } - if (Interlocked.CompareExchange(ref m_busyCount, 0, 0) == 0) + if (Interlocked.CompareExchange(ref _busyCount, 0, 0) == 0) { return true; } @@ -84,16 +84,16 @@ public bool WaitForAllPowershellCompleted(int timeoutMilliseconds) private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolMember poolMember) { - if(!m_availablePoolMembers.TryTake(out poolMember, milliseconds, m_cancellationToken)) + if(!_availablePoolMembers.TryTake(out poolMember, milliseconds, _cancellationToken)) { - m_cancellationToken.ThrowIfCancellationRequested(); + _cancellationToken.ThrowIfCancellationRequested(); Debug.WriteLine($"WaitForAvailablePowershell - TryTake failed"); poolMember = null; return false; } - poolMember.PowerShell.RunspacePool = m_runspacePool; - Debug.WriteLine($"WaitForAvailablePowershell - Busy: {m_busyCount} _processed {m_processedCount}, member = {poolMember.Index}"); + poolMember.PowerShell.RunspacePool = _runspacePool; + Debug.WriteLine($"WaitForAvailablePowershell - Busy: {_busyCount} _processed {_processedCount}, member = {poolMember.Index}"); return true; } @@ -101,33 +101,33 @@ private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolM public void Dispose() { Streams.Dispose(); - m_availablePoolMembers.Dispose(); - m_runspacePool?.Dispose(); + _availablePoolMembers.Dispose(); + _runspacePool?.Dispose(); } public void ReportAvailable(PowerShellPoolMember poolmember) { - Interlocked.Decrement(ref m_busyCount); - Interlocked.Increment(ref m_processedCount); - Interlocked.Add(ref m_totalPercentComplete, poolmember.PercentComplete); - while (!m_availablePoolMembers.TryAdd(poolmember, 1000, m_cancellationToken)) + Interlocked.Decrement(ref _busyCount); + Interlocked.Increment(ref _processedCount); + Interlocked.Add(ref _totalPercentComplete, poolmember.PercentComplete); + while (!_availablePoolMembers.TryAdd(poolmember, 1000, _cancellationToken)) { - m_cancellationToken.ThrowIfCancellationRequested(); + _cancellationToken.ThrowIfCancellationRequested(); Debug.WriteLine($"WaitForAvailablePowershell - TryAdd failed"); } - Debug.WriteLine($"ReportAvailable - Busy: {m_busyCount} _processed {m_processedCount}, member = {poolmember.Index}"); + Debug.WriteLine($"ReportAvailable - Busy: {_busyCount} _processed {_processedCount}, member = {poolmember.Index}"); } public void ReportStopped(PowerShellPoolMember powerShellPoolMember) { - Interlocked.Decrement(ref m_busyCount); + Interlocked.Decrement(ref _busyCount); } public void Stop() { - m_availablePoolMembers.CompleteAdding(); - foreach (var poolMember in m_poolMembers) + _availablePoolMembers.CompleteAdding(); + foreach (var poolMember in _poolMembers) { poolMember.Stop(); } @@ -136,7 +136,7 @@ public void Stop() public void AddProgressChange(int change) { - Interlocked.Add(ref m_totalPercentComplete, change); + Interlocked.Add(ref _totalPercentComplete, change); } } } \ No newline at end of file diff --git a/src/PSParallel/ProgressManager.cs b/src/PSParallel/ProgressManager.cs index eb3a1ac..28a898d 100644 --- a/src/PSParallel/ProgressManager.cs +++ b/src/PSParallel/ProgressManager.cs @@ -6,31 +6,31 @@ namespace PSParallel class ProgressManager { public int TotalCount { get; set; } - private readonly ProgressRecord m_progressRecord; - private readonly Stopwatch m_stopwatch; + private readonly ProgressRecord _progressRecord; + private readonly Stopwatch _stopwatch; public ProgressManager(int activityId, string activity, string statusDescription, int parentActivityId = -1, int totalCount = 0) { TotalCount = totalCount; - m_stopwatch = new Stopwatch(); - m_progressRecord = new ProgressRecord(activityId, activity, statusDescription) {ParentActivityId = parentActivityId}; + _stopwatch = new Stopwatch(); + _progressRecord = new ProgressRecord(activityId, activity, statusDescription) {ParentActivityId = parentActivityId}; } public void UpdateCurrentProgressRecord(int count) { - if (!m_stopwatch.IsRunning && TotalCount > 0) + if (!_stopwatch.IsRunning && TotalCount > 0) { - m_stopwatch.Start(); + _stopwatch.Start(); } - m_progressRecord.RecordType = ProgressRecordType.Processing; + _progressRecord.RecordType = ProgressRecordType.Processing; if (TotalCount > 0) { var percentComplete = GetPercentComplete(count); - if (percentComplete != m_progressRecord.PercentComplete) + if (percentComplete != _progressRecord.PercentComplete) { - m_progressRecord.PercentComplete = percentComplete; - m_progressRecord.SecondsRemaining = GetSecondsRemaining(count); + _progressRecord.PercentComplete = percentComplete; + _progressRecord.SecondsRemaining = GetSecondsRemaining(count); } } } @@ -39,22 +39,22 @@ public void UpdateCurrentProgressRecord(string currentOperation, int count) { UpdateCurrentProgressRecord(count); - m_progressRecord.CurrentOperation = TotalCount > 0 ? $"({count}/{TotalCount}) {currentOperation}" : currentOperation; + _progressRecord.CurrentOperation = TotalCount > 0 ? $"({count}/{TotalCount}) {currentOperation}" : currentOperation; } - public ProgressRecord ProgressRecord => m_progressRecord; + public ProgressRecord ProgressRecord => _progressRecord; public ProgressRecord Completed() { - m_stopwatch.Reset(); + _stopwatch.Reset(); - m_progressRecord.RecordType = ProgressRecordType.Completed; - return m_progressRecord; + _progressRecord.RecordType = ProgressRecordType.Completed; + return _progressRecord; } - private int GetSecondsRemaining(int count) => count == 0 ? -1 : (int) ((TotalCount - count)*m_stopwatch.ElapsedMilliseconds/1000/count); + private int GetSecondsRemaining(int count) => count == 0 ? -1 : (int) ((TotalCount - count)*_stopwatch.ElapsedMilliseconds/1000/count); private int GetPercentComplete(int count) => count*100/TotalCount; - public int ActivityId => m_progressRecord.ActivityId; + public int ActivityId => _progressRecord.ActivityId; } } diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index b20a100..c0efca3 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -11,7 +11,7 @@ namespace PSParallelTests [TestClass] public sealed class InvokeParallelTests : IDisposable { - readonly RunspacePool m_runspacePool; + readonly RunspacePool _runspacePool; public InvokeParallelTests() { @@ -33,16 +33,16 @@ public InvokeParallelTests() { new SessionStateVariableEntry("ErrorActionPreference", ActionPreference.Continue, "Dictates the action taken when an error message is delivered"), }); - m_runspacePool = RunspaceFactory.CreateRunspacePool(iss); - m_runspacePool.SetMaxRunspaces(10); - m_runspacePool.Open(); + _runspacePool = RunspaceFactory.CreateRunspacePool(iss); + _runspacePool.SetMaxRunspaces(10); + _runspacePool.Open(); } [TestMethod] public void TestOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddCommand("Invoke-Parallel") .AddParameter("ScriptBlock", ScriptBlock.Create("$_* 2")) @@ -60,7 +60,7 @@ public void TestParallelOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddCommand("Invoke-Parallel") .AddParameter("ScriptBlock", ScriptBlock.Create("$_* 2")) @@ -78,7 +78,7 @@ public void TestVerboseOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("$VerbosePreference='Continue'", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -99,7 +99,7 @@ public void TestNoVerboseOutputWithoutPreference() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddStatement() .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Verbose $_")) @@ -118,7 +118,7 @@ public void TestDebugOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("$DebugPreference='Continue'", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -139,7 +139,7 @@ public void TestNoDebugOutputWithoutPreference() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.Commands.Clear(); ps.AddStatement() .AddCommand("Invoke-Parallel", false) @@ -159,7 +159,7 @@ public void TestWarningOutput() using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("$WarningPreference='Continue'", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -179,7 +179,7 @@ public void TestNoWarningOutputWithoutPreference() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("$WarningPreference='SilentlyContinue'", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -200,7 +200,7 @@ public void TestErrorOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("$ErrorActionPreference='Continue'", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -220,7 +220,7 @@ public void TestNoErrorOutputWithoutPreference() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("$ErrorActionPreference='SilentlyContinue'", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -240,7 +240,7 @@ public void TestBinaryExpressionVariableCapture() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("[int]$x=10", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -259,7 +259,7 @@ public void TestAssingmentExpressionVariableCapture() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript("[int]$x=10;", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() @@ -278,7 +278,7 @@ public void TestProgressOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddStatement() .AddCommand("Invoke-Parallel", false) @@ -300,7 +300,7 @@ public void TestNoProgressOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddStatement() .AddCommand("Invoke-Parallel", false) @@ -324,7 +324,7 @@ public void TestFunctionCaptureOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript(@" function foo($x) {return $x * 2} ", false); @@ -349,7 +349,7 @@ public void TestRecursiveFunctionCaptureOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript(@" function foo($x) {return 2 * $x} function bar($x) {return 3 * (foo $x)} @@ -373,7 +373,7 @@ function bar($x) {return 3 * (foo $x)} public void TestLimitingVariables() { using (PowerShell ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript(@" $x = 1 $y = 2 @@ -399,7 +399,7 @@ public void TestLimitingFunctions() { using (PowerShell ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.RunspacePool = _runspacePool; ps.AddScript(@" function f{1} function g{} @@ -421,7 +421,7 @@ function g{} public void Dispose() { - m_runspacePool.Dispose(); + _runspacePool.Dispose(); } } } From d7786dced26c77613743c66b53f7dfb5180b80aa Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 8 Feb 2016 14:33:39 +0100 Subject: [PATCH 28/38] Removing obsolete tests --- src/PSParallelTests/InvokeParallelTests.cs | 76 ---------------------- 1 file changed, 76 deletions(-) diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index c0efca3..4c54bf0 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -343,82 +343,6 @@ function foo($x) {return $x * 2} } - - [TestMethod] - public void TestRecursiveFunctionCaptureOutput() - { - using (var ps = PowerShell.Create()) - { - ps.RunspacePool = _runspacePool; - ps.AddScript(@" -function foo($x) {return 2 * $x} -function bar($x) {return 3 * (foo $x)} -", false); - - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("bar $_")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress"); - - var input = new PSDataCollection {1, 2, 3, 4, 5}; - input.Complete(); - var output = ps.Invoke(input); - var sum = output.Aggregate(0, (a, b) => a + b); - Assert.AreEqual(90, sum); - } - } - - [TestMethod] - public void TestLimitingVariables() - { - using (PowerShell ps = PowerShell.Create()) { - ps.RunspacePool = _runspacePool; - ps.AddScript(@" -$x = 1 -$y = 2 - ", false); - - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ImportVariable", 'x') - .AddParameter("ScriptBlock", ScriptBlock.Create("$x,$y")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress") - .AddParameter("InputObject", 1); - - var output = ps.Invoke(); - int x = (int) output[0].BaseObject; - Assert.AreEqual(1,x); - Assert.IsNull(output[1]); - } - } - - [TestMethod] - public void TestLimitingFunctions() - { - using (PowerShell ps = PowerShell.Create()) - { - ps.RunspacePool = _runspacePool; - ps.AddScript(@" -function f{1} -function g{} - ", false).Invoke(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ImportFunction", 'f') - .AddParameter("ScriptBlock", ScriptBlock.Create("f;g")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress") - .AddParameter("InputObject", 1); - - var output = ps.Invoke(); - int x = (int)output[0].BaseObject; - Assert.AreEqual(1, x); - Assert.IsTrue(ps.HadErrors); - } - } - public void Dispose() { _runspacePool.Dispose(); From 379a7ba3e676207e05d99da78c6c3a637b3a391a Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Sun, 17 Apr 2016 23:57:02 +0200 Subject: [PATCH 29/38] Splitting cmdlet into two workers to simplify logic --- scripts/dbg.ps1 | 11 +- src/PSParallel/InvokeParallelCommand.cs | 253 +++++++++++++-------- src/PSParallel/PowerShellPoolMember.cs | 2 +- src/PSParallel/PowerShellPoolStreams.cs | 54 ++++- src/PSParallel/PowershellPool.cs | 83 ++++--- src/PSParallel/ProgressManager.cs | 56 ++--- src/PSParallelTests/InvokeParallelTests.cs | 53 +---- 7 files changed, 280 insertions(+), 232 deletions(-) diff --git a/scripts/dbg.ps1 b/scripts/dbg.ps1 index 4699be4..e341414 100644 --- a/scripts/dbg.ps1 +++ b/scripts/dbg.ps1 @@ -3,13 +3,7 @@ param( [int] $Milliseconds = 500 ) -function new-philosopher { - param($name, [string[]] $treats) - [PSCustomObject] @{ - Name = $name - Treats = $treats - } -} +Import-Module PSParallel -RequiredVersion 2.2.1 function new-philosopher { param($name, [string[]] $treats) @@ -19,6 +13,7 @@ function new-philosopher { } } + $philosopherData = @( new-philosopher 'Immanuel Kant' 'was a real pissant','who where very rarely stable' new-philosopher 'Heidegger' 'was a boozy beggar', 'Who could think you under the table' @@ -34,7 +29,7 @@ $philosopherData = @( ) -1..100 | invoke-parallel -Throttle $ThrottleLimit -ProgressActivity "Parallel Philosofers" { +1..100 | invoke-parallel -Throttle $ThrottleLimit { diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 40b8281..6879391 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Management.Automation; @@ -19,46 +20,41 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable public ScriptBlock ScriptBlock { get; set; } [Parameter(ParameterSetName = "Progress")] - [Alias("ppi")] + [Alias("ppi")] public int ParentProgressId { get; set; } = -1; [Parameter(ParameterSetName = "Progress")] - [Alias("pi")] + [Alias("pi")] public int ProgressId { get; set; } = 1000; [Parameter(ParameterSetName = "Progress")] - [Alias("pa")] + [Alias("pa")] [ValidateNotNullOrEmpty] public string ProgressActivity { get; set; } = "Invoke-Parallel"; [Parameter] - [ValidateRange(1,128)] + [ValidateRange(1, 128)] public int ThrottleLimit { get; set; } = 32; - [Parameter] + [Parameter] [AllowNull] [Alias("iss")] public InitialSessionState InitialSessionState { get; set; } - + [Parameter(ValueFromPipeline = true, Mandatory = true)] public PSObject InputObject { get; set; } [Parameter(ParameterSetName = "NoProgress")] public SwitchParameter NoProgress { get; set; } - private readonly CancellationTokenSource m_cancelationTokenSource = new CancellationTokenSource(); - private PowershellPool m_powershellPool; - private ProgressManager m_progressManager; - - // this is only used when NoProgress is not specified - // Input is then captured in ProcessRecored and processed in EndProcessing - private List m_input; + private readonly CancellationTokenSource _cancelationTokenSource = new CancellationTokenSource(); + internal PowershellPool PowershellPool; private static InitialSessionState GetSessionState(SessionState sessionState) { var initialSessionState = InitialSessionState.CreateDefault2(); CaptureVariables(sessionState, initialSessionState); - CaptureFunctions(sessionState, initialSessionState); + CaptureFunctions(sessionState, initialSessionState); return initialSessionState; } @@ -67,12 +63,12 @@ private static IEnumerable GetFunctions(SessionState sessionState) try { var functionDrive = sessionState.InvokeProvider.Item.Get("function:"); - return (Dictionary.ValueCollection) functionDrive[0].BaseObject; - + return (Dictionary.ValueCollection)functionDrive[0].BaseObject; + } catch (DriveNotFoundException) { - return new FunctionInfo[] {}; + return new FunctionInfo[] { }; } } @@ -80,21 +76,22 @@ private static IEnumerable GetVariables(SessionState sessionState) { try { - string[] noTouchVariables = {"null", "true", "false", "Error"}; + string[] noTouchVariables = { "null", "true", "false", "Error" }; var variables = sessionState.InvokeProvider.Item.Get("Variable:"); - var psVariables = (IEnumerable) variables[0].BaseObject; - return psVariables.Where(p=>!noTouchVariables.Contains(p.Name)); + var psVariables = (IEnumerable)variables[0].BaseObject; + return psVariables.Where(p => !noTouchVariables.Contains(p.Name)); } catch (DriveNotFoundException) { - return new PSVariable[]{}; + return new PSVariable[] { }; } } private static void CaptureFunctions(SessionState sessionState, InitialSessionState initialSessionState) { var functions = GetFunctions(sessionState); - foreach (var func in functions) { + foreach (var func in functions) + { initialSessionState.Commands.Add(new SessionStateFunctionEntry(func.Name, func.Definition)); } } @@ -102,7 +99,7 @@ private static void CaptureFunctions(SessionState sessionState, InitialSessionSt private static void CaptureVariables(SessionState sessionState, InitialSessionState initialSessionState) { var variables = GetVariables(sessionState); - foreach(var variable in variables) + foreach (var variable in variables) { var existing = initialSessionState.Variables[variable.Name].FirstOrDefault(); if (existing != null && (existing.Options & (ScopedItemOptions.Constant | ScopedItemOptions.ReadOnly)) != ScopedItemOptions.None) @@ -118,7 +115,7 @@ private void ValidateParameters() if (NoProgress) { var boundParameters = MyInvocation.BoundParameters; - foreach(var p in new[]{nameof(ProgressActivity), nameof(ParentProgressId), nameof(ProgressId)}) + foreach (var p in new[] { nameof(ProgressActivity), nameof(ParentProgressId), nameof(ProgressId) }) { if (!boundParameters.ContainsKey(p)) continue; var argumentException = new ArgumentException($"'{p}' must not be specified together with 'NoProgress'", p); @@ -139,92 +136,44 @@ InitialSessionState GetSessionState() } return GetSessionState(SessionState); } - + + + private WorkerBase _worker; protected override void BeginProcessing() { ValidateParameters(); var iss = GetSessionState(); - m_powershellPool = new PowershellPool(ThrottleLimit, iss, m_cancelationTokenSource.Token); - m_powershellPool.Open(); - if (!NoProgress) - { - m_progressManager = new ProgressManager(ProgressId, ProgressActivity, $"Processing with {ThrottleLimit} workers", ParentProgressId); - m_input = new List(500); - } + PowershellPool = new PowershellPool(ThrottleLimit, iss, _cancelationTokenSource.Token); + PowershellPool.Open(); + _worker = NoProgress ? (WorkerBase) new NoProgressWorker(this) : new ProgressWorker(this); } - + protected override void ProcessRecord() { - if(NoProgress) - { - while (!m_powershellPool.TryAddInput(ScriptBlock, InputObject)) - { - WriteOutputs(); - } - } - else - { - m_input.Add(InputObject); - } + _worker.ProcessRecord(InputObject); } protected override void EndProcessing() { - try - { - if (!NoProgress) - { - m_progressManager.TotalCount = m_input.Count; - foreach (var i in m_input) - { - var processed = m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount(); - m_progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", processed); - WriteProgress(m_progressManager.ProgressRecord); - while (!m_powershellPool.TryAddInput(ScriptBlock, i)) - { - WriteOutputs(); - } - } - } - while(!m_powershellPool.WaitForAllPowershellCompleted(100)) - { - if(!NoProgress) - { - m_progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", m_powershellPool.ProcessedCount); - WriteProgress(m_progressManager.ProgressRecord); - } - if (Stopping) - { - return; - } - WriteOutputs(); - } - WriteOutputs(); - } - finally - { - if(!NoProgress) - { - WriteProgress(m_progressManager.Completed()); - } - } + _worker.EndProcessing(); + } protected override void StopProcessing() { - m_cancelationTokenSource.Cancel(); - m_powershellPool?.Stop(); + _cancelationTokenSource.Cancel(); + PowershellPool?.Stop(); } private void WriteOutputs() { Debug.WriteLine("Processing output"); - if (m_cancelationTokenSource.IsCancellationRequested) + if (_cancelationTokenSource.IsCancellationRequested) { return; } - var streams = m_powershellPool.Streams; + var streams = PowershellPool.Streams; foreach (var o in streams.Output.ReadAll()) { WriteObject(o, false); @@ -250,30 +199,134 @@ private void WriteOutputs() { WriteVerbose(v.Message); } - var progressCount = streams.Progress.Count; - if (progressCount > 0) + _worker.WriteProgress(streams.ReadAllProgress()); + } + + public void Dispose() + { + PowershellPool?.Dispose(); + _cancelationTokenSource.Dispose(); + } + + + private abstract class WorkerBase + { + protected readonly InvokeParallelCommand Cmdlet; + protected readonly PowershellPool Pool; + protected bool Stopping => Cmdlet.Stopping; + protected void WriteOutputs() => Cmdlet.WriteOutputs(); + protected void WriteProgress(ProgressRecord record) => Cmdlet.WriteProgress(record); + public abstract void ProcessRecord(PSObject inputObject); + public abstract void EndProcessing(); + public abstract void WriteProgress(Collection progress); + protected ScriptBlock ScriptBlock => Cmdlet.ScriptBlock; + + protected WorkerBase(InvokeParallelCommand cmdlet) + { + Cmdlet = cmdlet; + Pool = cmdlet.PowershellPool; + } + } + + class NoProgressWorker : WorkerBase + { + public NoProgressWorker(InvokeParallelCommand cmdlet) : base(cmdlet) { - foreach (var p in streams.Progress.ReadAll()) + } + + public override void ProcessRecord(PSObject inputObject) + { + while (!Pool.TryAddInput(Cmdlet.ScriptBlock, Cmdlet.InputObject)) { - if(!NoProgress) + Cmdlet.WriteOutputs(); + } + } + + public override void EndProcessing() + { + while (!Pool.WaitForAllPowershellCompleted(100)) + { + if (Stopping) { - p.ParentActivityId = m_progressManager.ActivityId; + return; } - WriteProgress(p); - } - if(!NoProgress) - { - m_progressManager.UpdateCurrentProgressRecord(m_powershellPool.ProcessedCount + m_powershellPool.GetPartiallyProcessedCount()); - WriteProgress(m_progressManager.ProgressRecord); + WriteOutputs(); + } + WriteOutputs(); + } + + public override void WriteProgress(Collection progress) + { + foreach (var p in progress) + { + base.WriteProgress(p); } } } - public void Dispose() + class ProgressWorker : WorkerBase { - m_powershellPool?.Dispose(); - m_cancelationTokenSource.Dispose(); + readonly ProgressManager _progressManager; + private readonly List _input; + public ProgressWorker(InvokeParallelCommand cmdlet) : base(cmdlet) + { + _progressManager = new ProgressManager(cmdlet.ProgressId, cmdlet.ProgressActivity, $"Processing with {cmdlet.ThrottleLimit} workers", cmdlet.ParentProgressId); + _input = new List(500); + } + + public override void ProcessRecord(PSObject inputObject) + { + _input.Add(inputObject); + } + + public override void EndProcessing() + { + try + { + _progressManager.TotalCount = _input.Count; + foreach (var i in _input) + { + var processed = Pool.ProcessedCount + Pool.GetPartiallyProcessedCount(); + _progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", processed); + WriteProgress(_progressManager.ProgressRecord); + while (!Pool.TryAddInput(ScriptBlock, i)) + { + WriteOutputs(); + } + } + + while (!Pool.WaitForAllPowershellCompleted(100)) + { + + _progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", Pool.ProcessedCount); + WriteProgress(_progressManager.ProgressRecord); + + if (Stopping) + { + return; + } + WriteOutputs(); + } + WriteOutputs(); + } + finally + { + WriteProgress(_progressManager.Completed()); + } + } + + public override void WriteProgress(Collection progress) + { + foreach (var p in progress) + { + p.ParentActivityId = _progressManager.ActivityId; + WriteProgress(p); + } + _progressManager.UpdateCurrentProgressRecord(Pool.ProcessedCount + Pool.GetPartiallyProcessedCount()); + WriteProgress(_progressManager.ProgressRecord); + } } + } } diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 45e0726..9356578 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -127,7 +127,7 @@ private void ProgressOnDataAdded(object sender, DataAddedEventArgs dataAddedEven { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; m_percentComplete = record.PercentComplete; - m_poolStreams.Progress.Add(record); + m_poolStreams.AddProgress(record, m_index); } private void ErrorOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) diff --git a/src/PSParallel/PowerShellPoolStreams.cs b/src/PSParallel/PowerShellPoolStreams.cs index 56e64e0..bcbe46b 100644 --- a/src/PSParallel/PowerShellPoolStreams.cs +++ b/src/PSParallel/PowerShellPoolStreams.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Management.Automation; namespace PSParallel @@ -7,9 +10,9 @@ class PowerShellPoolStreams : IDisposable { public PSDataCollection Output { get; } = new PSDataCollection(100); public PSDataCollection Debug { get; } = new PSDataCollection(); - public PSDataCollection Progress { get; } = new PSDataCollection(); + private PSDataCollection Progress { get; } = new PSDataCollection(); public PSDataCollection Error { get; } = new PSDataCollection(); - public PSDataCollection Warning { get; } = new PSDataCollection(); + public PSDataCollection Warning { get; } = new PSDataCollection(); public PSDataCollection Information { get; } = new PSDataCollection(); public PSDataCollection Verbose { get; } = new PSDataCollection(); @@ -22,5 +25,52 @@ public void Dispose() Information.Dispose(); Verbose.Dispose(); } + + public void AddProgress(ProgressRecord progress, int index) + { + DoAddProgress(progress); + OnProgressChanged(progress.PercentComplete, index); + } + + public void ClearProgress(int index) + { + OnProgressChanged(0, index); + } + + protected void DoAddProgress(ProgressRecord progress) + { + Progress.Add(progress); + } + + protected virtual void OnProgressChanged(int progress, int index){} + + public Collection ReadAllProgress() + { + return Progress.ReadAll(); + } } + + class ProgressTrackingPowerShellPoolStreams : PowerShellPoolStreams + { + private readonly int _maxPoolSize; + private readonly int[] _poolProgress; + private int _currentProgress; + public ProgressTrackingPowerShellPoolStreams(int maxPoolSize) + { + _maxPoolSize = maxPoolSize; + _poolProgress = new int[maxPoolSize]; + } + + protected override void OnProgressChanged(int progress, int index) + { + lock(_poolProgress) { + _poolProgress[index] = progress; + _currentProgress = _poolProgress.Sum(); + } + } + + public int PoolPercentComplete => _currentProgress/_maxPoolSize; + + } + } \ No newline at end of file diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 8a34cb3..f89f937 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -12,40 +12,40 @@ namespace PSParallel { sealed class PowershellPool : IDisposable { - private int m_busyCount; - private int m_processedCount; - private readonly CancellationToken m_cancellationToken; - private readonly RunspacePool m_runspacePool; - private readonly List m_poolMembers; - private readonly BlockingCollection m_availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); + private int _busyCount; + private int _processedCount; + private readonly CancellationToken _cancellationToken; + private readonly RunspacePool _runspacePool; + private readonly List _poolMembers; + private readonly BlockingCollection _availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); - public int ProcessedCount => m_processedCount; + public int ProcessedCount => _processedCount; public PowershellPool(int poolSize, InitialSessionState initialSessionState, CancellationToken cancellationToken) { - m_poolMembers= new List(poolSize); - m_processedCount = 0; - m_cancellationToken = cancellationToken; + _poolMembers= new List(poolSize); + _processedCount = 0; + _cancellationToken = cancellationToken; for (var i = 0; i < poolSize; i++) { var powerShellPoolMember = new PowerShellPoolMember(this, i+1); - m_poolMembers.Add(powerShellPoolMember); - m_availablePoolMembers.Add(powerShellPoolMember); + _poolMembers.Add(powerShellPoolMember); + _availablePoolMembers.Add(powerShellPoolMember); } - m_runspacePool = RunspaceFactory.CreateRunspacePool(initialSessionState); - m_runspacePool.SetMaxRunspaces(poolSize); + _runspacePool = RunspaceFactory.CreateRunspacePool(initialSessionState); + _runspacePool.SetMaxRunspaces(poolSize); } public int GetPartiallyProcessedCount() { var totalPercentComplete = 0; - var count = m_poolMembers.Count; + var count = _poolMembers.Count; for (int i = 0; i < count; ++i) { - var percentComplete = m_poolMembers[i].PercentComplete; + var percentComplete = _poolMembers[i].PercentComplete; if (percentComplete < 0) { percentComplete = 0; @@ -67,50 +67,50 @@ public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) return false; } - Interlocked.Increment(ref m_busyCount); + Interlocked.Increment(ref _busyCount); poolMember.BeginInvoke(scriptblock, inputObject); return true; } public void Open() { - m_runspacePool.Open(); + _runspacePool.Open(); } + public bool WaitForAllPowershellCompleted(int timeoutMilliseconds) { - Contract.Requires(timeoutMilliseconds >=0); + Contract.Requires(timeoutMilliseconds >= 0); var startTicks = Environment.TickCount; var currendTicks = startTicks; while (currendTicks - startTicks < timeoutMilliseconds) { currendTicks = Environment.TickCount; - if (m_cancellationToken.IsCancellationRequested) + if (_cancellationToken.IsCancellationRequested) { return false; } - if (Interlocked.CompareExchange(ref m_busyCount, 0, 0) == 0) + if (Interlocked.CompareExchange(ref _busyCount, 0, 0) == 0) { return true; } Thread.Sleep(10); - } return false; } private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolMember poolMember) - { - if(!m_availablePoolMembers.TryTake(out poolMember, milliseconds, m_cancellationToken)) + { + if (!_availablePoolMembers.TryTake(out poolMember, milliseconds, _cancellationToken)) { - m_cancellationToken.ThrowIfCancellationRequested(); - Debug.WriteLine($"WaitForAvailablePowershell - TryTake failed"); + _cancellationToken.ThrowIfCancellationRequested(); + Debug.WriteLine("WaitForAvailablePowershell - TryTake failed"); poolMember = null; return false; } - - poolMember.PowerShell.RunspacePool = m_runspacePool; - Debug.WriteLine($"WaitForAvailablePowershell - Busy: {m_busyCount} _processed {m_processedCount}, member = {poolMember.Index}"); + + poolMember.PowerShell.RunspacePool = _runspacePool; + Debug.WriteLine($"WaitForAvailablePowershell - Busy: {_busyCount} _processed {_processedCount}, member = {poolMember.Index}"); return true; } @@ -118,32 +118,31 @@ private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolM public void Dispose() { Streams.Dispose(); - m_availablePoolMembers.Dispose(); - m_runspacePool?.Dispose(); + _availablePoolMembers.Dispose(); + _runspacePool?.Dispose(); } public void ReportAvailable(PowerShellPoolMember poolmember) { - Interlocked.Decrement(ref m_busyCount); - Interlocked.Increment(ref m_processedCount); - while (!m_availablePoolMembers.TryAdd(poolmember, 1000, m_cancellationToken)) + Interlocked.Decrement(ref _busyCount); + Interlocked.Increment(ref _processedCount); + while (!_availablePoolMembers.TryAdd(poolmember, 1000, _cancellationToken)) { - m_cancellationToken.ThrowIfCancellationRequested(); - Debug.WriteLine($"WaitForAvailablePowershell - TryAdd failed"); - } - Debug.WriteLine($"ReportAvailable - Busy: {m_busyCount} _processed {m_processedCount}, member = {poolmember.Index}"); - + _cancellationToken.ThrowIfCancellationRequested(); + Debug.WriteLine("WaitForAvailablePowershell - TryAdd failed"); + } + Debug.WriteLine($"ReportAvailable - Busy: {_busyCount} _processed {_processedCount}, member = {poolmember.Index}"); } public void ReportStopped(PowerShellPoolMember powerShellPoolMember) { - Interlocked.Decrement(ref m_busyCount); + Interlocked.Decrement(ref _busyCount); } public void Stop() { - m_availablePoolMembers.CompleteAdding(); - foreach (var poolMember in m_poolMembers) + _availablePoolMembers.CompleteAdding(); + foreach (var poolMember in _poolMembers) { poolMember.Stop(); } diff --git a/src/PSParallel/ProgressManager.cs b/src/PSParallel/ProgressManager.cs index ee7ca32..d16489e 100644 --- a/src/PSParallel/ProgressManager.cs +++ b/src/PSParallel/ProgressManager.cs @@ -7,31 +7,31 @@ namespace PSParallel class ProgressManager { public int TotalCount { get; set; } - private ProgressRecord m_progressRecord; - private readonly Stopwatch m_stopwatch; + private readonly ProgressRecord _progressRecord; + private readonly Stopwatch _stopwatch; public ProgressManager(int activityId, string activity, string statusDescription, int parentActivityId = -1, int totalCount = 0) { TotalCount = totalCount; - m_stopwatch = new Stopwatch(); - m_progressRecord = new ProgressRecord(activityId, activity, statusDescription) {ParentActivityId = parentActivityId}; + _stopwatch = new Stopwatch(); + _progressRecord = new ProgressRecord(activityId, activity, statusDescription) {ParentActivityId = parentActivityId}; } public void UpdateCurrentProgressRecord(int count) { - if (!m_stopwatch.IsRunning && TotalCount > 0) + if (!_stopwatch.IsRunning && TotalCount > 0) { - m_stopwatch.Start(); + _stopwatch.Start(); } - m_progressRecord.RecordType = ProgressRecordType.Processing; + _progressRecord.RecordType = ProgressRecordType.Processing; if (TotalCount > 0) { var percentComplete = GetPercentComplete(count); - if (percentComplete != m_progressRecord.PercentComplete) + if (percentComplete != _progressRecord.PercentComplete) { - m_progressRecord.PercentComplete = percentComplete; - m_progressRecord.SecondsRemaining = GetSecondsRemaining(count); + _progressRecord.PercentComplete = percentComplete; + _progressRecord.SecondsRemaining = GetSecondsRemaining(count); } } } @@ -40,34 +40,34 @@ public void UpdateCurrentProgressRecord(string currentOperation, int count) { UpdateCurrentProgressRecord(count); - m_progressRecord.CurrentOperation = TotalCount > 0 ? $"({count}/{TotalCount}) {currentOperation}" : currentOperation; + _progressRecord.CurrentOperation = TotalCount > 0 ? $"({count}/{TotalCount}) {currentOperation}" : currentOperation; } - public ProgressRecord ProgressRecord => m_progressRecord; + public ProgressRecord ProgressRecord => _progressRecord; public ProgressRecord Completed() { - m_stopwatch.Reset(); + _stopwatch.Reset(); - m_progressRecord.RecordType = ProgressRecordType.Completed; - return m_progressRecord; + _progressRecord.RecordType = ProgressRecordType.Completed; + return _progressRecord; } - private int GetSecondsRemaining(int count) => count == 0 ? -1 : (int) ((TotalCount - count)*m_stopwatch.ElapsedMilliseconds/1000/count); + private int GetSecondsRemaining(int count) => count == 0 ? -1 : (int) ((TotalCount - count)*_stopwatch.ElapsedMilliseconds/1000/count); private int GetPercentComplete(int count) => count*100/TotalCount; - public int ActivityId => m_progressRecord.ActivityId; + public int ActivityId => _progressRecord.ActivityId; } class ProgressProjector { - private readonly Stopwatch m_stopWatch; - private int m_percentComplete; + private readonly Stopwatch _stopWatch; + private int _percentComplete; public ProgressProjector() { - m_stopWatch = new Stopwatch(); - m_percentComplete = -1; + _stopWatch = new Stopwatch(); + _percentComplete = -1; } public void ReportProgress(int percentComplete) @@ -76,23 +76,23 @@ public void ReportProgress(int percentComplete) { percentComplete = 100; } - m_percentComplete = percentComplete; + _percentComplete = percentComplete; } - public bool IsValid => m_percentComplete > 0 && m_stopWatch.IsRunning; - public TimeSpan Elapsed => m_stopWatch.Elapsed; + public bool IsValid => _percentComplete > 0 && _stopWatch.IsRunning; + public TimeSpan Elapsed => _stopWatch.Elapsed; - public TimeSpan ProjectedTotalTime => new TimeSpan(Elapsed.Ticks * 100 / m_percentComplete); + public TimeSpan ProjectedTotalTime => new TimeSpan(Elapsed.Ticks * 100 / _percentComplete); public void Start() { - m_stopWatch.Start(); - m_percentComplete = 0; + _stopWatch.Start(); + _percentComplete = 0; } public void Stop() { - m_stopWatch.Stop(); + _stopWatch.Stop(); } } } diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index b20a100..24926f6 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -290,7 +290,7 @@ public void TestProgressOutput() input.Complete(); ps.Invoke(input); var progress = ps.Streams.Progress.ReadAll(); - Assert.AreEqual(11, progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); + Assert.AreEqual(13, progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); } } @@ -369,56 +369,7 @@ function bar($x) {return 3 * (foo $x)} } } - [TestMethod] - public void TestLimitingVariables() - { - using (PowerShell ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; - ps.AddScript(@" -$x = 1 -$y = 2 - ", false); - - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ImportVariable", 'x') - .AddParameter("ScriptBlock", ScriptBlock.Create("$x,$y")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress") - .AddParameter("InputObject", 1); - - var output = ps.Invoke(); - int x = (int) output[0].BaseObject; - Assert.AreEqual(1,x); - Assert.IsNull(output[1]); - } - } - - [TestMethod] - public void TestLimitingFunctions() - { - using (PowerShell ps = PowerShell.Create()) - { - ps.RunspacePool = m_runspacePool; - ps.AddScript(@" -function f{1} -function g{} - ", false).Invoke(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ImportFunction", 'f') - .AddParameter("ScriptBlock", ScriptBlock.Create("f;g")) - .AddParameter("ThrottleLimit", 1) - .AddParameter("NoProgress") - .AddParameter("InputObject", 1); - - var output = ps.Invoke(); - int x = (int)output[0].BaseObject; - Assert.AreEqual(1, x); - Assert.IsTrue(ps.HadErrors); - } - } - + public void Dispose() { m_runspacePool.Dispose(); From c3fb70bc3b667166758114a4bcd9066dac468fb5 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 20 Apr 2016 13:39:20 +0200 Subject: [PATCH 30/38] Ignoring .db --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 57a1574..6c86302 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,4 @@ FakesAssemblies/ # Visual Studio 6 workspace options file *.opt +*.db From cff762aab3f17e3093c7590256ff72b72bde2cfd Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 21 Apr 2016 10:47:05 +0200 Subject: [PATCH 31/38] Progress working --- src/PSParallel/InvokeParallelCommand.cs | 28 ++++++--- src/PSParallel/PSParallel.csproj | 29 ++++++++- src/PSParallel/PowerShellPoolMember.cs | 8 ++- src/PSParallel/PowerShellPoolStreams.cs | 2 + src/PSParallel/PowershellPool.cs | 33 +++++++--- src/PSParallel/ProgressManager.cs | 21 +++++-- src/PSParallel/PsParallelEventSource.cs | 62 +++++++++++++++++++ src/PSParallel/_EventRegisterUsersGuide.docx | Bin 0 -> 112690 bytes src/PSParallel/packages.config | 5 ++ 9 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 src/PSParallel/PsParallelEventSource.cs create mode 100644 src/PSParallel/_EventRegisterUsersGuide.docx create mode 100644 src/PSParallel/packages.config diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 6879391..47ca757 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -7,6 +7,8 @@ using System.Management.Automation.Runspaces; using System.Threading; +using static PSParallel.PsParallelEventSource; + // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable MemberCanBePrivate.Global @@ -126,15 +128,23 @@ private void ValidateParameters() InitialSessionState GetSessionState() { - if (MyInvocation.BoundParameters.ContainsKey(nameof(InitialSessionState))) + try { - if (InitialSessionState == null) + Log.BeginGetSessionState(); + if (MyInvocation.BoundParameters.ContainsKey(nameof(InitialSessionState))) { - return InitialSessionState.Create(); + if (InitialSessionState == null) + { + return InitialSessionState.Create(); + } + return InitialSessionState; } - return InitialSessionState; + return GetSessionState(SessionState); + } + finally + { + Log.EndGetSessionState(); } - return GetSessionState(SessionState); } @@ -286,7 +296,7 @@ public override void EndProcessing() _progressManager.TotalCount = _input.Count; foreach (var i in _input) { - var processed = Pool.ProcessedCount + Pool.GetPartiallyProcessedCount(); + var processed = Pool.GetEstimatedProgressCount(); _progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", processed); WriteProgress(_progressManager.ProgressRecord); while (!Pool.TryAddInput(ScriptBlock, i)) @@ -298,7 +308,7 @@ public override void EndProcessing() while (!Pool.WaitForAllPowershellCompleted(100)) { - _progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", Pool.ProcessedCount); + _progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", Pool.GetEstimatedProgressCount()); WriteProgress(_progressManager.ProgressRecord); if (Stopping) @@ -322,11 +332,9 @@ public override void WriteProgress(Collection progress) p.ParentActivityId = _progressManager.ActivityId; WriteProgress(p); } - _progressManager.UpdateCurrentProgressRecord(Pool.ProcessedCount + Pool.GetPartiallyProcessedCount()); + _progressManager.UpdateCurrentProgressRecord(Pool.GetEstimatedProgressCount()); WriteProgress(_progressManager.ProgressRecord); } } - - } } diff --git a/src/PSParallel/PSParallel.csproj b/src/PSParallel/PSParallel.csproj index 5a72cfb..fc7fb23 100644 --- a/src/PSParallel/PSParallel.csproj +++ b/src/PSParallel/PSParallel.csproj @@ -12,6 +12,9 @@ v4.5 512 + + + c:\ETW true @@ -33,6 +36,10 @@ false + + ..\..\packages\Microsoft.Diagnostics.Tracing.EventSource.Redist.1.1.28\lib\net40\Microsoft.Diagnostics.Tracing.EventSource.dll + True + False ..\..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.PowerShell.Commands.Utility\v4.0_3.0.0.0__31bf3856ad364e35\Microsoft.PowerShell.Commands.Utility.dll @@ -52,13 +59,31 @@ + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 9356578..79fe29f 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -15,8 +15,12 @@ class PowerShellPoolMember : IDisposable private readonly PSDataCollection m_input =new PSDataCollection(); private PSDataCollection m_output; private int m_percentComplete; - public int PercentComplete => m_percentComplete; - + public int PercentComplete + { + get { return m_percentComplete; } + set { m_percentComplete = value; } + } + public PowerShellPoolMember(PowershellPool pool, int index) { diff --git a/src/PSParallel/PowerShellPoolStreams.cs b/src/PSParallel/PowerShellPoolStreams.cs index bcbe46b..b0eacc4 100644 --- a/src/PSParallel/PowerShellPoolStreams.cs +++ b/src/PSParallel/PowerShellPoolStreams.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; +using static PSParallel.PsParallelEventSource; namespace PSParallel { @@ -28,6 +29,7 @@ public void Dispose() public void AddProgress(ProgressRecord progress, int index) { + Log.OnProgress(index, progress.PercentComplete, progress.CurrentOperation); DoAddProgress(progress); OnProgressChanged(progress.PercentComplete, index); } diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index f89f937..8d8c661 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -8,24 +8,24 @@ using System.Management.Automation.Runspaces; using System.Threading; +using static PSParallel.PsParallelEventSource; + namespace PSParallel { sealed class PowershellPool : IDisposable { + private readonly object _countLock = new object(); private int _busyCount; - private int _processedCount; private readonly CancellationToken _cancellationToken; private readonly RunspacePool _runspacePool; private readonly List _poolMembers; private readonly BlockingCollection _availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); - - public int ProcessedCount => _processedCount; + private int _processedCount; public PowershellPool(int poolSize, InitialSessionState initialSessionState, CancellationToken cancellationToken) { _poolMembers= new List(poolSize); - _processedCount = 0; _cancellationToken = cancellationToken; for (var i = 0; i < poolSize; i++) @@ -39,7 +39,7 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can _runspacePool.SetMaxRunspaces(poolSize); } - public int GetPartiallyProcessedCount() + private int GetPartiallyProcessedCount() { var totalPercentComplete = 0; var count = _poolMembers.Count; @@ -55,8 +55,17 @@ public int GetPartiallyProcessedCount() percentComplete = 100; } totalPercentComplete += percentComplete; - } - return totalPercentComplete / 100; + } + var partiallyProcessedCount = totalPercentComplete / 100; + Log.PartiallyProcessed(partiallyProcessedCount, totalPercentComplete); + return partiallyProcessedCount; + } + + public int GetEstimatedProgressCount() + { + lock(_countLock) { + return _processedCount + GetPartiallyProcessedCount(); + } } public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) @@ -110,7 +119,6 @@ private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolM } poolMember.PowerShell.RunspacePool = _runspacePool; - Debug.WriteLine($"WaitForAvailablePowershell - Busy: {_busyCount} _processed {_processedCount}, member = {poolMember.Index}"); return true; } @@ -125,13 +133,18 @@ public void Dispose() public void ReportAvailable(PowerShellPoolMember poolmember) { Interlocked.Decrement(ref _busyCount); - Interlocked.Increment(ref _processedCount); + lock (_countLock) + { + _processedCount++; + poolmember.PercentComplete = 0; + } + + poolmember.PercentComplete = 0; while (!_availablePoolMembers.TryAdd(poolmember, 1000, _cancellationToken)) { _cancellationToken.ThrowIfCancellationRequested(); Debug.WriteLine("WaitForAvailablePowershell - TryAdd failed"); } - Debug.WriteLine($"ReportAvailable - Busy: {_busyCount} _processed {_processedCount}, member = {poolmember.Index}"); } public void ReportStopped(PowerShellPoolMember powerShellPoolMember) diff --git a/src/PSParallel/ProgressManager.cs b/src/PSParallel/ProgressManager.cs index d16489e..43f81aa 100644 --- a/src/PSParallel/ProgressManager.cs +++ b/src/PSParallel/ProgressManager.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; using System.Management.Automation; - +using static PSParallel.PsParallelEventSource; namespace PSParallel { class ProgressManager @@ -33,7 +33,8 @@ public void UpdateCurrentProgressRecord(int count) _progressRecord.PercentComplete = percentComplete; _progressRecord.SecondsRemaining = GetSecondsRemaining(count); } - } + } + Log.UpdateCurrentProgressRecord(count); } public void UpdateCurrentProgressRecord(string currentOperation, int count) @@ -54,8 +55,20 @@ public ProgressRecord Completed() return _progressRecord; } - private int GetSecondsRemaining(int count) => count == 0 ? -1 : (int) ((TotalCount - count)*_stopwatch.ElapsedMilliseconds/1000/count); - private int GetPercentComplete(int count) => count*100/TotalCount; + private int GetSecondsRemaining(int count) + { + var secondsRemaining = count == 0 ? -1 : (int) ((TotalCount - count)*_stopwatch.ElapsedMilliseconds/1000/count); + Log.SecondsRemaining(count, TotalCount, secondsRemaining, _stopwatch.ElapsedTicks); + return secondsRemaining; + } + + private int GetPercentComplete(int count) + { + var percentComplete = count*100/TotalCount; + Log.GetProcessComplete(count, TotalCount); + return percentComplete; + } + public int ActivityId => _progressRecord.ActivityId; } diff --git a/src/PSParallel/PsParallelEventSource.cs b/src/PSParallel/PsParallelEventSource.cs new file mode 100644 index 0000000..5a68997 --- /dev/null +++ b/src/PSParallel/PsParallelEventSource.cs @@ -0,0 +1,62 @@ +using Microsoft.Diagnostics.Tracing; + +namespace PSParallel +{ + [EventSource(Name = "PowerCode-PSParallel-EventLog")] + public sealed class PsParallelEventSource : EventSource + { + public static PsParallelEventSource Log = new PsParallelEventSource(); + + [Event(1, Message = "UpdateProgress - Poolmember {0} -> {1}% : pool:{2}%", Channel = EventChannel.Debug)] + public void UpdateProgress(int poolMember, int progress, string poolProgress) + { + WriteEvent(1, poolMember, progress, poolProgress); + } + [Event(2, Message = "Seconds remaining - count: {0}/{1} : {2}s remaining, {3} ticks", Channel = EventChannel.Debug)] + public void SecondsRemaining(int count, int totalCount, int secondsRemaining, long elapsedTicks) + { + WriteEvent(2, count, totalCount, secondsRemaining, elapsedTicks); + } + + [Event(3, Message = "UpdateCurrentProgressRecord Count: {0}", Channel = EventChannel.Debug)] + public void UpdateCurrentProgressRecord(int count) + { + WriteEvent(3, count); + } + + [Event(4, Message = "ProcessComplete Count: {0}, TotalCount={1}", Channel = EventChannel.Debug)] + public void GetProcessComplete(int count, int totalCount) + { + WriteEvent(4, count, totalCount); + } + + [Event(5, Message = "Paritally processed Count: {0} TotalPercentComplete={1}", Channel = EventChannel.Debug)] + public void PartiallyProcessed(int processedCount, int totalPercentComplete) + { + WriteEvent(5, processedCount, totalPercentComplete); + } + + [Event(6, Message = "Worker {0}: {1}% {2}", Channel = EventChannel.Debug)] + public void OnProgress(int index, int percentComplete, string currentOperation) + { + WriteEvent(6, index, percentComplete, currentOperation); + } + + [Event(14, Message = "BeginGetSessionState", Task=Tasks.SessionState, Opcode = EventOpcode.Start, Channel = EventChannel.Debug)] + public void BeginGetSessionState() + { + WriteEvent(14); + } + [Event(15, Message = "EndGetSessionState", Task = Tasks.SessionState, Opcode = EventOpcode.Stop, Channel = EventChannel.Debug)] + public void EndGetSessionState() + { + WriteEvent(15); + } + } + + public class Tasks + { + public const EventTask SessionState = (EventTask)1; + public const EventTask CreateRunspace = (EventTask)2; + } +} \ No newline at end of file diff --git a/src/PSParallel/_EventRegisterUsersGuide.docx b/src/PSParallel/_EventRegisterUsersGuide.docx new file mode 100644 index 0000000000000000000000000000000000000000..f311d20a442ae11ca959c5fc73c4dabec809dbb3 GIT binary patch literal 112690 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 M9zLiCdM3*M0#iws1^@s6 literal 0 HcmV?d00001 diff --git a/src/PSParallel/packages.config b/src/PSParallel/packages.config new file mode 100644 index 0000000..ebecea3 --- /dev/null +++ b/src/PSParallel/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From b48a3f88b7044ac72caccc83e7e81e1e6c826bd1 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 21 Apr 2016 10:48:06 +0200 Subject: [PATCH 32/38] Bumping version to 2.2.2 --- module/PSParallel.psd1 | Bin 3888 -> 3888 bytes src/PSParallel/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 0d9e41339b60c68610d6d4e959729355838e42ba..21f998e1b787e959aef92ae6e37739a9215e21f2 100644 GIT binary patch delta 14 VcmdlWw?S@$A0wmDW`D+qyZ|O71i%0Q delta 14 VcmdlWw?S@$A0wmTW`D+qyZ|O11it_P diff --git a/src/PSParallel/Properties/AssemblyInfo.cs b/src/PSParallel/Properties/AssemblyInfo.cs index 79cea25..7e2c590 100644 --- a/src/PSParallel/Properties/AssemblyInfo.cs +++ b/src/PSParallel/Properties/AssemblyInfo.cs @@ -32,4 +32,4 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("2.2.0.0")] +[assembly: AssemblyFileVersion("2.2.2.0")] From c1abe6de34bf5f903cf662bf627526d55d35c8ef Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 21 Apr 2016 10:52:11 +0200 Subject: [PATCH 33/38] Fixing missing install files --- scripts/Install.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/Install.ps1 b/scripts/Install.ps1 index 3e9bce4..32862f0 100644 --- a/scripts/Install.ps1 +++ b/scripts/Install.ps1 @@ -34,6 +34,7 @@ if(-not (Test-Path $InstallDirectory)) @( 'module\PSParallel.psd1' 'src\PsParallel\bin\Release\PSParallel.dll' + 'src\PsParallel\bin\Release\Microsoft*.dll' ).Foreach{Copy-Item "$rootdir\$_" -Destination $InstallDirectory } $lang = @('en-us') From 3ee03e9fa6fbdcb53280de9603bf1c0120013011 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 3 May 2016 16:45:03 +0200 Subject: [PATCH 34/38] Manginging runspaces w/o using RunspacePool. Improving progress handling. --- src/PSParallel/InvokeParallelCommand.cs | 70 ++++++++++---- src/PSParallel/PowerShellPoolMember.cs | 107 ++++++++++++--------- src/PSParallel/PowershellPool.cs | 24 ++--- src/PSParallel/ProgressManager.cs | 86 +++++++++++++---- src/PSParallelTests/InvokeParallelTests.cs | 81 +++++++++------- 5 files changed, 231 insertions(+), 137 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 350cd90..8411270 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -77,8 +77,9 @@ private static IEnumerable GetVariables(SessionState sessionState) try { string[] noTouchVariables = { "null", "true", "false", "Error" }; - var variables = sessionState.InvokeProvider.Item.Get("Variable:"); + var variables = sessionState.InvokeProvider.Item.Get("Variable:"); var psVariables = (IEnumerable)variables[0].BaseObject; + return psVariables.Where(p => !noTouchVariables.Contains(p.Name)); } catch (DriveNotFoundException) @@ -102,11 +103,22 @@ private static void CaptureVariables(SessionState sessionState, InitialSessionSt foreach (var variable in variables) { var existing = initialSessionState.Variables[variable.Name].FirstOrDefault(); - if (existing != null && (existing.Options & (ScopedItemOptions.Constant | ScopedItemOptions.ReadOnly)) != ScopedItemOptions.None) + if (existing != null) { - continue; + if ((existing.Options & (ScopedItemOptions.Constant | ScopedItemOptions.ReadOnly)) != ScopedItemOptions.None) + { + continue; + } + else + { + initialSessionState.Variables.Remove(existing.Name, existing.GetType()); + initialSessionState.Variables.Add(new SessionStateVariableEntry(variable.Name, variable.Value, variable.Description, variable.Options, variable.Attributes)); + } + } + else + { + initialSessionState.Variables.Add(new SessionStateVariableEntry(variable.Name, variable.Value, variable.Description, variable.Options, variable.Attributes)); } - initialSessionState.Variables.Add(new SessionStateVariableEntry(variable.Name, variable.Value, variable.Description, variable.Options, variable.Attributes)); } } @@ -134,7 +146,7 @@ InitialSessionState GetSessionState() } return InitialSessionState; } - return GetSessionState(SessionState); + return GetSessionState(base.SessionState); } @@ -143,8 +155,7 @@ protected override void BeginProcessing() { ValidateParameters(); var iss = GetSessionState(); - PowershellPool = new PowershellPool(ThrottleLimit, iss, _cancelationTokenSource.Token); - PowershellPool.Open(); + PowershellPool = new PowershellPool(ThrottleLimit, iss, _cancelationTokenSource.Token); _worker = NoProgress ? (WorkerBase) new NoProgressWorker(this) : new ProgressWorker(this); } @@ -268,6 +279,7 @@ class ProgressWorker : WorkerBase { readonly ProgressManager _progressManager; private readonly List _input; + private int _lastEstimate = -1; public ProgressWorker(InvokeParallelCommand cmdlet) : base(cmdlet) { _progressManager = new ProgressManager(cmdlet.ProgressId, cmdlet.ProgressActivity, $"Processing with {cmdlet.ThrottleLimit} workers", cmdlet.ParentProgressId); @@ -282,25 +294,30 @@ public override void ProcessRecord(PSObject inputObject) public override void EndProcessing() { try - { + { _progressManager.TotalCount = _input.Count; + var lastPercentComplete = -1; foreach (var i in _input) { var processed = Pool.GetEstimatedProgressCount(); - _progressManager.UpdateCurrentProgressRecord($"Starting processing of {i}", processed); - WriteProgress(_progressManager.ProgressRecord); + _lastEstimate = processed; + _progressManager.SetCurrentOperation($"Starting processing of {i}"); + _progressManager.UpdateCurrentProgressRecord(processed); + var pr = _progressManager.ProgressRecord; + if (lastPercentComplete != pr.PercentComplete) + { + WriteProgress(pr); + } + while (!Pool.TryAddInput(ScriptBlock, i)) { WriteOutputs(); } } - + _progressManager.SetCurrentOperation("All work queued. Waiting for remaining work to complete."); while (!Pool.WaitForAllPowershellCompleted(100)) { - - _progressManager.UpdateCurrentProgressRecord("All work queued. Waiting for remaining work to complete.", Pool.GetEstimatedProgressCount()); - WriteProgress(_progressManager.ProgressRecord); - + WriteProgressIfUpdated(); if (Stopping) { return; @@ -311,6 +328,7 @@ public override void EndProcessing() } finally { + _progressManager.UpdateCurrentProgressRecord(Pool.GetEstimatedProgressCount()); WriteProgress(_progressManager.Completed()); } } @@ -319,11 +337,27 @@ public override void WriteProgress(Collection progress) { foreach (var p in progress) { - p.ParentActivityId = _progressManager.ActivityId; + if (p.ActivityId != _progressManager.ActivityId) + { + p.ParentActivityId = _progressManager.ActivityId; + } WriteProgress(p); } - _progressManager.UpdateCurrentProgressRecord(Pool.GetEstimatedProgressCount()); - WriteProgress(_progressManager.ProgressRecord); + if (progress.Count > 0) + { + WriteProgressIfUpdated(); + } + } + + private void WriteProgressIfUpdated() + { + var estimatedCompletedCount = Pool.GetEstimatedProgressCount(); + if (_lastEstimate != estimatedCompletedCount) + { + _lastEstimate = estimatedCompletedCount; + _progressManager.UpdateCurrentProgressRecord(estimatedCompletedCount); + WriteProgress(_progressManager.ProgressRecord); + } } } } diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 79fe29f..73f8cf9 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -1,33 +1,37 @@ using System; using System.Management.Automation; +using System.Management.Automation.Runspaces; namespace PSParallel { class PowerShellPoolMember : IDisposable { - private readonly PowershellPool m_pool; - private readonly int m_index; - private readonly PowerShellPoolStreams m_poolStreams; - private PowerShell m_powerShell; - public PowerShell PowerShell => m_powerShell; - public int Index => m_index ; - - private readonly PSDataCollection m_input =new PSDataCollection(); - private PSDataCollection m_output; - private int m_percentComplete; + private readonly PowershellPool _pool; + private readonly int _index; + private readonly PowerShellPoolStreams _poolStreams; + private readonly Runspace _runspace; + private PowerShell _powerShell; + public PowerShell PowerShell => _powerShell; + public int Index => _index ; + + private readonly PSDataCollection _input =new PSDataCollection(); + private PSDataCollection _output; + private int _percentComplete; public int PercentComplete { - get { return m_percentComplete; } - set { m_percentComplete = value; } + get { return _percentComplete; } + set { _percentComplete = value; } } - public PowerShellPoolMember(PowershellPool pool, int index) + public PowerShellPoolMember(PowershellPool pool, int index, Runspace runspace) { - m_pool = pool; - m_index = index; - m_poolStreams = m_pool.Streams; - m_input.Complete(); + _pool = pool; + _index = index; + _runspace = runspace; + _runspace.Open(); + _poolStreams = _pool.Streams; + _input.Complete(); CreatePowerShell(); } @@ -37,13 +41,13 @@ private void PowerShellOnInvocationStateChanged(object sender, PSInvocationState { case PSInvocationState.Stopped: ReleasePowerShell(); - m_pool.ReportStopped(this); + _pool.ReportStopped(this); break; case PSInvocationState.Completed: case PSInvocationState.Failed: ReleasePowerShell(); CreatePowerShell(); - m_pool.ReportAvailable(this); + _pool.ReportAvailable(this); break; } } @@ -51,20 +55,21 @@ private void PowerShellOnInvocationStateChanged(object sender, PSInvocationState private void CreatePowerShell() { var powerShell = PowerShell.Create(); + powerShell.Runspace = _runspace; HookStreamEvents(powerShell.Streams); powerShell.InvocationStateChanged += PowerShellOnInvocationStateChanged; - m_powerShell = powerShell; - m_output = new PSDataCollection(); - m_output.DataAdded += OutputOnDataAdded; + _powerShell = powerShell; + _output = new PSDataCollection(); + _output.DataAdded += OutputOnDataAdded; } private void ReleasePowerShell() { - UnhookStreamEvents(m_powerShell.Streams); - m_powerShell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; - m_output.DataAdded -= OutputOnDataAdded; - m_powerShell.Dispose(); - m_powerShell = null; + UnhookStreamEvents(_powerShell.Streams); + _powerShell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; + _output.DataAdded -= OutputOnDataAdded; + _powerShell.Dispose(); + _powerShell = null; } @@ -92,90 +97,96 @@ private void UnhookStreamEvents(PSDataStreams streams) public void BeginInvoke(ScriptBlock scriptblock, PSObject inputObject) { - m_percentComplete = 0; + _percentComplete = 0; string command = $"param($_,$PSItem, $PSPArallelIndex,$PSParallelProgressId){scriptblock}"; - m_powerShell.AddScript(command) + _powerShell.AddScript(command) .AddParameter("_", inputObject) .AddParameter("PSItem", inputObject) - .AddParameter("PSParallelIndex", m_index) - .AddParameter("PSParallelProgressId", m_index+1000); - m_powerShell.BeginInvoke(m_input, m_output); + .AddParameter("PSParallelIndex", _index) + .AddParameter("PSParallelProgressId", _index+1000); + _powerShell.BeginInvoke(_input, _output); + } + + internal void Reset() + { + _runspace.ResetRunspaceState(); } public void Dispose() { - var ps = m_powerShell; + var ps = _powerShell; if (ps != null) { UnhookStreamEvents(ps.Streams); ps.Dispose(); } - m_output.Dispose(); - m_input.Dispose(); + _output.Dispose(); + _input.Dispose(); + _runspace.Dispose(); } private void OutputOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var item = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Output.Add(item); + _poolStreams.Output.Add(item); } private void InformationOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var ir = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Information.Add(ir); + _poolStreams.Information.Add(ir); } private void ProgressOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_percentComplete = record.PercentComplete; - m_poolStreams.AddProgress(record, m_index); + _percentComplete = record.PercentComplete; + _poolStreams.AddProgress(record, _index); } private void ErrorOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Error.Add(record); + _poolStreams.Error.Add(record); } private void DebugOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Debug.Add(record); + _poolStreams.Debug.Add(record); } private void WarningOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Warning.Add(record); + _poolStreams.Warning.Add(record); } private void VerboseOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - m_poolStreams.Verbose.Add(record); + _poolStreams.Verbose.Add(record); } public void Stop() { - if(m_powerShell.InvocationStateInfo.State != PSInvocationState.Stopped) + if(_powerShell.InvocationStateInfo.State != PSInvocationState.Stopped) { - UnhookStreamEvents(m_powerShell.Streams); - m_powerShell.BeginStop(OnStopped, null); + UnhookStreamEvents(_powerShell.Streams); + _powerShell.BeginStop(OnStopped, null); } } private void OnStopped(IAsyncResult ar) { - var ps = m_powerShell; + var ps = _powerShell; if (ps == null) { return; } ps.EndStop(ar); - m_powerShell = null; + _powerShell = null; } } } \ No newline at end of file diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index eb5994f..81aa9ab 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.Contracts; using System.Management.Automation; +using System.Management.Automation.Host; using System.Management.Automation.Runspaces; using System.Threading; @@ -14,8 +15,7 @@ sealed class PowershellPool : IDisposable { private readonly object _countLock = new object(); private int _busyCount; - private readonly CancellationToken _cancellationToken; - private readonly RunspacePool _runspacePool; + private readonly CancellationToken _cancellationToken; private readonly List _poolMembers; private readonly BlockingCollection _availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); @@ -28,13 +28,11 @@ public PowershellPool(int poolSize, InitialSessionState initialSessionState, Can for (var i = 0; i < poolSize; i++) { - var powerShellPoolMember = new PowerShellPoolMember(this, i+1); + var runspace = RunspaceFactory.CreateRunspace(initialSessionState); + var powerShellPoolMember = new PowerShellPoolMember(this, i+1, runspace); _poolMembers.Add(powerShellPoolMember); _availablePoolMembers.Add(powerShellPoolMember); - } - - _runspacePool = RunspaceFactory.CreateRunspacePool(initialSessionState); - _runspacePool.SetMaxRunspaces(poolSize); + } } private int GetPartiallyProcessedCount() @@ -77,11 +75,7 @@ public bool TryAddInput(ScriptBlock scriptblock,PSObject inputObject) poolMember.BeginInvoke(scriptblock, inputObject); return true; } - - public void Open() - { - _runspacePool.Open(); - } + public bool WaitForAllPowershellCompleted(int timeoutMilliseconds) @@ -114,8 +108,7 @@ private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolM poolMember = null; return false; } - - poolMember.PowerShell.RunspacePool = _runspacePool; + poolMember.Reset(); return true; } @@ -123,8 +116,7 @@ private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolM public void Dispose() { Streams.Dispose(); - _availablePoolMembers.Dispose(); - _runspacePool?.Dispose(); + _availablePoolMembers.Dispose(); } public void ReportAvailable(PowerShellPoolMember poolmember) diff --git a/src/PSParallel/ProgressManager.cs b/src/PSParallel/ProgressManager.cs index 3a1910a..2291b91 100644 --- a/src/PSParallel/ProgressManager.cs +++ b/src/PSParallel/ProgressManager.cs @@ -6,8 +6,9 @@ namespace PSParallel class ProgressManager { public int TotalCount { get; set; } - private readonly ProgressRecord _progressRecord; - private readonly Stopwatch _stopwatch; + private ProgressRecord _progressRecord; + private readonly Stopwatch _stopwatch; + private string _currentOperation; public ProgressManager(int activityId, string activity, string statusDescription, int parentActivityId = -1, int totalCount = 0) { @@ -17,29 +18,33 @@ public ProgressManager(int activityId, string activity, string statusDescription } - public void UpdateCurrentProgressRecord(int count) + private void UpdateCurrentProgressRecordInternal(int count) { if (!_stopwatch.IsRunning && TotalCount > 0) { _stopwatch.Start(); - } - _progressRecord.RecordType = ProgressRecordType.Processing; + } + var current = TotalCount > 0 ? $"({count}/{TotalCount}) {_currentOperation}" : _currentOperation; + var pr = _progressRecord.Clone(); + pr.CurrentOperation = current; + pr.RecordType = ProgressRecordType.Processing; if (TotalCount > 0) - { - var percentComplete = GetPercentComplete(count); - if (percentComplete != _progressRecord.PercentComplete) - { - _progressRecord.PercentComplete = percentComplete; - _progressRecord.SecondsRemaining = GetSecondsRemaining(count); - } + { + pr.PercentComplete = GetPercentComplete(count); + pr.SecondsRemaining = GetSecondsRemaining(count); } - } + _progressRecord = pr; + } - public void UpdateCurrentProgressRecord(string currentOperation, int count) + public void SetCurrentOperation(string currentOperation) { - UpdateCurrentProgressRecord(count); + _currentOperation = currentOperation; + } - _progressRecord.CurrentOperation = TotalCount > 0 ? $"({count}/{TotalCount}) {currentOperation}" : currentOperation; + public void UpdateCurrentProgressRecord(int count) + { + + UpdateCurrentProgressRecordInternal(count); } public ProgressRecord ProgressRecord => _progressRecord; @@ -47,12 +52,12 @@ public void UpdateCurrentProgressRecord(string currentOperation, int count) public ProgressRecord Completed() { - _stopwatch.Reset(); - - _progressRecord.RecordType = ProgressRecordType.Completed; + _stopwatch.Reset(); + _progressRecord = _progressRecord.WithRecordType(ProgressRecordType.Completed); return _progressRecord; } + private int GetSecondsRemaining(int count) { var secondsRemaining = count == 0 ? -1 : (int) ((TotalCount - count)*_stopwatch.ElapsedMilliseconds/1000/count); @@ -104,4 +109,47 @@ public void Stop() _stopWatch.Stop(); } } + + static class ProgressRecordExtension + { + static ProgressRecord CloneProgressRecord(ProgressRecord record) + { + return new ProgressRecord(record.ActivityId, record.Activity, record.StatusDescription) + { + CurrentOperation = record.CurrentOperation, + ParentActivityId = record.ParentActivityId, + SecondsRemaining = record.SecondsRemaining, + PercentComplete = record.PercentComplete, + RecordType = record.RecordType + }; + } + + public static ProgressRecord Clone(this ProgressRecord record) + { + return CloneProgressRecord(record); + } + + public static ProgressRecord WithCurrentOperation(this ProgressRecord record, string currentOperation) + { + var r = CloneProgressRecord(record); + r.CurrentOperation = currentOperation; + return r; + } + + public static ProgressRecord WithRecordType(this ProgressRecord record, ProgressRecordType recordType) + { + var r = CloneProgressRecord(record); + r.RecordType = recordType; + return r; + } + + public static ProgressRecord WithPercentCompleteAndSecondsRemaining(this ProgressRecord record, int percentComplete, int secondsRemaining) + { + var r = CloneProgressRecord(record); + r.PercentComplete = percentComplete; + r.SecondsRemaining = secondsRemaining; + return r; + } + + } } diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index 24926f6..e8da425 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -12,36 +12,39 @@ namespace PSParallelTests public sealed class InvokeParallelTests : IDisposable { readonly RunspacePool m_runspacePool; - + InitialSessionState _iss; public InvokeParallelTests() - { + { + _iss = CreateInitialSessionState(); + m_runspacePool = RunspaceFactory.CreateRunspacePool(_iss); + m_runspacePool.SetMaxRunspaces(10); + m_runspacePool.Open(); + } + + private static InitialSessionState CreateInitialSessionState() + { var iss = InitialSessionState.Create(); iss.LanguageMode = PSLanguageMode.FullLanguage; - iss.Commands.Add(new [] + iss.Commands.Add(new[] { - new SessionStateCmdletEntry("Write-Error", typeof(WriteErrorCommand), null), - new SessionStateCmdletEntry("Write-Verbose", typeof(WriteVerboseCommand), null), - new SessionStateCmdletEntry("Write-Debug", typeof(WriteDebugCommand), null), - new SessionStateCmdletEntry("Write-Progress", typeof(WriteProgressCommand), null), - new SessionStateCmdletEntry("Write-Warning", typeof(WriteWarningCommand), null), + new SessionStateCmdletEntry("Write-Error", typeof(WriteErrorCommand), null), + new SessionStateCmdletEntry("Write-Verbose", typeof(WriteVerboseCommand), null), + new SessionStateCmdletEntry("Write-Debug", typeof(WriteDebugCommand), null), + new SessionStateCmdletEntry("Write-Progress", typeof(WriteProgressCommand), null), + new SessionStateCmdletEntry("Write-Warning", typeof(WriteWarningCommand), null), new SessionStateCmdletEntry("Write-Information", typeof(WriteInformationCommand), null), - new SessionStateCmdletEntry("Invoke-Parallel", typeof(InvokeParallelCommand), null), + new SessionStateCmdletEntry("Invoke-Parallel", typeof(InvokeParallelCommand), null), }); iss.Providers.Add(new SessionStateProviderEntry("Function", typeof(FunctionProvider), null)); - iss.Providers.Add(new SessionStateProviderEntry("Variable", typeof(VariableProvider), null)); - iss.Variables.Add(new [] - { - new SessionStateVariableEntry("ErrorActionPreference", ActionPreference.Continue, "Dictates the action taken when an error message is delivered"), - }); - m_runspacePool = RunspaceFactory.CreateRunspacePool(iss); - m_runspacePool.SetMaxRunspaces(10); - m_runspacePool.Open(); + iss.Providers.Add(new SessionStateProviderEntry("Variable", typeof(VariableProvider), null)); + return iss; } + [TestMethod] public void TestOutput() { using (var ps = PowerShell.Create()) - { + { ps.RunspacePool = m_runspacePool; ps.AddCommand("Invoke-Parallel") @@ -79,7 +82,7 @@ public void TestVerboseOutput() using (var ps = PowerShell.Create()) { ps.RunspacePool = m_runspacePool; - ps.AddScript("$VerbosePreference='Continue'", false).Invoke(); + ps.AddScript("$VerbosePreference=[System.Management.Automation.ActionPreference]::Continue", false).Invoke(); ps.Commands.Clear(); ps.AddStatement() .AddCommand("Invoke-Parallel", false) @@ -99,7 +102,8 @@ public void TestNoVerboseOutputWithoutPreference() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + ps.Runspace = RunspaceFactory.CreateRunspace(); + ps.Runspace.Open(); ps.AddStatement() .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Verbose $_")) @@ -110,6 +114,7 @@ public void TestNoVerboseOutputWithoutPreference() Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); var vrb = ps.Streams.Verbose.ReadAll(); Assert.IsFalse(vrb.Any(v => v.Message == "1"), "No verbose message should be '1'"); + ps.Runspace.Dispose(); } } @@ -117,20 +122,24 @@ public void TestNoVerboseOutputWithoutPreference() public void TestDebugOutput() { using (var ps = PowerShell.Create()) - { - ps.RunspacePool = m_runspacePool; - ps.AddScript("$DebugPreference='Continue'", false).Invoke(); - ps.Commands.Clear(); - ps.AddStatement() - .AddCommand("Invoke-Parallel", false) - .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Debug $_")) - .AddParameter("ThrottleLimit", 1); - var input = new PSDataCollection {1, 2, 3, 4, 5}; - input.Complete(); - ps.Invoke(input); - Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); - var dbg = ps.Streams.Debug.ReadAll(); - Assert.IsTrue(dbg.Any(d => d.Message == "1"), "Some debug message should be '1'"); + { + using (var rs = RunspaceFactory.CreateRunspace(_iss)) + { + rs.Open(); + ps.Runspace = rs; + ps.AddScript("$DebugPreference=[System.Management.Automation.ActionPreference]::Continue", false).Invoke(); + ps.Commands.Clear(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", ScriptBlock.Create("Write-Debug $_")) + .AddParameter("ThrottleLimit", 1); + var input = new PSDataCollection { 1, 2, 3, 4, 5 }; + input.Complete(); + ps.Invoke(input); + Assert.IsFalse(ps.HadErrors, "We don't expect errors here"); + var dbg = ps.Streams.Debug.ReadAll(); + Assert.IsTrue(dbg.Any(d => d.Message == "1"), "Some debug message should be '1'"); + } } } @@ -279,7 +288,7 @@ public void TestProgressOutput() using (var ps = PowerShell.Create()) { ps.RunspacePool = m_runspacePool; - + ps.AddScript("$ProgressPreference='Continue'", false).Invoke(); ps.AddStatement() .AddCommand("Invoke-Parallel", false) .AddParameter("ScriptBlock", @@ -290,7 +299,7 @@ public void TestProgressOutput() input.Complete(); ps.Invoke(input); var progress = ps.Streams.Progress.ReadAll(); - Assert.AreEqual(13, progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); + Assert.AreEqual(12, progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); } } From 01de377c86ac714719f31eddab76305c97c9267f Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 3 May 2016 16:52:00 +0200 Subject: [PATCH 35/38] Bumping version --- module/PSParallel.psd1 | Bin 3888 -> 4036 bytes src/PSParallel/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/module/PSParallel.psd1 b/module/PSParallel.psd1 index 21f998e1b787e959aef92ae6e37739a9215e21f2..85a0627e6ae28b82d7d292387862dae946bbd9e3 100644 GIT binary patch delta 139 zcmdlWcSL@JA0wmjW`D-{T;j$IybN3nK@6!3#XwvFgqaL^4C#{_c}1fY7?QwZ{1a{h0x?!yA17 delta 19 acmX>izd>$;A0wmDW`D-{T$`JCoR|Sa#0C}s diff --git a/src/PSParallel/Properties/AssemblyInfo.cs b/src/PSParallel/Properties/AssemblyInfo.cs index 7e2c590..fe5f249 100644 --- a/src/PSParallel/Properties/AssemblyInfo.cs +++ b/src/PSParallel/Properties/AssemblyInfo.cs @@ -32,4 +32,4 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("2.2.2.0")] +[assembly: AssemblyFileVersion("2.2.3.0")] From a7bfdb96bd1504a021a3616e3133896d09d493dd Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 15 Aug 2016 19:37:30 +0200 Subject: [PATCH 36/38] bugfixes to reusing runspaces --- src/PSParallel/InvokeParallelCommand.cs | 1 + src/PSParallel/PowerShellPoolMember.cs | 60 +++++++++++++--------- src/PSParallel/PowershellPool.cs | 10 ++-- src/PSParallelTests/InvokeParallelTests.cs | 29 +++++++++-- 4 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/PSParallel/InvokeParallelCommand.cs b/src/PSParallel/InvokeParallelCommand.cs index 8411270..42ab9b2 100644 --- a/src/PSParallel/InvokeParallelCommand.cs +++ b/src/PSParallel/InvokeParallelCommand.cs @@ -307,6 +307,7 @@ public override void EndProcessing() if (lastPercentComplete != pr.PercentComplete) { WriteProgress(pr); + lastPercentComplete = pr.PercentComplete; } while (!Pool.TryAddInput(ScriptBlock, i)) diff --git a/src/PSParallel/PowerShellPoolMember.cs b/src/PSParallel/PowerShellPoolMember.cs index 73f8cf9..50265ae 100644 --- a/src/PSParallel/PowerShellPoolMember.cs +++ b/src/PSParallel/PowerShellPoolMember.cs @@ -1,6 +1,7 @@ using System; using System.Management.Automation; using System.Management.Automation.Runspaces; +using System.Threading; namespace PSParallel { @@ -8,8 +9,8 @@ class PowerShellPoolMember : IDisposable { private readonly PowershellPool _pool; private readonly int _index; + private readonly InitialSessionState _initialSessionState; private readonly PowerShellPoolStreams _poolStreams; - private readonly Runspace _runspace; private PowerShell _powerShell; public PowerShell PowerShell => _powerShell; public int Index => _index ; @@ -24,15 +25,14 @@ public int PercentComplete } - public PowerShellPoolMember(PowershellPool pool, int index, Runspace runspace) + public PowerShellPoolMember(PowershellPool pool, int index, InitialSessionState initialSessionState) { _pool = pool; _index = index; - _runspace = runspace; - _runspace.Open(); + _initialSessionState = initialSessionState; _poolStreams = _pool.Streams; _input.Complete(); - CreatePowerShell(); + CreatePowerShell(initialSessionState); } private void PowerShellOnInvocationStateChanged(object sender, PSInvocationStateChangedEventArgs psInvocationStateChangedEventArgs) @@ -44,18 +44,20 @@ private void PowerShellOnInvocationStateChanged(object sender, PSInvocationState _pool.ReportStopped(this); break; case PSInvocationState.Completed: - case PSInvocationState.Failed: - ReleasePowerShell(); - CreatePowerShell(); + case PSInvocationState.Failed: + ResetPowerShell(); _pool.ReportAvailable(this); break; } } - private void CreatePowerShell() + private void CreatePowerShell(InitialSessionState initialSessionState) { - var powerShell = PowerShell.Create(); - powerShell.Runspace = _runspace; + var powerShell = PowerShell.Create(RunspaceMode.NewRunspace); + var runspace = RunspaceFactory.CreateRunspace(initialSessionState); + runspace.ApartmentState = ApartmentState.MTA; + powerShell.Runspace = runspace; + runspace.Open(); HookStreamEvents(powerShell.Streams); powerShell.InvocationStateChanged += PowerShellOnInvocationStateChanged; _powerShell = powerShell; @@ -63,17 +65,30 @@ private void CreatePowerShell() _output.DataAdded += OutputOnDataAdded; } + public void ResetPowerShell() + { + UnhookStreamEvents(_powerShell.Streams); + _powerShell.Runspace.ResetRunspaceState(); + var runspace = _powerShell.Runspace; + _powerShell = PowerShell.Create(RunspaceMode.NewRunspace); + _powerShell.Runspace = runspace; + + HookStreamEvents(_powerShell.Streams); + _powerShell.InvocationStateChanged += PowerShellOnInvocationStateChanged; + _output = new PSDataCollection(); + _output.DataAdded += OutputOnDataAdded; + } + private void ReleasePowerShell() { UnhookStreamEvents(_powerShell.Streams); _powerShell.InvocationStateChanged -= PowerShellOnInvocationStateChanged; _output.DataAdded -= OutputOnDataAdded; - _powerShell.Dispose(); - _powerShell = null; + _powerShell.Dispose(); } - private void HookStreamEvents(PSDataStreams streams) + private void HookStreamEvents(PSDataStreams streams) { streams.Debug.DataAdded += DebugOnDataAdded; streams.Error.DataAdded += ErrorOnDataAdded; @@ -103,26 +118,21 @@ public void BeginInvoke(ScriptBlock scriptblock, PSObject inputObject) .AddParameter("_", inputObject) .AddParameter("PSItem", inputObject) .AddParameter("PSParallelIndex", _index) - .AddParameter("PSParallelProgressId", _index+1000); + .AddParameter("PSParallelProgressId", _index + 1000); _powerShell.BeginInvoke(_input, _output); } - internal void Reset() - { - _runspace.ResetRunspaceState(); - } - public void Dispose() { var ps = _powerShell; if (ps != null) { UnhookStreamEvents(ps.Streams); + ps.Runspace?.Dispose(); ps.Dispose(); } _output.Dispose(); - _input.Dispose(); - _runspace.Dispose(); + _input.Dispose(); } private void OutputOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) @@ -140,9 +150,9 @@ private void InformationOnDataAdded(object sender, DataAddedEventArgs dataAddedE private void ProgressOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) { - var record = ((PSDataCollection)sender)[dataAddedEventArgs.Index]; - _percentComplete = record.PercentComplete; - _poolStreams.AddProgress(record, _index); + var psDataCollection = ((PSDataCollection) sender); + var record = psDataCollection[dataAddedEventArgs.Index]; + _poolStreams.AddProgress(record, _index); } private void ErrorOnDataAdded(object sender, DataAddedEventArgs dataAddedEventArgs) diff --git a/src/PSParallel/PowershellPool.cs b/src/PSParallel/PowershellPool.cs index 81aa9ab..0412a26 100644 --- a/src/PSParallel/PowershellPool.cs +++ b/src/PSParallel/PowershellPool.cs @@ -17,6 +17,7 @@ sealed class PowershellPool : IDisposable private int _busyCount; private readonly CancellationToken _cancellationToken; private readonly List _poolMembers; + private readonly InitialSessionState _initialSessionState; private readonly BlockingCollection _availablePoolMembers = new BlockingCollection(new ConcurrentQueue()); public readonly PowerShellPoolStreams Streams = new PowerShellPoolStreams(); private int _processedCount; @@ -24,12 +25,12 @@ sealed class PowershellPool : IDisposable public PowershellPool(int poolSize, InitialSessionState initialSessionState, CancellationToken cancellationToken) { _poolMembers= new List(poolSize); + _initialSessionState = initialSessionState; _cancellationToken = cancellationToken; for (var i = 0; i < poolSize; i++) - { - var runspace = RunspaceFactory.CreateRunspace(initialSessionState); - var powerShellPoolMember = new PowerShellPoolMember(this, i+1, runspace); + { + var powerShellPoolMember = new PowerShellPoolMember(this, i+1, initialSessionState); _poolMembers.Add(powerShellPoolMember); _availablePoolMembers.Add(powerShellPoolMember); } @@ -107,8 +108,7 @@ private bool TryWaitForAvailablePowershell(int milliseconds, out PowerShellPoolM Debug.WriteLine("WaitForAvailablePowershell - TryTake failed"); poolMember = null; return false; - } - poolMember.Reset(); + } return true; } diff --git a/src/PSParallelTests/InvokeParallelTests.cs b/src/PSParallelTests/InvokeParallelTests.cs index e8da425..02b105f 100644 --- a/src/PSParallelTests/InvokeParallelTests.cs +++ b/src/PSParallelTests/InvokeParallelTests.cs @@ -12,7 +12,7 @@ namespace PSParallelTests public sealed class InvokeParallelTests : IDisposable { readonly RunspacePool m_runspacePool; - InitialSessionState _iss; + readonly InitialSessionState _iss; public InvokeParallelTests() { _iss = CreateInitialSessionState(); @@ -63,7 +63,7 @@ public void TestParallelOutput() { using (var ps = PowerShell.Create()) { - ps.RunspacePool = m_runspacePool; + //ps.RunspacePool = m_runspacePool; ps.AddCommand("Invoke-Parallel") .AddParameter("ScriptBlock", ScriptBlock.Create("$_* 2")) @@ -157,7 +157,7 @@ public void TestNoDebugOutputWithoutPreference() var input = new PSDataCollection {1, 2, 3, 4, 5}; input.Complete(); ps.Invoke(input); - var dbg = ps.Streams.Debug.ReadAll(); + var dbg = ps.Streams.Debug.ReadAll(); Assert.IsFalse(dbg.Any(d => d.Message == "1"), "No debug message should be '1'"); } } @@ -299,7 +299,28 @@ public void TestProgressOutput() input.Complete(); ps.Invoke(input); var progress = ps.Streams.Progress.ReadAll(); - Assert.AreEqual(12, progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); + Assert.IsTrue(10 < progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); + } + } + + [TestMethod] + public void TestProgressOutput2Workers() + { + using (var ps = PowerShell.Create()) + { + ps.RunspacePool = m_runspacePool; + ps.AddScript("$ProgressPreference='Continue'", false).Invoke(); + ps.AddStatement() + .AddCommand("Invoke-Parallel", false) + .AddParameter("ScriptBlock", + ScriptBlock.Create("Write-Progress -activity 'Test' -Status 'Status' -currentoperation $_")) + .AddParameter("ThrottleLimit", 2); + + var input = new PSDataCollection { 1, 2, 3, 4, 5, 6, 7,8, 9,10 }; + input.Complete(); + ps.Invoke(input); + var progress = ps.Streams.Progress.ReadAll(); + Assert.IsTrue(19 <= progress.Count(pr => pr.Activity == "Invoke-Parallel" || pr.Activity == "Test")); } } From f89aa13660d5c5f1bb8fed706b0ab7638df0741a Mon Sep 17 00:00:00 2001 From: Sergei Vorobev Date: Thu, 27 Sep 2018 23:41:33 -0700 Subject: [PATCH 37/38] Fix headers in README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c653793..d90a3d0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PSParallel Invoke scriptblocks in parallel runspaces -##Installation +## Installation ```PowerShell Install-Module PSParallel ``` @@ -13,7 +13,7 @@ Install-Module PSParallel Variables and functions are captured from the parent session. -##Throttling +## Throttling To control the degree of parallelism, i.e. the number of concurrent runspaces, use the -ThrottleLimit parameter ```PowerShell @@ -27,9 +27,9 @@ The overhead of spinning up new PowerShell classes is non-zero. Invoke-Parallel ![image](https://github.com/powercode/PSParallel/raw/master/images/Invoke-Parallel.png) -##Contributions +## Contributions Pull requests and/or suggestions are more than welcome. -###Acknowlegementes +### Acknowlegementes The idea and the basis for the implementation comes from [RamblingCookieMonster](https://github.com/RamblingCookieMonster). -Cudos for that implementation also goes to Boe Prox(@proxb) and Sergei Vorobev(@xvorsx). \ No newline at end of file +Cudos for that implementation also goes to Boe Prox(@proxb) and Sergei Vorobev(@xvorsx). From b2e717605df1d3412da513163622de8e30efca3a Mon Sep 17 00:00:00 2001 From: Darius Hutchison Date: Thu, 16 Jul 2020 10:26:20 -0500 Subject: [PATCH 38/38] Fixed typos and updated PS aliases --- README.md | 11 ++++++++--- scripts/Install.ps1 | 2 +- scripts/Publish-ToGallery.ps1 | 2 +- scripts/dbg.ps1 | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d90a3d0..afe6ec8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # PSParallel + Invoke scriptblocks in parallel runspaces ## Installation + ```PowerShell Install-Module PSParallel ``` @@ -14,11 +16,12 @@ Install-Module PSParallel Variables and functions are captured from the parent session. ## Throttling + To control the degree of parallelism, i.e. the number of concurrent runspaces, use the -ThrottleLimit parameter ```PowerShell # process lots of crash dumps -Get-ChildItem -recurce *.dmp | Invoke-Parallel -ThrottleLimit 64 -ProgressActivity "Processing dumps" { +Get-ChildItem -Recurse *.dmp | Invoke-Parallel -ThrottleLimit 64 -ProgressActivity "Processing dumps" { [PSCustomObject] @{ Dump=$_; Analysis = cdb.exe -z $_.fullname -c '"!analyze -v;q"' } ``` @@ -28,8 +31,10 @@ The overhead of spinning up new PowerShell classes is non-zero. Invoke-Parallel ![image](https://github.com/powercode/PSParallel/raw/master/images/Invoke-Parallel.png) ## Contributions + Pull requests and/or suggestions are more than welcome. -### Acknowlegementes +### Acknowledgements + The idea and the basis for the implementation comes from [RamblingCookieMonster](https://github.com/RamblingCookieMonster). -Cudos for that implementation also goes to Boe Prox(@proxb) and Sergei Vorobev(@xvorsx). +Kudos for that implementation also goes to Boe Prox(@proxb) and Sergei Vorobev(@xvorsx). diff --git a/scripts/Install.ps1 b/scripts/Install.ps1 index 3e9bce4..ac7fa11 100644 --- a/scripts/Install.ps1 +++ b/scripts/Install.ps1 @@ -1,4 +1,4 @@ -$manPath = Get-ChildItem -recurse $PSScriptRoot/../module -include *.psd1 | select -first 1 +$manPath = Get-ChildItem -recurse $PSScriptRoot/../module -include *.psd1 | Select-Object -first 1 $man = Test-ModuleManifest $manPath $name = $man.Name diff --git a/scripts/Publish-ToGallery.ps1 b/scripts/Publish-ToGallery.ps1 index aca7d72..8aff037 100644 --- a/scripts/Publish-ToGallery.ps1 +++ b/scripts/Publish-ToGallery.ps1 @@ -1,4 +1,4 @@ -$manPath = Get-ChildItem -recurse $PSScriptRoot/../module -include *.psd1 | select -first 1 +$manPath = Get-ChildItem -recurse $PSScriptRoot/../module -include *.psd1 | Select-Object -first 1 $man = Test-ModuleManifest $manPath $name = $man.Name diff --git a/scripts/dbg.ps1 b/scripts/dbg.ps1 index e341414..ed4c670 100644 --- a/scripts/dbg.ps1 +++ b/scripts/dbg.ps1 @@ -35,7 +35,7 @@ $philosopherData = @( $pd = $philosopherData[($_ -1)% $philosopherData.Count] - 1..100 | foreach { + 1..100 | ForEach-Object { $op = switch($_ % 8) { 0 { 'sleeping' }