I've got a macro looping with powershell and at the end of a run to clean up it kills and restarts the browser, Standard FireFox version firefox.exe
Using:
taskkill /F /IM firefox.exe /T
How can I prevent it from killing the dev version of FireFox too, which has the same executable name?
EDIT:
I was asked to post the code
Link to the original script from github https://github.com/A9T9/RPA/blob/2501e8cbc504160e2aab89a7e35ece9fcf2873e1/command-line/powershell/run%20one%20macro%20forever.ps1
This script shows how to run a macro "forever"
by checking on the command line return value
and killing/restarting Browser if needed
function PlayAndWait ([string]$macro)
{
$timeout_seconds = 300
$path_downloaddir = "C:\Users\xxxx\ui-logs\"
$path_autorun_html = "C:\Users\xxxxx\UIVision Powershell Script\ui.vision.html"
$log = "log_" + $(get-date -f MM-dd-yyyy_HH_mm_ss) + ".txt"
$path_log = $path_downloaddir + $log
$browser = 2
Switch ($browser) {
1 {$cmd = "${env:Program Files(x86)}\Google\Chrome\Application\chrome.exe"; break}
2 {$cmd = "${env:ProgramFiles}\Mozilla Firefox\firefox.exe"; break} #For FIREFOX
}
$arg = """file:///"+ $path_autorun_html + "?macro="+ $macro + "&direct=1&closeRPA=1&closeBrowser=1&savelog="+$log+""""
Start-Process -FilePath $cmd -ArgumentList $arg #Launch the browser and run the macro
$status_runtime = 0
Write-Host "Log file will show up at " + $path_log
while (!(Test-Path $path_log) -and ($status_runtime -lt $timeout_seconds))
{
Write-Host "Waiting for macro to finish, seconds=" $status_runtime
Start-Sleep 10
$status_runtime = $status_runtime + 10
}
if ($status_runtime -lt $timeout_seconds)
{
$status_text = Get-Content $path_log -First 1
$status_int = -1
If ($status_text -contains "Status=OK") {$status_int = 1}
}
else
{
$status_text = "Macro did not complete within the time given:" + $timeout_seconds
$status_int = -2
}
remove-item $path_log #clean up
return $status_int, $status_text, $status_runtime
}
$testreport = "C:\xxxx\ui-logs\uireport.txt"
For ($i=0; $i -le 9999; $i++) {
Write-Host "Loop Number:" $i
$result = PlayAndWait MyMacro-04-2022 #run the macro
$errortext = $result[1] #Get error text or OK
$runtime = $result[2] #Get runtime
$report = "Loop:" + $i + " Return code: " + $result[0]+ " Macro runtime: "+$runtime+" seconds, Result: "+ $errortext
Write-Host $report
Add-content $testreport -value ($report)
if ($result[0] -ne 1)
{
taskkill /F /IM firefox.exe /T
$report = "Loop:" + $i + " Firefox closed"
Add-content $testreport -value ($report)
}
}
Assuming Developer edition is installed in the default directory of "C:\Program Files\Firefox Developer Edition\firefox.exe"
Get-Process Firefox | ?{$_.path -notmatch "dev"} | Stop-Process -Force
Alternatively you can use the exe file description field
Get-Process Firefox | ?{$(Get-ItemProperty -Path $_.Path).VersionInfo.FileDescription -notmatch "Developer"} | Stop-Process -Force
edit: to kill the whole process tree as per this post:
function Kill-Tree {
Param([int]$ppid)
Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $ppid } | ForEach-Object { Kill-Tree $_.ProcessId }
Stop-Process -Id $ppid
}
Get-Process Firefox | ?{$(Get-ItemProperty -Path $_.Path).VersionInfo.FileDescription -notmatch "Developer"} | %{Kill-Tree $_.Id}
Below PowerShell code can help you get motivated to write your own alias for ninja like activity
Replace %userprofile% with your username
Replace [hints] with the original path
Type $profile in your powershell cli, and append the below code to get started
Will this activity drag the systems performance?
function poweroff_ {
shutdown -s -f
}
function hibernate_ {
shutdown -h
}
function restart_ {
shutdown -r -f
}
function eject_usb($driveletter){
# safely eject the mention drive eg:- d:\
$driveEject = New-Object -comObject Shell.Application
$driveEject.Namespace(17).ParseName($driveletter).InvokeVerb("Eject")
}
function cleanup_ {
#remove clutter from temp directory
rm -r -fo "C:\Users\%userprofile%\AppData\Local\Temp"
#clear Recycle-bin
clear-RecycleBin -confirm:$false
}
function pomodoro_timer{
#Reference: https://en.wikipedia.org/wiki/Pomodoro_Technique
Write-Host "'f' for Foucs(40 mins), 'sb' fo Short Break(10 mins), 'lb' for Long Break(30 mins)"
$logic = Read-Host "Ready? [f/sb/lb]"
if($logic -eq "f"){
$minutes = 40
} elseif ($logic -eq "sb") {
$minutes = 10
} elseif ($logic -eq "lb") {
$minutes = 30
}
#To-Do: Include background music [vlc] for each mode, and upon completing - play alert sound [custom sound, from local path]
$seconds = $minutes * 60
$delay = 1 #seconds between ticks
for ($i = $seconds; $i -gt 0; $i = $i - $delay) {
$percentComplete = 100 - (($i / $seconds) * 100)
Write-Progress -SecondsRemaining $i `
-Activity "Pomodoro Focus sessions" `
-Status "Time remaining:" `
-PercentComplete $percentComplete
if ($i -eq 16){Write-Host "Wrapping up, you will be available in $i seconds" -ForegroundColor Green}
Start-Sleep -Seconds $delay
}#Timer ended
}
# Native command renaming
set-alias -Name unzip -Value expand-archive
# Path specific
set-alias -Name np -Value notepad.exe
set-alias -name notes -Value C:\Users\%userprofile%\AppData\Local\[notetaking app.exe]
set-alias -name brave -value C:\Users\%userprofile%\AppData\Local\BraveSoftware\Brave-Browser\Application\brave.exe
Set-Alias -name paswdmng -value C:\Users\%userprofile[paswword manager app.exe]
Set-Alias -Name vs -value "C:\Users\%userprofile%\AppData\Local\Programs\Microsoft VS Code\Code.exe"
Set-Alias -Name outlook -Value "C:\Users\%userprofile%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Brave Apps\Outlook.lnk"
Set-Alias -Name teams -Value "C:\Users\%userprofile%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Microsoft Teams.lnk"
Set-Alias -Name vlc -Value "C:\Program Files\VideoLAN\VLC\vlc.exe"
# Invoking custom fuctions
New-Alias poweroff poweroff_
New-Alias frezze hibernate_
New-Alias restart restart_
New-Alias eject eject_usb
New-Alias cleanup cleanup_
New-alias focusmode pomodoro_timer
So I needed a script that watches a directory and converts files using HandbrakeCLI. I found a part of this powershell here on stackoverflow and I adjusted some things for my project.
$global:watch = "C:\~\cmp\" ### watching directory
$global:convert = "C:\~\convert\" ### handbrakecli and preset location
$global:outp = "C:\~\output_folder\" ### output location
$global:extIn = ".mkv"
$global:extOut = ".mp4"
Write-Host "Watching directory = $watch"
Write-Host "HandbrakeCLI / json preset location = $convert"
Write-Host "Output directory = $outp"
Write-Host "Input extension = $extIn ; Output extension = $extOut"
Write-Host "Waiting for change in directory..."
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher;
$watcher.Path = $watch;
$watcher.Filter = "*"+$extIn;
$watcher.IncludeSubdirectories = $false;
$watcher.EnableRaisingEvents = $false;
### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action =
{
$path = $Event.SourceEventArgs.FullPath;
$handbrakecli = $convert+"HandBrakeCLI.exe";
$fl = Split-Path $path -leaf;
Write-Host "New file found: $fl";
$flName, $flExt = $fl.split('.')
$mp4File = $watch+"\"+$flName+$extOut
$changeType = $Event.SourceEventArgs.ChangeType
$logline = "$(Get-Date), $changeType, $path"
Add-content -path ($convert+"log.txt") -value $logline
Write-Host "Added entry to log"
Write-Host "Start converting using HandbrakeCLI..."
& cmd.exe /c $handbrakecli -i $path -o $mp4File --preset-import-file my_preset.json
Write-Host "Done converting!"
Write-Host "Moving file to folder..."
& cmd.exe /c move /Y $mp4File $outp
Write-Host "File moved!"
& cmd.exe /c del $path /F
Write-Host "$fl has been removed from local folder"
}
### DECIDE WHICH EVENTS SHOULD BE WATCHED
Register-ObjectEvent $watcher "Created" -Action $action
Register-ObjectEvent $watcher "Changed" -Action $action
Register-ObjectEvent $watcher "Renamed" -Action $action
while ($true) {sleep 5}
While at first everything seemed to work, I started to notice that "sometimes" the subtitles were not added or green frames were inserted (or replaced original frame) after every frame (normal - green - normal - green - etc.).
An example: I added 2 mkv files to the directory, the 1st one got converted just fine with subtitles while the 2nd file didn't have any subtitles.
I'm an amateur when it comes to this stuff, but I think it has something to do with the & cmd.exe /c. I also found that you could to Start-Process in powershell, but I don't know how to use it.
So if someone could help me convert this & cmd.exe /c $handbrakecli -i $path -o $mp4File --preset-import-file my_preset.json to something with Start-Process ..., maybe it will help me out.
EDIT
So I made the changes that Tomalak suggested (simpler this way), but Move-Item and Remove-Item don't seem to work.
EDIT 2
Added -LiteralPath as argument for Move-Item / Remove-Item (needed for filenames containt square brackets)
$inputFolder = "C:\~\cmp\"
$outputFolder = "C:\~\output_folder\"
$handbrake = "C:\~\convert\HandBrakeCLI.exe"
$presetJson ="C:\~\convert\my_preset.json"
$extIn = "mkv"
$extOut = "mp4"
while ($true) {
Get-ChildItem -Path $inputFolder -Filter "*.$extIn" | ForEach-Object {
$inFile = $_.FullName
$outFile = $inputFolder + $_.FullName.split('\.')[-2] + ".$extOut" #changed this because I wanted the file in the same directory as input file
Write-Host "Converting: $inFile"
& $handbrake -i $inFile -o $outFile --preset-import-file $presetJson
if ($LASTEXITCODE -eq 0) {
Move-Item -LiteralPath $outFile $outputFolder -Force #move to output folder
Write-Host "Done: $outFile"
Remove-Item -LiteralPath $inFile -Force #removing the input item, not output
Write-Host "Removed input file!"
} else {
Write-Error "Conversion failed!"
}
}
sleep 5
}
While subtitles are added to all output files, I still get green-flickering sometimes. I used 3 files as a test run, result: 1st flickering, 2nd OK, 3rd flickering. I have no clue why some are fine and some got the flickering. So I'm considering to maybe use ffmpeg instead.
EDIT 3
For future visitors: use ffmpeg instead of HandbrakeCLI:
ffmpeg.exe -i "C:\~\inputfile.mkv" -filter_complex "subtitles='C\:/Users/~/inputfile.mkv'" -c:v libx264 -preset veryfast -b:v 2750k -c:a aac $outputfile.mp4
Instead of using file system notifications, structure your script around a simple endless loop:
$inputFolder = "C:\~\cmp"
$outputFolder = "C:\~\convert"
$handbrake = "C:\~\convert\HandBrakeCLI.exe"
$presetJson = "C:\~\convert\my_preset.json"
$extIn = "mkv"
$extOut = "mp4"
while ($true) {
Get-ChildItem -Path $inputFolder -Filter "*.$extIn" | ForEach-Object {
$inFile = $_.FullName
$outFile = "$($_.BaseName).$extOut"
if (Test-Path $outFile) { Remove-Item $outFile -Force -LiteralPath }
Write-Host "Converting: $inFile"
& $handbrake -i $inFile -o $outFile --preset-import-file $presetJson
if ($LASTEXITCODE -eq 0) {
Move-Item $outFile $outputFolder -Force -LiteralPath
Write-Host "Done: $outFile"
} else {
Write-Error "Conversion not successful."
}
}
sleep 5
}
The & makes Powershell execute whatever program the $handbrake variable points to.
As an exercise you can convert the top-level variables to script parameters, so that you can re-use the script for other batch jobs.
I'm working on a simple script that monitors a folder for large video files and if it's larger then the set size it should send it to HandBrake to convert.
The main function of the script is done and works more or less but I am having trouble displaying proper output from HandBrake.
Here is what I have written so far:
$Folder = "E:\Series" #Folder to be monitored for large video files.
$Output = "E:\Encoded" #Folder where encoding jobs go to, and that will be monitored for completed jobs to replace the original file with.
$MaxMB = "15" #Max MB a minute
$MI_CLI = "C:\Program Files\MediaInfoCLI\MediaInfo.exe" #Location oo MediaInfoCLI
$HB_CLI = "C:\Program Files\Handbrake\HandBrakeCLI.exe" #Location of HandBrakeCLI
$HB_Container = "Copy" #HandBrake container output
$Filter = '*.*'
Write-Host "Monitoring "
$fsw = New-Object IO.FileSystemWatcher $Folder, $Filter
$fsw.IncludeSubdirectories = $true
Register-ObjectEvent $fsw Changed -SourceIdentifier FileUpdated -Action {
Start-Sleep -s 1
$FilePath = $EventArgs.FullPath
$FileName = $FilePath.split('\')[-1]
if($FilePath -imatch '\.(?:mp4|mkv)'){
if((Test-Path -LiteralPath $filePath) -and -not (Test-Path -LiteralPath "$Output\$FileName")){
if(Test-FileReady $FilePath){
$fileSize = (Get-Item $FilePath).length
if($fileSize -ge 734003200){
Write-Host ""
Write-Host "Large Video Detected: `"$($FileName)`""
Write-Host "Sending To HandBrake..."
HB-Convert $FilePath $Output
}
}
}
}
}
function HB-Convert{
param ([parameter(Mandatory=$true)][string]$source,[parameter(Mandatory=$true)][string]$dest)
if(-not (Test-Path $source) -or -not (Test-Path $dest)) {return}
$FileName = $source.split('\')[-1]
start-process $HB_CLI -ArgumentList "-i `"$source`" -t 13 --angle 1 -c 1 -o `"$dest\$FileName`" -f mkv -w 1280 --crop 0:0:0:0 --loose-anamorphic --modulus 2 -e x265 -q 20 --vfr -a 1 -E copy -6 dpl2 -R Auto -B 160 -D 0 --gain 0 --audio-fallback ac3 --encoder-preset=faster --verbose=0 2> log.txt" -wait -nonewwindow
#HandBrakeCLI -i `"$source`" -t 13 --angle 1 -c 1 -o `"$dest\$FileName`" -f mkv -w 1280 --crop 0:0:0:0 --loose-anamorphic --modulus 2 -e x265 -q 20 --vfr -a 1 -E copy -6 dpl2 -R Auto -B 160 -D 0 --gain 0 --audio-fallback ac3 --encoder-preset=faster --verbose=0 2> log.txt
}
function Test-FileReady {
Param([parameter(Mandatory=$true)]$path)
if (Test-Path -LiteralPath $path) {
trap {
return $false
}
$stream = New-Object system.IO.StreamReader $path
if ($stream) {
$stream.Close()
return $true
}
}
}
Now in HB-Convert function I have 2 lines that call HandBrakeCLI.
one through Start-Process
and one using HandBrakeCLI(I have added handbrake dir to my system environment variable)
the latter one is now marked out.
When I would call HB-Convert(Using HandBrakeCLI not Start-Process) manually in the command prompt everything works like it should I only get the progress of handbrake displayed.
Encoding: task 1 of 1, 74.66 % (26.78 fps, avg 47.17 fps, ETA
00h05m40s)
Now when this is called through the FileSystemWatcher It will not display anything from handbrake it will only show output from the script it self
Large Video Detected: "FileName"
Sending To HandBrake...
it will hang there till the encoding is done
now when FileSystemWatcher calls HB-Convert with start-process handbrake will output all data not just the progress witch is very annoying.
so how would I get it to display only the progress when its called true the FileSystemWatcher.
I have been trying to get it to work for hours its driving me nuts. hope some one here can fix it.
I only learned a bit of PS for this so when I say that I am a nub in PS thats a understatement :D
Ok I got it to work
adding this to the script
function global:HB-UpdateProgress{
Process{
$position = $host.ui.rawui.cursorposition
$position.X = 0
$host.ui.rawui.cursorposition = $position
Write-Host #($input)[0] -NoNewline
}
}
then use it as a pipeline when calling HandBrakeCLI will display the output properly
Is there any way to copy a really large file (from one server to another) in PowerShell AND display its progress?
There are solutions out there to use Write-Progress in conjunction with looping to copy many files and display progress. However I can't seem to find anything that would show progress of a single file.
Any thoughts?
It seems like a much better solution to just use BitsTransfer, it seems to come OOTB on most Windows machines with PowerShell 2.0 or greater.
Import-Module BitsTransfer
Start-BitsTransfer -Source $Source -Destination $Destination -Description "Backup" -DisplayName "Backup"
I haven't heard about progress with Copy-Item. If you don't want to use any external tool, you can experiment with streams. The size of buffer varies, you may try different values (from 2kb to 64kb).
function Copy-File {
param( [string]$from, [string]$to)
$ffile = [io.file]::OpenRead($from)
$tofile = [io.file]::OpenWrite($to)
Write-Progress -Activity "Copying file" -status "$from -> $to" -PercentComplete 0
try {
[byte[]]$buff = new-object byte[] 4096
[long]$total = [int]$count = 0
do {
$count = $ffile.Read($buff, 0, $buff.Length)
$tofile.Write($buff, 0, $count)
$total += $count
if ($total % 1mb -eq 0) {
Write-Progress -Activity "Copying file" -status "$from -> $to" `
-PercentComplete ([long]($total * 100 / $ffile.Length))
}
} while ($count -gt 0)
}
finally {
$ffile.Dispose()
$tofile.Dispose()
Write-Progress -Activity "Copying file" -Status "Ready" -Completed
}
}
Alternativly this option uses the native windows progress bar...
$FOF_CREATEPROGRESSDLG = "&H0&"
$objShell = New-Object -ComObject "Shell.Application"
$objFolder = $objShell.NameSpace($DestLocation)
$objFolder.CopyHere($srcFile, $FOF_CREATEPROGRESSDLG)
cmd /c copy /z src dest
not pure PowerShell, but executable in PowerShell and it displays progress in percents
I amended the code from stej (which was great, just what i needed!) to use larger buffer, [long] for larger files and used System.Diagnostics.Stopwatch class to track elapsed time and estimate time remaining.
Also added reporting of transfer rate during transfer and outputting overall elapsed time and overall transfer rate.
Using 4MB (4096*1024 bytes) buffer to get better than Win7 native throughput copying from NAS to USB stick on laptop over wifi.
On To-Do list:
add error handling (catch)
handle get-childitem file list as input
nested progress bars when copying multiple files (file x of y, % if
total data copied etc)
input parameter for buffer size
Feel free to use/improve :-)
function Copy-File {
param( [string]$from, [string]$to)
$ffile = [io.file]::OpenRead($from)
$tofile = [io.file]::OpenWrite($to)
Write-Progress `
-Activity "Copying file" `
-status ($from.Split("\")|select -last 1) `
-PercentComplete 0
try {
$sw = [System.Diagnostics.Stopwatch]::StartNew();
[byte[]]$buff = new-object byte[] (4096*1024)
[long]$total = [long]$count = 0
do {
$count = $ffile.Read($buff, 0, $buff.Length)
$tofile.Write($buff, 0, $count)
$total += $count
[int]$pctcomp = ([int]($total/$ffile.Length* 100));
[int]$secselapsed = [int]($sw.elapsedmilliseconds.ToString())/1000;
if ( $secselapsed -ne 0 ) {
[single]$xferrate = (($total/$secselapsed)/1mb);
} else {
[single]$xferrate = 0.0
}
if ($total % 1mb -eq 0) {
if($pctcomp -gt 0)`
{[int]$secsleft = ((($secselapsed/$pctcomp)* 100)-$secselapsed);
} else {
[int]$secsleft = 0};
Write-Progress `
-Activity ($pctcomp.ToString() + "% Copying file # " + "{0:n2}" -f $xferrate + " MB/s")`
-status ($from.Split("\")|select -last 1) `
-PercentComplete $pctcomp `
-SecondsRemaining $secsleft;
}
} while ($count -gt 0)
$sw.Stop();
$sw.Reset();
}
finally {
write-host (($from.Split("\")|select -last 1) + `
" copied in " + $secselapsed + " seconds at " + `
"{0:n2}" -f [int](($ffile.length/$secselapsed)/1mb) + " MB/s.");
$ffile.Close();
$tofile.Close();
}
}
Not that I'm aware of. I wouldn't recommend using copy-item for this anyway. I don't think it has been designed to be robust like robocopy.exe to support retry which you would want for extremely large file copies over the network.
i found none of the examples above met my needs, i wanted to copy a directory with sub directories, the problem is my source directory had too many files so i quickly hit the BITS file limit (i had > 1500 file) also the total directory size was quite large.
i found a function using robocopy that was a good starting point at https://keithga.wordpress.com/2014/06/23/copy-itemwithprogress/, however i found it wasn't quite robust enough, it didn't handle trailing slashes, spaces gracefully and did not stop the copy when the script was halted.
Here is my refined version:
function Copy-ItemWithProgress
{
<#
.SYNOPSIS
RoboCopy with PowerShell progress.
.DESCRIPTION
Performs file copy with RoboCopy. Output from RoboCopy is captured,
parsed, and returned as Powershell native status and progress.
.PARAMETER Source
Directory to copy files from, this should not contain trailing slashes
.PARAMETER Destination
DIrectory to copy files to, this should not contain trailing slahes
.PARAMETER FilesToCopy
A wildcard expresion of which files to copy, defaults to *.*
.PARAMETER RobocopyArgs
List of arguments passed directly to Robocopy.
Must not conflict with defaults: /ndl /TEE /Bytes /NC /nfl /Log
.PARAMETER ProgressID
When specified (>=0) will use this identifier for the progress bar
.PARAMETER ParentProgressID
When specified (>= 0) will use this identifier as the parent ID for progress bars
so that they appear nested which allows for usage in more complex scripts.
.OUTPUTS
Returns an object with the status of final copy.
REMINDER: Any error level below 8 can be considered a success by RoboCopy.
.EXAMPLE
C:\PS> .\Copy-ItemWithProgress c:\Src d:\Dest
Copy the contents of the c:\Src directory to a directory d:\Dest
Without the /e or /mir switch, only files from the root of c:\src are copied.
.EXAMPLE
C:\PS> .\Copy-ItemWithProgress '"c:\Src Files"' d:\Dest /mir /xf *.log -Verbose
Copy the contents of the 'c:\Name with Space' directory to a directory d:\Dest
/mir and /XF parameters are passed to robocopy, and script is run verbose
.LINK
https://keithga.wordpress.com/2014/06/23/copy-itemwithprogress
.NOTES
By Keith S. Garner (KeithGa#KeithGa.com) - 6/23/2014
With inspiration by Trevor Sullivan #pcgeek86
Tweaked by Justin Marshall - 02/20/2020
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Source,
[Parameter(Mandatory=$true)]
[string]$Destination,
[Parameter(Mandatory=$false)]
[string]$FilesToCopy="*.*",
[Parameter(Mandatory = $true,ValueFromRemainingArguments=$true)]
[string[]] $RobocopyArgs,
[int]$ParentProgressID=-1,
[int]$ProgressID=-1
)
#handle spaces and trailing slashes
$SourceDir = '"{0}"' -f ($Source -replace "\\+$","")
$TargetDir = '"{0}"' -f ($Destination -replace "\\+$","")
$ScanLog = [IO.Path]::GetTempFileName()
$RoboLog = [IO.Path]::GetTempFileName()
$ScanArgs = #($SourceDir,$TargetDir,$FilesToCopy) + $RobocopyArgs + "/ndl /TEE /bytes /Log:$ScanLog /nfl /L".Split(" ")
$RoboArgs = #($SourceDir,$TargetDir,$FilesToCopy) + $RobocopyArgs + "/ndl /TEE /bytes /Log:$RoboLog /NC".Split(" ")
# Launch Robocopy Processes
write-verbose ("Robocopy Scan:`n" + ($ScanArgs -join " "))
write-verbose ("Robocopy Full:`n" + ($RoboArgs -join " "))
$ScanRun = start-process robocopy -PassThru -WindowStyle Hidden -ArgumentList $ScanArgs
try
{
$RoboRun = start-process robocopy -PassThru -WindowStyle Hidden -ArgumentList $RoboArgs
try
{
# Parse Robocopy "Scan" pass
$ScanRun.WaitForExit()
$LogData = get-content $ScanLog
if ($ScanRun.ExitCode -ge 8)
{
$LogData|out-string|Write-Error
throw "Robocopy $($ScanRun.ExitCode)"
}
$FileSize = [regex]::Match($LogData[-4],".+:\s+(\d+)\s+(\d+)").Groups[2].Value
write-verbose ("Robocopy Bytes: $FileSize `n" +($LogData -join "`n"))
#determine progress parameters
$ProgressParms=#{}
if ($ParentProgressID -ge 0) {
$ProgressParms['ParentID']=$ParentProgressID
}
if ($ProgressID -ge 0) {
$ProgressParms['ID']=$ProgressID
} else {
$ProgressParms['ID']=$RoboRun.Id
}
# Monitor Full RoboCopy
while (!$RoboRun.HasExited)
{
$LogData = get-content $RoboLog
$Files = $LogData -match "^\s*(\d+)\s+(\S+)"
if ($null -ne $Files )
{
$copied = ($Files[0..($Files.Length-2)] | ForEach-Object {$_.Split("`t")[-2]} | Measure-Object -sum).Sum
if ($LogData[-1] -match "(100|\d?\d\.\d)\%")
{
write-progress Copy -ParentID $ProgressParms['ID'] -percentComplete $LogData[-1].Trim("% `t") $LogData[-1]
$Copied += $Files[-1].Split("`t")[-2] /100 * ($LogData[-1].Trim("% `t"))
}
else
{
write-progress Copy -ParentID $ProgressParms['ID'] -Complete
}
write-progress ROBOCOPY -PercentComplete ($Copied/$FileSize*100) $Files[-1].Split("`t")[-1] #ProgressParms
}
}
} finally {
if (!$RoboRun.HasExited) {Write-Warning "Terminating copy process with ID $($RoboRun.Id)..."; $RoboRun.Kill() ; }
$RoboRun.WaitForExit()
# Parse full RoboCopy pass results, and cleanup
(get-content $RoboLog)[-11..-2] | out-string | Write-Verbose
remove-item $RoboLog
write-output ([PSCustomObject]#{ ExitCode = $RoboRun.ExitCode })
}
} finally {
if (!$ScanRun.HasExited) {Write-Warning "Terminating scan process with ID $($ScanRun.Id)..."; $ScanRun.Kill() }
$ScanRun.WaitForExit()
remove-item $ScanLog
}
}
Hate to be the one to bump an old subject, but I found this post extremely useful. After running performance tests on the snippets by stej and it's refinement by Graham Gold, plus the BITS suggestion by Nacht, I have decuded that:
I really liked Graham's command with time estimations and speed readings.
I also really liked the significant speed increase of using BITS as my transfer method.
Faced with the decision between the two... I found that Start-BitsTransfer supported Asynchronous mode. So here is the result of my merging the two.
function Copy-File {
# ref: https://stackoverflow.com/a/55527732/3626361
param([string]$From, [string]$To)
try {
$job = Start-BitsTransfer -Source $From -Destination $To `
-Description "Moving: $From => $To" `
-DisplayName "Backup" -Asynchronous
# Start stopwatch
$sw = [System.Diagnostics.Stopwatch]::StartNew()
Write-Progress -Activity "Connecting..."
while ($job.JobState.ToString() -ne "Transferred") {
switch ($job.JobState.ToString()) {
"Connecting" {
break
}
"Transferring" {
$pctcomp = ($job.BytesTransferred / $job.BytesTotal) * 100
$elapsed = ($sw.elapsedmilliseconds.ToString()) / 1000
if ($elapsed -eq 0) {
$xferrate = 0.0
}
else {
$xferrate = (($job.BytesTransferred / $elapsed) / 1mb);
}
if ($job.BytesTransferred % 1mb -eq 0) {
if ($pctcomp -gt 0) {
$secsleft = ((($elapsed / $pctcomp) * 100) - $elapsed)
}
else {
$secsleft = 0
}
Write-Progress -Activity ("Copying file '" + ($From.Split("\") | Select-Object -last 1) + "' # " + "{0:n2}" -f $xferrate + "MB/s") `
-PercentComplete $pctcomp `
-SecondsRemaining $secsleft
}
break
}
"Transferred" {
break
}
Default {
throw $job.JobState.ToString() + " unexpected BITS state."
}
}
}
$sw.Stop()
$sw.Reset()
}
finally {
Complete-BitsTransfer -BitsJob $job
Write-Progress -Activity "Completed" -Completed
}
}
This recursive function copies files and directories recursively from source path to destination path
If file already exists on destination path, it copies them only with newer files.
Function Copy-FilesBitsTransfer(
[Parameter(Mandatory=$true)][String]$sourcePath,
[Parameter(Mandatory=$true)][String]$destinationPath,
[Parameter(Mandatory=$false)][bool]$createRootDirectory = $true)
{
$item = Get-Item $sourcePath
$itemName = Split-Path $sourcePath -leaf
if (!$item.PSIsContainer){ #Item Is a file
$clientFileTime = Get-Item $sourcePath | select LastWriteTime -ExpandProperty LastWriteTime
if (!(Test-Path -Path $destinationPath\$itemName)){
Start-BitsTransfer -Source $sourcePath -Destination $destinationPath -Description "$sourcePath >> $destinationPath" -DisplayName "Copy Template file" -Confirm:$false
if (!$?){
return $false
}
}
else{
$serverFileTime = Get-Item $destinationPath\$itemName | select LastWriteTime -ExpandProperty LastWriteTime
if ($serverFileTime -lt $clientFileTime)
{
Start-BitsTransfer -Source $sourcePath -Destination $destinationPath -Description "$sourcePath >> $destinationPath" -DisplayName "Copy Template file" -Confirm:$false
if (!$?){
return $false
}
}
}
}
else{ #Item Is a directory
if ($createRootDirectory){
$destinationPath = "$destinationPath\$itemName"
if (!(Test-Path -Path $destinationPath -PathType Container)){
if (Test-Path -Path $destinationPath -PathType Leaf){ #In case item is a file, delete it.
Remove-Item -Path $destinationPath
}
New-Item -ItemType Directory $destinationPath | Out-Null
if (!$?){
return $false
}
}
}
Foreach ($fileOrDirectory in (Get-Item -Path "$sourcePath\*"))
{
$status = Copy-FilesBitsTransfer $fileOrDirectory $destinationPath $true
if (!$status){
return $false
}
}
}
return $true
}
Sean Kearney from the Hey, Scripting Guy! Blog has a solution I found works pretty nicely.
Function Copy-WithProgress
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$Source,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$Destination
)
$Source=$Source.tolower()
$Filelist=Get-Childitem "$Source" –Recurse
$Total=$Filelist.count
$Position=0
foreach ($File in $Filelist)
{
$Filename=$File.Fullname.tolower().replace($Source,'')
$DestinationFile=($Destination+$Filename)
Write-Progress -Activity "Copying data from '$source' to '$Destination'" -Status "Copying File $Filename" -PercentComplete (($Position/$total)*100)
Copy-Item $File.FullName -Destination $DestinationFile
$Position++
}
}
Then to use it:
Copy-WithProgress -Source $src -Destination $dest
Trevor Sullivan has a write-up on how to add a command called Copy-ItemWithProgress to PowerShell on Robocopy.