├── .gitignore ├── LICENSE ├── README.md ├── Remotely.Tests.ps1 ├── Remotely.psd1 ├── Remotely.psm1 ├── appveyor.yml └── machineConfig.csv /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerShell/Remotely/03731aac12affe686aa6a23f728d5c4d133220b6/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 PowerShell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Synopsis 2 | ============ 3 | Executes a script block against a remote runspace. Remotely can be used with Pester for executing script blocks on a remote system. 4 | 5 | Description 6 | ====================== 7 | The contents on the Remotely block are executed on a remote runspace. The connection information of the runspace is supplied in a CSV file of the format: 8 | 9 | ``` 10 | ComputerName,Username,Password 11 | ComputerName1,Username1,Password1 12 | ComputerName2,Username2,Password2 13 | ``` 14 | 15 | The filename must be `machineConfig.csv`. 16 | 17 | The CSV file is expected to be placed next to this file. 18 | 19 | If the CSV file is not found or username is not specified, the machine name is ignored and runspace to localhost 20 | is created for executing the script block. 21 | 22 | If the password has a ',' then it needs to be escaped by using quotes like: 23 | 24 | ``` 25 | ComputerName,Username,Password 26 | ComputerName1,Username1,Password1 27 | ComputerName2,Username2,"Some,other,password" 28 | ``` 29 | 30 | To get access to the streams, use GetVerbose, GetDebugOutput, GetError, GetProgressOutput, 31 | GetWarning on the resultant object. 32 | 33 | Example 34 | ============ 35 | Usage in Pester: 36 | 37 | ```powershell 38 | Describe "Add-Numbers" { 39 | It "adds positive numbers" { 40 | Remotely { 2 + 3 } | Should Be 5 41 | } 42 | 43 | It "gets verbose message" { 44 | $sum = Remotely { Write-Verbose -Verbose "Test Message" } 45 | $sum.GetVerbose() | Should Be "Test Message" 46 | } 47 | 48 | It "can pass parameters to remote block" { 49 | $num = 10 50 | $process = Remotely { param($number) $number + 1 } -ArgumentList $num 51 | $process | Should Be 11 52 | } 53 | } 54 | ``` 55 | 56 | Links 57 | ============ 58 | * https://github.com/PowerShell/Remotely 59 | * https://github.com/pester/Pester 60 | 61 | Running Tests 62 | ============= 63 | Pester-based tests are located in ```/Remotely.Tests.ps1``` 64 | 65 | * Ensure Pester is installed on the machine 66 | * Run tests: 67 | .\Remotely.Tests.ps1 68 | 69 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 70 | -------------------------------------------------------------------------------- /Remotely.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Function to update the table that defines the machineconfig.csv 2 | # based on test environment variables 3 | Function Update-ConfigContentTable 4 | { 5 | param( 6 | [parameter(Mandatory=$true)] 7 | [HashTable]$configContentTable 8 | 9 | ) 10 | 11 | if($global:AppveyorRemotelyUserName -or $global:AppveyorRemotelyPassword) 12 | { 13 | $computername = $configContentTable.ComputerName 14 | if($computername -ieq 'localhost') 15 | { 16 | $computername = '127.0.0.1' 17 | } 18 | elseif ( $computername -ieq '.') 19 | { 20 | $computername = '::1' 21 | } 22 | 23 | $configContentTable.ComputerName = $computername 24 | } 25 | 26 | if($global:AppveyorRemotelyUserName) 27 | { 28 | $configContentTable['Username']=$global:AppveyorRemotelyUserName 29 | } 30 | 31 | if($global:AppveyorRemotelyPassword) 32 | { 33 | $configContentTable['Password']=$global:AppveyorRemotelyPassword 34 | } 35 | 36 | return $configContentTable 37 | } 38 | 39 | Describe "Add-Numbers" { 40 | BeforeAll { 41 | $configFile = (join-path $PSScriptRoot 'machineConfig.csv') 42 | $configContentTable = @{ComputerName = "localhost" } 43 | Update-ConfigContentTable -configContentTable $configContentTable 44 | $configContent = @([pscustomobject] $configContentTable) | ConvertTo-Csv -NoTypeInformation 45 | $configContent | Out-File -FilePath $configFile -Force 46 | } 47 | $testcases = @( @{NoSessionValue = $true}, @{NoSessionValue = $false}) 48 | 49 | It "can execute script with NoSessionValue : " -TestCases $testcases { 50 | param($NoSessionValue) 51 | 52 | $expectedRemotelyTarget = 'localhost' 53 | if($configContentTable['Computername']) 54 | { 55 | $expectedRemotelyTarget = $configContentTable['Computername'] 56 | } 57 | 58 | $output = Remotely { 1 + 1 } -NoSession:$NoSessionValue 59 | $output | Should Be 2 60 | 61 | if($NoSessionValue -eq $true) 62 | { 63 | $output.RemotelyTarget | Should BeNullOrEmpty 64 | } 65 | else 66 | { 67 | $output.RemotelyTarget | Should Be $expectedRemotelyTarget 68 | } 69 | } 70 | 71 | It "can return an array with NoSessionValue : " -TestCases $testcases { 72 | param($NoSessionValue) 73 | $returnObjs = Remotely { 1..10 } -NoSession:$NoSessionValue 74 | $returnObjs.count | Should Be 10 75 | } 76 | 77 | It "can return a hashtable with NoSessionValue : " -TestCases $testcases { 78 | param($NoSessionValue) 79 | $returnObjs = Remotely { @{Value = 2} } -NoSession:$NoSessionValue 80 | $returnObjs["Value"] | Should Be 2 81 | } 82 | 83 | It "can get verbose message with NoSessionValue : " -TestCases $testcases { 84 | param($NoSessionValue) 85 | $output = Remotely { Write-Verbose -Verbose "Verbose Message" } -NoSession:$NoSessionValue 86 | $output.GetVerbose() | Should Be "Verbose Message" 87 | } 88 | 89 | It "can get error message with NoSessionValue : " -TestCases $testcases { 90 | param($NoSessionValue) 91 | $output = Remotely { Write-Error "Error Message" } -NoSession:$NoSessionValue 92 | $output.GetError() | Should Be "Error Message" 93 | } 94 | 95 | It "can get warning message with NoSessionValue : " -TestCases $testcases { 96 | param($NoSessionValue) 97 | $output = Remotely { Write-Warning "Warning Message" } -NoSession:$NoSessionValue 98 | $output.GetWarning() | Should Be "Warning Message" 99 | } 100 | 101 | It "can get debug message with NoSessionValue : " -TestCases $testcases { 102 | param($NoSessionValue) 103 | $output = Remotely -NoSession:$NoSessionValue { 104 | $originalPreference = $DebugPreference 105 | $DebugPreference = "continue" 106 | Write-Debug "Debug Message" 107 | $DebugPreference = $originalPreference 108 | } 109 | $output.GetDebugOutput() | Should Be "Debug Message" 110 | } 111 | 112 | It "can get progress message with NoSessionValue : " -TestCases $testcases { 113 | param($NoSessionValue) 114 | $output = Remotely -NoSession:$NoSessionValue { Write-Progress -Activity "Test" -Status "Testing" -Id 1 -PercentComplete 100 -SecondsRemaining 0 } 115 | $output.GetProgressOutput().Activity | Should Be "Test" 116 | $output.GetProgressOutput().StatusDescription | Should Be "Testing" 117 | $output.GetProgressOutput().ActivityId | Should Be 1 118 | } 119 | 120 | It 'can return $false as a value with NoSessionValue : ' -TestCases $testcases { 121 | param($NoSessionValue) 122 | $output = Remotely { $false } -NoSession:$NoSessionValue 123 | $output | Should Be $false 124 | } 125 | 126 | It 'can return throw messages with NoSessionValue : ' -TestCases $testcases { 127 | param($NoSessionValue) 128 | $output = Remotely { throw 'bad' } -NoSession:$NoSessionValue 129 | $output.GetError().FullyQualifiedErrorId | Should Be 'bad' 130 | } 131 | 132 | It "can pass parameters to remote block with NoSessionValue : " -TestCases $testcases { 133 | param($NoSessionValue) 134 | $num = 10 135 | $process = Remotely { param($number) $number + 1 } -ArgumentList $num -NoSession:$NoSessionValue 136 | $process | Should Be 11 137 | } 138 | 139 | It "can get remote sessions" { 140 | Remotely { 1 + 1 } | Should Be 2 141 | $remoteSessions = Get-RemoteSession 142 | 143 | $remoteSessions | % { $remoteSessions.Name -match "Remotely" | Should Be $true} 144 | } 145 | 146 | It "can get target of the remotely block" { 147 | $expectedRemotelyTarget = 'localhost' 148 | if($configContentTable['Computername']) 149 | { 150 | $expectedRemotelyTarget = $configContentTable['Computername'] 151 | } 152 | 153 | $output = Remotely { 1 } 154 | $output.RemotelyTarget | Should Be $expectedRemotelyTarget 155 | } 156 | 157 | It "can handle delete sessions" { 158 | Remotely { 1 + 1 } | Should Be 2 159 | $previousSession = Get-RemoteSession 160 | $previousSession | Remove-PSSession 161 | 162 | ##New session should be created 163 | Remotely { 1 + 1 } | Should Be 2 164 | $newSession = Get-RemoteSession 165 | $previousSession.Name | Should Not Be $newSession.Name 166 | } 167 | 168 | It "can execute against more than 1 remote machines" { 169 | # Testing with no configuration name for compatibility 170 | $configFile = (join-path $PSScriptRoot 'machineConfig.csv') 171 | $configContentTable = @{ComputerName = "localhost" } 172 | Update-ConfigContentTable -configContentTable $configContentTable 173 | $configContentTable2 = @{ComputerName = "." } 174 | Update-ConfigContentTable -configContentTable $configContentTable2 175 | $configContent = @([pscustomobject] $configContentTable, [pscustomobject] $configContentTable2) | ConvertTo-Csv -NoTypeInformation 176 | $configContent | Out-File -FilePath $configFile -Force 177 | 178 | try 179 | { 180 | $results = Remotely { 1 + 1 } 181 | $results.Count | Should Be 2 182 | 183 | foreach($result in $results) 184 | { 185 | $result | Should Be 2 186 | } 187 | } 188 | catch 189 | { 190 | $_.FullyQualifiedErrorId | Should Be $null 191 | } 192 | finally 193 | { 194 | Remove-Item $configFile -ErrorAction SilentlyContinue -Force 195 | } 196 | } 197 | } 198 | 199 | Describe "ConfigurationName" { 200 | BeforeAll { 201 | $configFile = (join-path $PSScriptRoot 'machineConfig.csv') 202 | } 203 | AfterAll { 204 | Remove-Item $configFile -ErrorAction SilentlyContinue -Force 205 | } 206 | Context "Default configuration name" { 207 | $configContentTable = @{ 208 | ComputerName = "localhost" 209 | Username = $null 210 | Password = $null 211 | ConfigurationName = "Microsoft.PowerShell" 212 | } 213 | Update-ConfigContentTable -configContentTable $configContentTable 214 | $configContent = @([pscustomobject] $configContentTable) | ConvertTo-Csv -NoTypeInformation 215 | $configContent | Out-File -FilePath $configFile -Force 216 | 217 | it "Should connect when a configurationName is specified" { 218 | 219 | $results = Remotely { 1 + 1 } 220 | 221 | $results | Should Be 2 222 | } 223 | } 224 | 225 | Context "Invalid configuration name" { 226 | 227 | Write-Verbose "Clearing remote session..." -Verbose 228 | Clear-RemoteSession 229 | $configContentTable = @{ 230 | ComputerName = "localhost" 231 | Username = $null 232 | Password = $null 233 | ConfigurationName = "Microsoft.PowerShell2" 234 | } 235 | Update-ConfigContentTable -configContentTable $configContentTable 236 | $configContent = @([pscustomobject] $configContentTable) | ConvertTo-Csv -NoTypeInformation 237 | $configContent | Out-File -FilePath $configFile -Force 238 | 239 | $expectedRemotelyTarget = 'localhost' 240 | if($configContentTable['Computername']) 241 | { 242 | $expectedRemotelyTarget = $configContentTable['Computername'] 243 | } 244 | 245 | 246 | it "Should not connect to an invalid ConfigurationName" { 247 | {$results = Remotely { 1 + 1 }} | should throw "Connecting to remote server $expectedRemotelyTarget failed with the following error message : The WS-Management service cannot process the request. Cannot find the Microsoft.PowerShell2 session configuration in the WSMan: drive on the $expectedRemotelyTarget computer. For more information, see the about_Remote_Troubleshooting Help topic." 248 | } 249 | } 250 | } 251 | Describe "Clear-RemoteSession" { 252 | It "can clear remote sessions" { 253 | Clear-RemoteSession 254 | Get-PSSession -Name Remotely* | Should Be $null 255 | } 256 | } -------------------------------------------------------------------------------- /Remotely.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script module or binary module file associated with this manifest. 4 | ModuleToProcess = 'Remotely.psm1' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '1.0.0' 8 | 9 | # ID used to uniquely identify this module 10 | GUID = 'a20c9efd-077e-4c53-a500-f74dec071799' 11 | 12 | # Author of this module 13 | Author = 'Microsoft Corporation' 14 | 15 | # Company or vendor of this module 16 | CompanyName = 'Microsoft Corporation' 17 | 18 | # Copyright statement for this module 19 | Copyright = '(c) 2015 Microsoft Corporation. All rights reserved.' 20 | 21 | # Description of the functionality provided by this module 22 | Description = 'Remotely provides a framework to execute script blocks on a remote machine using PowerShell remoting.' 23 | 24 | # Minimum version of the Windows PowerShell engine required by this module 25 | PowerShellVersion = '2.0' 26 | 27 | # Functions to export from this module 28 | FunctionsToExport = @('Remotely', 'Clear-RemoteSession', 'Get-RemoteSession') 29 | 30 | # Cmdlets to export from this module 31 | CmdletsToExport = '*' 32 | 33 | # Variables to export from this module 34 | VariablesToExport = '*' 35 | 36 | # Aliases to export from this module 37 | AliasesToExport = '*' 38 | 39 | # List of all modules packaged with this module 40 | # ModuleList = @() 41 | 42 | # List of all files packaged with this module 43 | # FileList = @() 44 | 45 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 46 | PrivateData = @{ 47 | 48 | PSData = @{ 49 | 50 | # Tags applied to this module. These help with module discovery in online galleries. 51 | # Tags = @() 52 | 53 | # A URL to the license for this module. 54 | # LicenseUri = '' 55 | 56 | # A URL to the main website for this project. 57 | # ProjectUri = '' 58 | 59 | # A URL to an icon representing this module. 60 | # IconUri = '' 61 | 62 | # ReleaseNotes of this module 63 | # ReleaseNotes = '' 64 | 65 | } # End of PSData hashtable 66 | 67 | } # End of PrivateData hashtable 68 | 69 | # HelpInfo URI of this module 70 | # HelpInfoURI = '' 71 | 72 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 73 | # DefaultCommandPrefix = '' 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Remotely.psm1: -------------------------------------------------------------------------------- 1 | function Remotely { 2 | <# 3 | .SYNOPSIS 4 | Executes a script block against a remote runspace. Remotely can be used with Pester for executing script blocks on remote system. 5 | 6 | .DESCRIPTION 7 | The contents on the Remotely block are executed on a remote runspace. The connection information of the runspace is supplied in a CSV file of the format: 8 | 9 | ComputerName,Username,Password 10 | machinename,user,password 11 | 12 | The file name must be machineConfig.csv 13 | 14 | The CSV file is expected to be placed next to this file. 15 | 16 | If the CSV file is not found or username is not specified, the machine name is ignored and runspace to localhost 17 | is created for executing the script block. 18 | 19 | If the ConfigurationName is not in the CSV file, Remotely will default to Microsoft.PowerShell. 20 | 21 | If the password has a ',' then it needs to be escaped by using quotes like: 22 | ComputerName,Username,Password,ConfigurationName 23 | machinename,user,"Some,password",Microsoft.Powershell 24 | 25 | To get access to the streams GetVerbose, GetDebugOutput, GetError, GetProgressOutput, GetWarning can be used on the resultant object. 26 | 27 | .PARAMETER Test 28 | The script block that should throw an exception if the expectation of the test is not met. 29 | 30 | .PARAMETER ArgumentList 31 | Arguments that will be passed to the script block. 32 | 33 | .PARAMETER NoSession 34 | The switch opts to use the script block without using any powershell sessions, but local runspace. 35 | 36 | .EXAMPLE 37 | 38 | Describe "Add-Numbers" { 39 | It "adds positive numbers" { 40 | Remotely { 2 + 3 } | Should Be 5 41 | } 42 | 43 | It "gets verbose message" { 44 | $sum = Remotely { Write-Verbose -Verbose "Test Message" } 45 | $sum.GetVerbose() | Should Be "Test Message" 46 | } 47 | 48 | It "can pass parameters to remote block" { 49 | $num = 10 50 | $process = Remotely { param($number) $number + 1 } -ArgumentList $num 51 | $process | Should Be 11 52 | } 53 | 54 | It "adds positive numbers with NoSession" { 55 | Remotely { 2 + 3 } -NoSession | Should Be 5 56 | } 57 | } 58 | 59 | .LINK 60 | https://github.com/PowerShell/Remotely 61 | https://github.com/pester/Pester 62 | #> 63 | 64 | param( 65 | [Parameter(Mandatory = $true, Position = 0)] 66 | [ScriptBlock] $test = {}, 67 | 68 | [Parameter(Mandatory = $false, Position = 1)] 69 | $ArgumentList, 70 | 71 | [Parameter(Mandatory = $false, Position =2)] 72 | [switch]$NoSession 73 | ) 74 | 75 | $results = @() 76 | 77 | if(-not $NoSession.IsPresent) 78 | { 79 | if($script:sessionsHashTable -eq $null) 80 | { 81 | $script:sessionsHashTable = @{} 82 | } 83 | 84 | $machineConfigFile = Join-Path $PSScriptRoot "machineConfig.CSV" 85 | 86 | CreateSessions -machineConfigFile $machineConfigFile 87 | 88 | $sessions = @() 89 | 90 | foreach($sessionInfo in $script:sessionsHashTable.Values.GetEnumerator()) 91 | { 92 | CheckAndReConnect -sessionInfo $sessionInfo 93 | $sessions += $sessionInfo.Session 94 | } 95 | 96 | if($sessions.Count -le 0) 97 | { 98 | throw "No sessions are available" 99 | } 100 | 101 | $testjob = Invoke-Command -Session $sessions -ScriptBlock $test -AsJob -ArgumentList $ArgumentList | Wait-Job 102 | 103 | foreach($childJob in $testjob.ChildJobs) 104 | { 105 | $outputStream = ConstructOutputStream -resultObj $childJob.Output -streamSource $childJob 106 | 107 | if($childJob.State -eq 'Failed') 108 | { 109 | $childJob | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable jobError 110 | $outputStream.__Streams.Error = $jobError 111 | } 112 | 113 | $results += ,$outputStream 114 | } 115 | 116 | $testjob | Remove-Job -Force 117 | } 118 | else 119 | { 120 | $ps = [Powershell]::Create() 121 | $ps.runspace = [runspacefactory]::CreateRunspace() 122 | $ps.runspace.open() 123 | 124 | try 125 | { 126 | $res = $ps.AddScript($test.ToString()).AddArgument($ArgumentList).Invoke() 127 | } 128 | catch 129 | { 130 | $executionError = $_.Exception.InnerException.ErrorRecord 131 | } 132 | 133 | $outputStream = ConstructOutputStream -resultObj $res -streamSource $ps.Streams 134 | 135 | if(($ps.Streams.Error.Count -eq 0) -and ($ps.HadErrors)) 136 | { 137 | $outputStream.__streams.Error = $executionError; 138 | } 139 | 140 | $results += ,$outputStream 141 | 142 | $ps.Dispose() 143 | } 144 | 145 | $results 146 | } 147 | 148 | function ConstructOutputStream 149 | { 150 | param( 151 | $resultObj, 152 | $streamSource 153 | ) 154 | 155 | if($resultObj.Count -eq 0) 156 | { 157 | [object] $outputStream = New-Object psobject 158 | } 159 | else 160 | { 161 | [object] $outputStream = $resultObj | % { $_ } 162 | } 163 | 164 | $errorStream = CopyStreams $streamSource.Error 165 | $verboseStream = CopyStreams $streamSource.Verbose 166 | $debugStream = CopyStreams $streamSource.Debug 167 | $warningStream = CopyStreams $streamSource.Warning 168 | $progressStream = CopyStreams $streamSource.Progress 169 | 170 | $allStreams = @{ 171 | Error = $errorStream 172 | Verbose = $verboseStream 173 | DebugOutput = $debugStream 174 | Warning = $warningStream 175 | ProgressOutput = $progressStream 176 | } 177 | 178 | $outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType NoteProperty -Name __Streams -Value $allStreams -Force 179 | $outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetError -Value { return $this.__Streams.Error } -Force 180 | $outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetVerbose -Value { return $this.__Streams.Verbose } -Force 181 | $outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetDebugOutput -Value { return $this.__Streams.DebugOutput } -Force 182 | $outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetProgressOutput -Value { return $this.__Streams.ProgressOutput } -Force 183 | $outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetWarning -Value { return $this.__Streams.Warning } -Force 184 | $outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType NoteProperty -Name RemotelyTarget -Value $streamSource.Location -Force 185 | return $outputStream 186 | } 187 | 188 | function CopyStreams 189 | { 190 | param( [Parameter(Position=0, Mandatory=$true)] $inputStream) 191 | 192 | $outStream = New-Object 'System.Management.Automation.PSDataCollection[PSObject]' 193 | 194 | foreach($item in $inputStream) 195 | { 196 | $outStream.Add($item) 197 | } 198 | 199 | $outStream.Complete() 200 | 201 | ,$outStream 202 | } 203 | 204 | function CreateSessions 205 | { 206 | param([string] $machineConfigFile) 207 | 208 | if(Test-Path $machineConfigFile) 209 | { 210 | Write-Verbose "Found machine configuration file: $machineConfigFile" 211 | 212 | $machineInfo = Import-Csv $machineConfigFile 213 | 214 | foreach($item in $machineInfo) 215 | { 216 | $configurationName = 'Microsoft.PowerShell' 217 | if($item.ConfigurationName) 218 | { 219 | $configurationName = $item.ConfigurationName 220 | } 221 | 222 | if([String]::IsNullOrEmpty($item.UserName)) 223 | { 224 | Write-Verbose "No username specified. Creating session to localhost." 225 | CreateLocalSession $item.ComputerName -configurationName $configurationName 226 | } 227 | else 228 | { 229 | $password = ConvertTo-SecureString -String $item.Password -AsPlainText -Force 230 | $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $item.Username,$password 231 | 232 | if(-not $script:sessionsHashTable.ContainsKey($item.ComputerName)) 233 | { 234 | $sessionName = "Remotely" + (Get-Random).ToString() 235 | 236 | Write-Verbose "Creating new session, computer: $($item.ComputerName); ConfigurationName: $($ConfigurationName) " 237 | $sessionInfo = CreateSessionInfo -Session (New-PSSession -ComputerName $item.ComputerName -Credential $cred -Name $sessionName -configurationname $configurationName -ErrorAction Stop) -Credential $cred 238 | $script:sessionsHashTable.Add($sessionInfo.session.ComputerName, $sessionInfo) 239 | } 240 | } 241 | } 242 | } 243 | else 244 | { 245 | Write-Verbose "No machine configuration file found. Creating session to localhost." 246 | CreateLocalSession 247 | } 248 | } 249 | 250 | function CreateLocalSession 251 | { 252 | param( 253 | [Parameter(Position=0)] $machineName = 'localhost', 254 | $configurationName = 'Microsoft.PowerShell' 255 | ) 256 | 257 | if(-not $script:sessionsHashTable.ContainsKey($machineName)) 258 | { 259 | $sessionName = "Remotely" + (Get-Random).ToString() 260 | 261 | Write-Verbose "Creating new local session, ConfigurationName: $($ConfigurationName) " 262 | $sessionInfo = CreateSessionInfo -Session (New-PSSession -ComputerName $machineName -Name $sessionName -ConfigurationName $configurationName -ErrorAction Stop) 263 | 264 | $script:sessionsHashTable.Add($machineName, $sessionInfo) 265 | } 266 | } 267 | 268 | function CreateSessionInfo 269 | { 270 | param( 271 | [Parameter(Position=0, Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Runspaces.PSSession] $Session, 272 | [System.Management.Automation.PSCredential] $Credential 273 | ) 274 | 275 | return [PSCustomObject] @{ Session = $Session; Credential = $Credential; ConfigurationName=$Session.ConfigurationName } 276 | } 277 | 278 | function CheckAndReconnect 279 | { 280 | param([Parameter(Position=0, Mandatory=$true)] [ValidateNotNullOrEmpty()] $sessionInfo) 281 | 282 | if($sessionInfo.Session.State -ne [System.Management.Automation.Runspaces.RunspaceState]::Opened) 283 | { 284 | Write-Verbose "Unexpected session state: $sessionInfo.Session.State for machine $($sessionInfo.Session.ComputerName). Re-creating session" 285 | 286 | if($sessionInfo.Session.ComputerName -ne "localhost") 287 | { 288 | $sessionInfo.Session = New-PSSession -ComputerName $sessionInfo.Session.ComputerName -Credential $sessionInfo.Credential -configurationname $sessionInfo.ConfigurationName 289 | } 290 | else 291 | { 292 | Write-Verbose "Creating local session with configurationname:$sessionInfo.ConfigurationName" 293 | $sessionInfo.Session = New-PSSession -ComputerName 'localhost' -configurationname $sessionInfo.ConfigurationName 294 | } 295 | } 296 | } 297 | 298 | function Clear-RemoteSession 299 | { 300 | foreach($sessionInfo in $script:sessionsHashTable.Values.GetEnumerator()) 301 | { 302 | Remove-PSSession $sessionInfo.Session 303 | } 304 | 305 | $script:sessionsHashTable.Clear() 306 | } 307 | 308 | function Get-RemoteSession 309 | { 310 | $sessions = @() 311 | foreach($sessionInfo in $script:sessionsHashTable.Values.GetEnumerator()) 312 | { 313 | $sessions += $sessionInfo.Session 314 | } 315 | 316 | $sessions 317 | } 318 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | #---------------------------------# 2 | # environment configuration # 3 | #---------------------------------# 4 | version: 1.0.0.{build} 5 | install: 6 | - ps: | 7 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force 8 | Install-Module -Name Pester -Repository PSGallery -Force 9 | # generate password 10 | $randomObj = New-Object System.Random 11 | $password = "" 12 | 1..(Get-Random -Minimum 15 -Maximum 126) | ForEach { $password = $password + [char]$randomObj.next(45,126) } 13 | # change password 14 | $objUser = [ADSI]("WinNT://$($env:computername)/appveyor") 15 | $objUser.SetPassword($password) 16 | $global:AppveyorRemotelyUserName = 'appveyor' 17 | $global:AppveyorRemotelyPassword = $password 18 | 19 | #---------------------------------# 20 | # build configuration # 21 | #---------------------------------# 22 | 23 | build: false 24 | 25 | #---------------------------------# 26 | # test configuration # 27 | #---------------------------------# 28 | 29 | test_script: 30 | - ps: | 31 | Import-Module .\Remotely.psd1 32 | $testResultsFile = ".\TestsResults.xml" 33 | $res = Invoke-Pester -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru 34 | (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFile)) 35 | if ($res.FailedCount -gt 0) { 36 | throw "$($res.FailedCount) tests failed." 37 | } 38 | #---------------------------------# 39 | # deployment configuration # 40 | #---------------------------------# 41 | 42 | # scripts to run before deployment 43 | deploy_script: 44 | - ps: | 45 | # Creating project artifact 46 | $stagingDirectory = (Resolve-Path ..).Path 47 | $manifest = Join-Path $pwd "Remotely.psd1" 48 | #(Get-Content $manifest -Raw).Replace("1.0.0", $env:APPVEYOR_BUILD_VERSION) | Out-File $manifest 49 | $zipFilePath = Join-Path $stagingDirectory "$(Split-Path $pwd -Leaf).zip" 50 | Add-Type -assemblyname System.IO.Compression.FileSystem 51 | [System.IO.Compression.ZipFile]::CreateFromDirectory($pwd, $zipFilePath) 52 | Push-AppveyorArtifact $zipFilePath 53 | -------------------------------------------------------------------------------- /machineConfig.csv: -------------------------------------------------------------------------------- 1 | ComputerName,Username,Password,ConfigurationName 2 | localhost --------------------------------------------------------------------------------