The title says it all i have a Start-Job with a script block that instead of executing the commands it output the info about the job.
The order in which this gets executed is the following
$location = $(Get-Location).toString()
$oldModulePath = $env:PSModulePath
$env:PSModulePath = $env:PSModulePath + ";" + $location + "\LongitudePowershellModules"
$env:PSModulePath
$configXML = [xml](Get-Content $location"\InstallationConfig.XML")
$config = $configXML.root
Import-Module CreateComponentsFolder -PassThru
Import-Module CreateTransferFolder -PassThru
Import-Module ConfigureDB -PassThru
Import-Module FastProxyTools -PassThru
Import-Module WspTools -PassThru
Import-Module CopySharepointPowershellXML -PassThru
Import-Module SearchCenterTools -PassThru
Import-Module ConfigureFAST -PassThru
# 1 - CreateComponentsFolder
CreateLongitudeComponentsFolder -currentLocation $location
And the module with the start-job
function CreateLongitudeComponentsFolder
{
Param(
[Parameter(Mandatory=$True, Position=1)]
[string]$currentLocation
)
$scriptBlock = {Write-Host "Copying files to '"C:\Program Files\Ba-insight\Longitude Fast Component"'"`
Copy-Item $currentLocation"\Longitude Fast Component" "C:\Program Files\Ba-insight\Longitude Fast Component" -recurs`
}
Start-Job -ScriptBlock $scriptBlock -ArgumentList $currentLocation -Name "CopyingFiles"
$scriptBlock = {$fileAndFolderList = Get-ChildItem $currentLocation"\Longitude Fast Component" -recurs
$targetLocationFileAndFolderList = Get-ChildItem "C:\Program Files\Ba-insight\Longitude Fast Component" -recurs
$timer = new-object System.Timers.Timer
$timer.Interval = 500 #0.5 sec
$timer.AutoReset = $true
$itemsPresent = $fileAndFolderList | Measure-Object
$action = {foreach($item in $fileAndFolderList)`
{`
if($itemsPresent -ne 0)`
{`
if($targetLocationFileAndFolderList.IndexOf($item) -ne -1)`
{`
$itemsPresent = $itemsPresent - 1`
}`
else`
{`
$itemsPresent = $fileAndFolderList | Measure-Object`
}`
}`
else`
{`
$timer.stop()`
}`
}`
}
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $action | Out-Null
$timer.Enabled = $true
}
Start-Job -ScriptBlock $scriptBlock -ArgumentList $currentLocation -Name "CheckingFiles"
Wait-Job -Name "CheckingFiles"
Write-Host "All files have been copied."
}
I don't get any errors but instead of executing the commands it writes the following for both start-jobs
HasMoreData : True
StatusMessage :
Location : localhost
Command : Write-Host "Copying files to '"C:\Program Files\Ba-insight\Longitude Fast Component"'"`
Copy-Item $currentLocation"\Longitude Fast Component" "C:\Program Files\Ba-insight\Longitud
e Fast Component" -recurs`
JobStateInfo : Running
Finished : System.Threading.ManualResetEvent
InstanceId : 70ab6414-0ca4-467e-b283-20057e4141ad
Id : 1
Name : CopyingFiles
ChildJobs : {Job2}
Output : {}
Error : {}
Progress : {}
Verbose : {}
Debug : {}
Warning : {}
State : Running
It probably has to do with how i wrote the script blocks but i cannot figure out what is different from all the other examples I've seen.
Also is it possible to have a start-job wait for user input using the read-host command?
EDIT: I found the problem with why the job was not executing. The parameter was not being passed correctly. Here is a link for where i found the solution.
Passing parameters to start-job
The last example works and you have to do it like that even for a single parameter.
I still do have one question. The stuff i wrote above still gets outputed to the console. Is there any way to suppress unwanted output from the start-job cmdlet?
I think you need to include the parameters in the script block so that you can pass that down to the job using the argumentlist parameter, as it stands I believe $currentLocation will be null in the script block.
So you need something like this in your script blocks:
$scriptBlock = {
param([string]$currentLocation)
$source - Join-Path -Path $currentLocation -ChildPath "Longitude Fast Component"
$destination = "C:\Program Files\Ba-insight\Longitude Fast Component"
Write-Host "Copying files to [$destination]"
Copy-Item $source $destination -recurse
}
Related
Is there a way to "invoke-command" to a remote computer such that I can reboot my computer and the job will still be running, and I can check the output log whenever I want?
PS> invoke-command -Computer Remote1 -File "ScriptThatRunsFor7days.ps1"
PS> restart-computer
PS> # Hey where's my job on remote computer? Can i see it running and connect to
# its output after rebooting my computer?
Isn't it easier to just register a scheduled task that runs the script on the remote computer?
For logging just use the cmdlet Start-Transcript at the top of the script.
I made a script not to long ago to easely register scheduled tasks on a remote computer. Maybe you can try out and see if it works for you?
[CmdletBinding()]
param(
[parameter(Mandatory=$true)]
[string]
$PSFilePath,
[parameter(Mandatory=$true)]
[string]
$TaskName,
[parameter(Mandatory=$true)]
[string]
$ComputerName
)
$VerbosePreference="Continue"
New-Variable -Name ScriptDestinationFolder -Value "Windows\PowershellScripts" -Option Constant -Scope Script
New-Variable -Name ScriptSourcePath -Value $PSFilePath -Option Constant -Scope Script
Write-Verbose "Script sourcepath: $ScriptSourcePath"
New-Variable -Name PSTaskName -Value $TaskName -Option Constant -Scope Script
Write-Verbose "TaskName: $TaskName"
$File = Split-Path $ScriptSourcePath -leaf
Write-Verbose "Filename: $File"
New-Variable -Name PSFileName -Value $File -Option Constant -Scope Script
Write-Verbose "PSFileName: $PSFileName"
$ExecutionTime = New-TimeSpan -Hours 8
Write-Verbose "Execution time: $ExecutionTime hours"
Invoke-Command -ComputerName $ComputerName -ScriptBlock {
$VerbosePreference="Continue"
#Removing old Scheduled Task
Write-Verbose "Unregistering old scheduled task.."
Stop-ScheduledTask -TaskName $Using:PSTaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $Using:PSTaskName -Confirm:$false -ErrorAction SilentlyContinue
#Creating destination directory for Powershell script
$PSFolderPath = "C:" , $Using:ScriptDestinationFolder -join "\"
Write-Verbose "Creating folder for script file on client: $PSFolderPath"
New-Item -Path $PSFolderPath -ItemType Directory -Force
#Scheduled Task definitions
$Trigger = New-ScheduledTaskTrigger -Daily -At "8am"
$PSFilePath = "C:", $Using:ScriptDestinationFolder , $Using:PSFileName -join "\"
Write-Verbose "Setting path for script file to destination folder on client: $PSFilePath"
$Action = New-ScheduledTaskAction -Execute PowerShell -Argument "-File $PSFilePath"
$Principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType S4U
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -ExecutionTimeLimit $Using:ExecutionTime -StartWhenAvailable
$Task = Register-ScheduledTask -TaskName $Using:PSTaskName -Principal $Principal -Action $Action -Settings $Settings -Trigger $Trigger
$Task = Get-ScheduledTask -TaskName $Using:PSTaskName
$Task.Triggers[0].EndBoundary = [DateTime]::Now.AddDays(90).ToString("yyyyMMdd'T'HH:mm:ssZ")
Write-Verbose "Trigger expiration date set to: $Task.Triggers[0].EndBoundary"
$Task.Settings.DeleteExpiredTaskAfter = 'P1D'
Write-Verbose "Scheduled task will be deleted after $Task.Settings.DeleteExpiredTaskAfter after expiry."
$Task | Set-ScheduledTask -ErrorAction SilentlyContinue
} #End Invoke-Command
#Copy script file from source to the computer
$ScriptDestination = "\" , $ComputerName , "C$", $ScriptDestinationFolder -join "\"
Write-Verbose "Script destination is set to: $ScriptDestination"
Write-Verbose "Copying script file: `"$ScriptSourcePath`" to `"$ScriptDestination`""
Copy-Item -Path $ScriptSourcePath -Destination $ScriptDestination -Force
Usage:
Create-ScheduledTask-Test.ps1 -ComputerName MyRemoteComputer -PSFilePath "ScriptToRun.ps1" -TaskName DoSomeWork
Something with scheduled jobs. I'm copying the script to the remote computer using a pssession.
$s = new-pssession remote1
copy-item script.ps1 c:\users\admin\documents -tosession $s
invoke-command $s { Register-ScheduledJob test script.ps1 -Runnow }
And then later, only when it starts running, it will automatically appear as a regular job on the remote computer:
invoke-command remote1 { get-job | receive-job -keep }
vonPryz provided the crucial pointer:
On Windows, PowerShell offers disconnected remote sessions that allow you to reconnect and collect output later, from any client session, even after a logoff or reboot - assuming that the disconnected session on the remote computer hasn't timed out.
See the conceptual about_Remote_Disconnected_Sessions help topic.
The following sample script demonstrates the approach:
Save it to a *.ps1 file and adapt the $computerName and $sessionName variable values.
The script assumes that the current user identity can be used as-is to remote into the target computer; if that is not the case, add a -Credential argument to the Invoke-Command and Get-PSSession calls.
Invoke the script and, when prompted, choose when to connect to the disconnected remote session that was created - including after a logoff / reboot, in which case the script is automatically reinvoked in order to connect to the disconnected session and retrieve its output.
See the source-code comments for details, particularly with respect to the idle timeout.
One aspect not covered below is output buffering: a disconnected session that runs for a long time without having its output retrieved can potentially accumulate a lot of output. By default, if the output buffer fills up, execution is suspended. The OutputBufferingMode session option controls the behavior - see the New-PSSessionOption cmdlet.
The gist of the solution is:
An Invoke-Command call with the -InDisconnectedSession switch that starts an operation on a remote computer in an instantly disconnected session. That is, the call returns as soon as the operation was started without returning any results from the operation yet (it returns information about the disconnected session instead).
A later Receive-PSSession call - which may happen after a reboot - implicitly connects to the disconnected session and retrieves the results of the operation.
$ErrorActionPreference = 'Stop'
# ADAPT THESE VALUES AS NEEDED
$computer = '???' # The remote target computer's name.
$sessionName = 'ReconnectMe' # A session name of your choice.
# See if the target session already exists.
$havePreviousSession = Get-PSSession -ComputerName $computer -Name $sessionName
if (-not $havePreviousSession) {
# Create a disconnected session with a distinct custom name
# with a command that runs an output loop indefinitely.
# The command returns instantly and outputs a session-information object
# for the disconnected session.
# Note that [int]::MaxValue is used to get the maximum idle timeout,
# but the effective value is capped by the value of the remote machine's
# MaxIdleTimeoutMs WSMan configuration item, which defaults to 12 hours.
Write-Verbose -vb "Creating a disconnected session named $sessionName on computer $computer..."
$disconnectedSession =
Invoke-Command -ComputerName $computer -SessionName $sessionName -InDisconnectedSession -SessionOption #{ IdleTimeout=[int]::MaxValue } { while ($true) { Write-Host -NoNewLine .; Start-Sleep 1 } }
# Prompt the user for when to connect and retrieve the output
# from the disconnected session.
do {
$response = Read-Host #"
---
Disconnected session $sessionName created on computer $computer.
You can connect to it and retrieve its output from any session on this machine,
even after a reboot.
* If you choose to log off or reboot now, this script re-runs automatically
when you log back in, in order to connect to the remote session and collect its output.
* To see open sessions on the target computer on demand, run the following
(append | Remove-PSSession to remove them):
Get-PSSession -ComputerName $computer
---
Do you want to (L)og off, (R)eboot, (C)onnect right now, or (Q)uit (submit with ENTER)? [l/r/c/q]
"#
} while (($response = $response.Trim()) -notin 'l', 'r', 'c', 'q')
$autoRelaunchCmd = {
Set-ItemProperty registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce 'ReconnectDemo' "$((Get-Command powershell.exe).Path) -noexit -command `"Set-Location '$PWD'; . '$PSCommandPath'`""
}
switch ($response) {
'q' { Write-Verbose -vb 'Aborted.'; exit 2 }
'c' { break } # resume below
'l' {
Write-Verbose -vb "Logging off..."
& $autoRelaunchCmd
logoff.exe
exit
}
'r' {
Write-Verbose -vb "Rebooting..."
& $autoRelaunchCmd
Restart-Computer
exit
}
}
}
# Getting here means that a remote disconnection session was previously created.
# Reconnect and retrieve its output.
# Implicitly reconnect to the session by name,
# and receive a job object representing the remotely running command.
# Note: Despite what the docs say, -OutTarget Job seems to be the default.
# Use -Output Host to directly output the results of the running command.
Write-Verbose -vb "Connecting to previously created session $sessionName on computer $computer and receiving its output..."
$job = Receive-PSSession -ComputerName $computer -Name $sessionName -OutTarget Job
# Get the output from the job, timing out after a few seconds.
$job | Wait-Job -Timeout 3
$job | Remove-Job -Force # Forcefully terminate the job with the indefinitely running command.
# Remove the session.
Write-Host
Write-Verbose -Verbose "Removing remote session..."
Get-PSSession -ComputerName $computer -Name $sessionName | Remove-PSSession
function remote_nohup {
param(
[string]$Machine,
[string]$Cmd
)
$job_tstamp = $(get-date -f MMdd_HHmm_ss)
$job_name = "${job_tstamp}"
$job_dir_start = (Get-Location).Path
$job_dir_sched = "$env:userprofile/Documents/jobs"
$job_file = "${job_dir_sched}/${job_name}.run.ps1"
$job_log = "${job_dir_sched}/${job_name}.log"
$job_computer = $Machine
$job_cmd = $Cmd
# Create Job File
$job_ps1 = #(
"`$ErrorActionPreference = `"Stop`""
""
"Start-Transcript -path $job_log -append"
""
"try {"
" write-host 'job_begin:($job_name)'"
" "
" set-location $job_dir_start -EA 0"
" "
" write-host 'job_cmd:($job_cmd)'"
" $job_cmd | out-host"
""
" write-host 'job_end:($job_name)'"
"}"
"catch {"
" `$msg = `$_"
" write-host `$msg"
" write-error `$msg"
"}"
"finally {"
" Stop-Transcript"
"}"
""
"Exit-PSSession"
)
try {
New-Item -ItemType Directory -Force -EA:0 -Path $job_dir_sched | out-null
copy-Item $remote_profile_ps1 $job_profile
write-host "Creating File: $job_file"
$f1 = open_w $job_file -fatal
foreach ($line in $job_ps1) {
$f1.WriteLine($line)
}
}
finally {
$f1.close()
}
# Create Jobs Dir
write-host "Creating remote job Directory"
Invoke-Command -Computer $job_computer -ScriptBlock {
New-Item -ItemType Directory -Force -EA:0 -Path $using:job_dir_sched | out-null
}
# Copy Job File
write-host "copy-Item -recurse -ToSession `$s2 $job_file $job_file"
$s2 = New-PSSession -Computer $job_computer
copy-Item -ToSession $s2 $job_file $job_file
Receive-PSSession -Session $s2
Remove-PSSession -Session $s2
# Create Persistent Background Job
write-host "Submitting job to remote scheduler"
Invoke-Command -Computer $job_computer -ScriptBlock {
Register-ScheduledJob -RunNow -Name $using:job_name -File $using:job_file
Exit-PSSession
}
# NOTE: Log file from run is placed on
# remote computer under jobs dir
}
function open_w {
param([string]$path, [switch]$fatal)
try {
write-host "path: $path"
$pathfix = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
$handle = [System.IO.StreamWriter]::new($pathfix, $false) #false:over-write, true:append
}
catch {
if ($fatal) {
write-host -ForegroundColor Red "EXCEPTION: " + $PSItem.ToString()
exit 1
}
return $null
}
return $handle
}
I have a questions regard what the difference is when accepting values through either a Pipeline, or Parameter input.
I ask because:
Accepting values by pipeline in my script, works.
Accepting values using the parameter argument such as, "-ComputerName" sort of works.
I say sort of because, It does take my input from the same variable up until theres one that it cant connect to, then it stops the script. Is there a reason for this?
Heres my script for anyone curious:
Function Set-DWRCVersion {
<#
NAME
Set-DWRCVersion
SYNTAX
Set-DWRCVersion [[-ComputerName] <string[]>] [<CommonParameters>]
EXAMPLES
Set-DWRCVersion -ComputerName LocalHost,ComputerOne
'LocalHost','ComputerOne' | Set-DWRCVersion
$CritNames | Set-DWRCVersion
$CritNames | Set-DWRCVersion |Export-Csv "$($env:USERPROFILE.Substring(0,20))\Desktop\NewCrit.CSV" -Force -NoTypeInformation
#>
[cmdletbinding()]
Param(
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipeLineByPropertyName=$true)]
[ValidateLength(1,15)]
[Alias('Comp','CN','name')]
[string[]]$ComputerName)
Process{
try{
[string]$Path = "C:\Users\Abraham\Desktop\dwrcs" #Dameware File Path (current)
foreach($Computer in $ComputerName){
$PSSession = New-PSSession -ComputerName $Computer -ErrorAction Stop
$TestPath = Invoke-Command -Session $PSSession -ScriptBlock { Test-Path -Path "C:\Windows\dwrcs" }
if($TestPath -eq $false){
Copy-Item -Path $Path -Destination "\\$Computer\C$\Windows" -Recurse -Force }
#Start-Sleep -Seconds 1
$EXEVersion = Invoke-Command -Session $PSSession -ScriptBlock { [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Windows\dwrcs\DWRCS.EXE").FileVersion }
$DLLVersion = Invoke-Command -Session $PSSession -ScriptBlock { [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Windows\dwrcs\DWRCRSS.dll").FileVersion }
if($EXEVersion -notmatch '12.1.2.584'){
[PSCustomObject] #{
"Computer Name" = $Computer
"Status" = "Online"
"DWRC Version" = $EXEVersion
"DWRCRSS DLL" = $DLLVersion
"DWRC Check" = "Not up to date" }
""
Write-Output -InputObject "Version not current"
Write-Output -InputObject "Stopping Service. . ."
Invoke-Command -Session $PSSession -ScriptBlock {
Stop-Service -Name dwmrcs;
Get-Item -Path C:\windows\dwrcs | Rename-Item -NewName "dwrcs.old" }
Remove-Item -Path "\\$Computer\c$\Windows\dwrcs.old" -Recurse -Force -ErrorAction SilentlyContinue
#Start-Sleep 1
Write-Output -InputObject "Copying new files over. . ."
Copy-Item -Path $Path -Destination "\\$Computer\C$\Windows" -Recurse -Force
Write-Output -InputObject "Starting Service. . ."
Invoke-Command -Session $PSSession -ScriptBlock { Start-Service -Name dwmrcs }
}
elseif($EXEVersion -match '12.1.2.584') {
[PSCustomObject] #{
"Computer Name" = $Computer
"Status" = "Online"
"DWRC Version" = $EXEVersion
"DWRCRSS DLL" = $DLLVersion
"Version Check" = "Up to Date" }
}
else { Write-Output -InputObject "Error Occured"
Throw "$($Error[0].Exception.Message)" }
}
} Catch {
[PSCustomObject] #{
"Computer Name" = $Computer
"Status" = "Offline"
"DWRC Version" = $null
"DWRCRSS DLL" = $null
"Version Check" = $null }
} Finally {
if ($PSSession) {
Get-PSSession | Remove-PSSession }
}
}
}
Overall, its a pretty simple script where it will get some remote file versions for me. I am just not sure why accepting values from the pipeline doesn't stop the script, but inputting them using the regular Parameter does.
Im importing from a csv to get the names using the following:
$Crit = Import-Csv -Path "$($env:USERPROFILE.Substring(0,20))\Desktop\med.csv"
$CritNames = $Crit.'NetBios Name' -replace "AREA51\\",""
<# Names return like so:
COmputerOne
COmputerTwo
COmputerThr
Etc.. #>
And when running it using the pipeline input: $CritNames | Set-DWRCVersion which works but, Set-DWRCVersion -ComputerName $CritNames doesn't; well it does up until it hits an offline computer then stops the script.
Is there something im missing? Can someone much smarter than me edumacate me?(:
You can solve this by moving the try/catch/finally statement inside the foreach loop:
process {
foreach($Computer in $ComputerName){
try {
# ...
}
catch {
# ...
}
finally {
# ...
}
}
}
Why would this make a difference?
When explicitly binding a value via -ComputerName, the process block is only invoked once - but when you supply input via the pipeline, the process block is invoked once per input item.
You can observe this behavior with a simple test function like this:
function Test-ProcessInvocation {
param(
[Parameter(Mandatory,ValueFromPipeline)]
[string[]]$ComputerName
)
process {
"Running process block with $($ComputerName.Length) input arguments: [$ComputerName]"
}
}
Running this with both input modes will make this behavior clearly visible:
PS ~> Test-ProcessInvocation -ComputerName 1,2,3
Running process block with 3 input arguments: [1 2 3]
PS ~> 1,2,3 |Test-ProcessInvocation
Running process block with 1 input arguments: [1]
Running process block with 1 input arguments: [2]
Running process block with 1 input arguments: [3]
This means that running Set-DWRCVersion -ComputerName Computer1,Computer2 translates to this sequence of statements:
# Single invocation of `process` block with $ComputerName = all input values at once
try {
foreach($Computer in 'Computer1','Computer2'){
}
}
catch {}
finally {}
Running 'Computer1','Computer2' |Set-DWRCVersion on the other hand:
# `process` block repeats per input item
try {
foreach($Computer in 'Computer1'){
}
}
catch {}
finally {}
try {
foreach($Computer in 'Computer2'){
}
}
catch {}
finally {}
As a result, throwing an error inside the foreach loop when invoking via the pipeline never "skips" any items, because the loop is only ever operating on one at a time.
By inverting the relationship between the loop and the try/catch statement, any errors will now have been captured and handled inside the loop, and it will no longer skip the remaining input values in $ComputerName.
I'm attempting to develop a script with PowerShell to remotely install/update flash player for multiple machines. No matter what I do, I can't get the install to work properly at all. I'm very limited with my tools so I have to use PowerShell, and the MSI install of Flashplayer. I'll post my script below, any help at all would be greatly appreciated.
$Computers = Get-Content C:\Users\name\Desktop\flash.txt
(tried these 3 methods to install none work)
$install = #("/a","/i", "\\$Computer\c$\temp\flash\install_flash_player_32_plugin.msi", "/qn","/norestart")
Invoke-Command -ComputerName $Computer -ScriptBlock {Start-Process "Msiexec" -arg "$using:install" -Wait -PassThru} -Filepath msiexec.exe
#This returns with "invoke-command: parameter set cannot be resolved using the specified named parameters"
Invoke-Command -ComputerName $computer -ScriptBlock {Start-Process -Filepath msiexec.exe "$using:install" -Wait -PassThru} -Filepath msiexec.exe
#this returns the same error.
Invoke-Command -ComputerName $Computer -ScriptBlock {start-process msiexec -argumentlist #('/a','/i','"\\$Computer\c$\temp\flash\install_flash_player_32_plugin.msi"','/qn')}
#this seemingly skips the install entirely.
I've used similar scripts for other programs and had no problems installing them, but none of the methods I use or have researched are working properly.
This should do the trick, I'll explain why it wasn't working bellow:
$Computers = Get-Content C:\Users\name\Desktop\flash.txt
$params = '/i <path to AcroPro.msi> LANG_LIST=en_US TRANSFORMS="1033.mst" /qb'
$Computers | % {
Invoke-Command -ScriptBlock {
Param(
[Parameter(Mandatory=$true,Position=0)]
[String]$arguments
)
return Start-Process msiexec.exe -ArgumentList $arguments -Wait -PassThru
} -ComputerName $_ -ArgumentList $params
}
So, it wasn't working because the ScriptBlock on Invoke-Command cant see variables that you've declared on your powershell session, think of it like you are walking to that remote computer and inputting that code by hand, you wont have the value (metaphor).
I did a few more changes:
I moved all params into 1 single string, no need to have them in array.
Added $Computers | to iterate through computer names.
Removed FilePath as this is meant to be used differently, documentation(Example #1).
Set $MinutesToWait to whatever amount of minutes you want.
No need to try to pass msiexec, as it comes with windows the default path is "C:\WINDOWS\system32\msiexec.exe"
Added a return even though its never necessary, but to make it more readable and to show you intent to return the output of the msiexec process.
Replaced \\$Computer\c$ with C:\ as there's no need to use a network connection if you are pointing to the host you are running the command in/
Hope it helps, good luck.
EDIT:
So, as you mentioned the pipeline execution gets stuck, I had this issue in the past when creating the computer preparation script for my department, what I did was use jobs to create parallel executions of the installation so if there's a computer that for some reason is slower or is just flat out stuck and never ends you can identify it, try the following as is to see how it works and then do the replaces:
#region ######## SetUp #######
$bannerInProgress = #"
#######################
#Jobs are still running
#######################
"#
$bannerDone = #"
##################################################
#DONE see results of finished installations bellow
##################################################
"#
#VARS TO SET
$MinutesToWait = 1
$computers = 1..10 | % {"qwerty"*$_} #REPLACE THIS WITH YOUR COMPUTER VALUES (Get-Content C:\Users\name\Desktop\flash.txt)
#endregion
#region ######## Main #######
#Start Jobs (REPLACE SCRIPTBLOCK OF JOB WITH YOUR INVOKE-COMMAND)
$jobs = [System.Collections.ArrayList]::new()
foreach($computer in $computers){
$jobs.Add(
(Start-Job -Name $computer -ScriptBlock {
Param(
[Parameter(Mandatory=$true, Position=0)]
[String]$computer
)
Sleep -s (Get-Random -Minimum 5 -Maximum 200)
$computer
} -ArgumentList $computer)
) | Out-Null
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
$acceptedWait = $MinutesToWait * 60 * 1000 # mins -> sec -> millis
$running = $true
do {
cls
$jobsRunning = $jobs | Where-Object State -EQ 'Running'
if ($jobsRunning) {
Write-Host $bannerInProgress
foreach ($job in $jobsRunning) {
Write-Host "The job `"$($job.Name)`" is still running. It started at $($job.PSBeginTime)"
}
Sleep -s 3
} else {
$running = $false
}
if($timer.ElapsedMilliseconds -ge $acceptedWait){
$timer.Stop()
Write-Host "Accepted time was reached, stopping all jobs still pending." -BackgroundColor Red
$failed = #()
foreach($job in $jobsRunning){
$output = $job | Receive-Job
$failed += [PsCustomObject]#{
"ComputerName" = $job.Name;
"Output" = $output;
}
$job | Remove-Job -Force
$jobs.Remove($job)
}
$failed | Export-Csv .\pendingInstallations.csv -NoTypeInformation -Force
$running = $false
}
}while($running)
Write-host $bannerDone
$results = #()
foreach($job in $jobs){
$output = $job | Receive-Job
$results += [PsCustomObject]#{
"ComputerName" = $job.Name;
"Output" = $output;
}
}
$results | Export-Csv .\install.csv -NoTypeInformation -Force
#endregion
This script will trigger 10 jobs that only wait and return its names, then the jobs that got completed in the time that you set are consider correct and the ones that didn't are consider as pending, both groups get exported to a CSVfor review. You will need to replace the following to work as you intended:
Add $params = '/i <path to AcroPro.msi> LANG_LIST=en_US TRANSFORMS="1033.mst" /qb' in the SetUp region
Replace the declaration of $computers with $computers = Get-Content C:\Users\name\Desktop\flash.txt
Replace the body of Start-Job scriptblock with Invoke-command from thew first snippet of code in this answer.
you should end-up with something like:
.
.code
.
$params = '/i <path to AcroPro.msi> LANG_LIST=en_US TRANSFORMS="1033.mst" /qb'
#VARS TO SET
$MinutesToWait = 1
$computers = Get-Content C:\Users\name\Desktop\flash.txt
#endregion
#region ######## Main #######
#Start Jobs
$jobs = [System.Collections.ArrayList]::new()
foreach($computer in $computers){
$jobs.Add(
(Start-Job -Name $computer -ScriptBlock {
Param(
[Parameter(Mandatory=$true, Position=0)]
[String]$computer
)
Invoke-Command -ScriptBlock {
Param(
[Parameter(Mandatory=$true,Position=0)]
[String]$arguments
)
return Start-Process msiexec.exe -ArgumentList $arguments -Wait -PassThru
} -ComputerName $computer -ArgumentList $params
} -ArgumentList $computer)
) | Out-Null
}
.
. code
.
I know it looks like a complete mess, but it works.
Hope it helps.
I am currently trying to change the proceedure from this site into a script to automate the process. http://www.freenode-windows.org/resources/vista-7/windows-update
When I check Control Panel> System & Security>Windows Update > View Update History-- updates KB3020369, KB3172605, and KB3125574 do not show up as installed. Is there something wrong with my foreach loop?
<########
CONFIGURATION TO STOP WINDOWS UPDATES
#########>
$rmpth = 'c:\windows\softwaredistribution\WuRedir'
$ws = get-service wuauserv
if($ws.Status -eq "Stopped"){
msg * "Update Service Stopped"
}else{
stop-service wuauserv -Force
msg * "Stopping Update Service, Update Service Stopped"
}
if(test-path $rmpth){
remove-item $rmpth -Force -Confirm:$false
}
<###########
CONFIGURATION TO INSTALL WINDOWS PATCH
###########>
$pathofupdates = #("KB3020369", "KB3172605", "KB3125574")
Foreach($item in $pathofupdates)
{
$wusainit = "/quiet /norestart C:\temp\Windows /extract C:\temp\Windows\${item}.msu"
$disminit = "/online /quiet /norestart /add-package /PackagePath:C:\temp\Windows\${disminit}.cab"
$SB={ Start-Process -FilePath 'wusa.exe' -ArgumentList $wusainit.ToString() -Wait -PassThru }
Invoke-Command -ScriptBlock $SB
$SB={ Start-Process -FilePath 'dism.exe' -ArgumentList $disminit.ToString() -Wait -PassThru }
Invoke-Command -ScriptBlock $SB
}
foreach loop and .msu file order was the issue. Updates had to be in a certain order. renamed updates to 1.KB3020369.msu, 2.KB3172605.msu, and 3.KB3125574.msu.
Found new method for applying .msu updates on
https://harshilsharma63.wordpress.com/2014/12/27/how-to-install-multiple-msu-windows-update-files-with-powershell-script/
<###########
CONFIGURATION TO INSTALL WINDOWS PATCH
###########>
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$dir = (Get-Item -Path $dir -Verbose).FullName
Foreach($item in (ls $dir *.msu -Name))
{
echo $item
$item = $dir + "\" + $item
wusa $item /quiet /norestart | Out-Null
}
I'm scripting the installation and configuration procedure for my company's desktop application. We send out a kiosk and can't put everything in the installer... moving right along! I'm using Start-Process to wait for msiexec to complete.
function Run-Installer
{
param
(
[string] $msi = $(throw "Required parameter: 'msi'"),
)
if(-not(Test-Path $msi -Type Leaf))
{
throw "The installer could not be found: '$msi'"
}
$name = (Get-Item $msi).BaseName
Write-Host "Installing $name"
$p =
#(
"/I `"$msi`"", # Install this MSI
"/QN", # Quietly, without a UI
"/L*V `"$ENV:TEMP\$name.log`"" # Verbose output to this log
)
Start-Process -FilePath "msiexec" -ArgumentList $p -Wait
}
Where I want to get fancy is with the log output from msiexec. I want to stream the contents of the log to the console while the installer is running. I'm guessing there are multiple parts to the solution
Running the installer in a background, waitable manner
Tailing the log file until some condition is met (installer job completes)
Optional: Filtering the output or writing to debug/verbose for fun
function Start-FileTail {
param($path)
# Get unique source ID
$sourceID = "FileTailLine-" + [guid]::NewGuid()
$job = Start-Job -ArgumentList $path, $sourceID {
param($path,$sid)
Register-EngineEvent -SourceIdentifier $sid -Forward
do{}until(Test-Path $path)
$fs = New-Object IO.FileStream ($path, [IO.FileMode]::Open,
[IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
$sr = New-Object IO.StreamReader ($fs)
$lines = #()
while(1) {
$line = $sr.ReadLine()
$lines += $line
# Send after every 100 reads
if($lines.Count -gt 100) {
# Join lines into 1 string
$text = #($lines| where {$_} ) -join "`n"
# Only send if text was found
if($text){New-Event -SourceIdentifier $sid -MessageData $text}
$lines = #()
}
}
}
$event = Register-EngineEvent -SourceIdentifier $sourceID -Action {
Write-Host $event.MessageData
}
New-Object Object|
Add-Member -Name Job -Type NoteProperty -Value $job -PassThru|
Add-Member -Name SourceIdentifier -Type NoteProperty -Value $sourceID -PassThru
}
function Stop-FileTail {
param($TailInfo)
Remove-Job $TailInfo.Job -Force
Unregister-Event -SourceIdentifier $tail.SourceIdentifier
}
You can remove the job, and unregister the event once the install is done.
Change Write-Host to Write-Verbose for -Verbose support
EDIT: I tested my answer while installing an application, and found that it was pretty slow when reading the log file. I updated the Get-Content call to use -ReadCount 100 to send the data as arrays of lines. The Write-Host line was updated to handle the arrays.
I also found that using the -Wait switch on Start-Process caused all of the log output to be written after the install was finished. This can be fixed by using:
$msi = Start-Process -FilePath "msiexec" -ArgumentList $p -PassThru
do{}until($msi.HasExited)
EDIT 2: Hmm, I don't get all of the log file when I use -Wait and -ReadCount together. I put the reading of the log file back to how I originally had it. I'm not sure what to do about the speed yet.
EDIT 3: I've updated the code to use a StreamReader instead of Get-Content, and put the code into functions. You would then call it like:
$path = "$ENV:TEMP\$name.log"
if(Test-Path $path){Remove-Item $path}
$msi = Start-Process -FilePath "msiexec" -ArgumentList $p -PassThru
$tail = Start-FileTail $p
do{}until($msi.HasExited)
sleep 1 # Allow time to finish reading log.
Stop-FileTail $tail