nie podoba Ci się w jaki sposób działa jakieś polecenie? nie chcesz/nie możesz grzebać w źródłowych plikach?
dziś Kacper pokaże, w jaki sposób nadpisać standardowe polecenie, przesilając je.
.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.
Tym razem dla odmiany najpierw skrypt, potem opis.
function Copy-Item { [CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess=$true, ConfirmImpact='Medium', SupportsTransactions=$true, HelpUri='http://go.microsoft.com/fwlink/?LinkID=113292')] param( [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string[]] ${Path}, [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [Alias('PSPath')] [string[]] ${LiteralPath}, [Parameter(Position=1, ValueFromPipelineByPropertyName=$true)] [string] ${Destination}, [switch] ${Container}, [switch] ${Force}, [string] ${Filter}, [string[]] ${Include}, [string[]] ${Exclude}, [switch] ${Recurse}, [switch] ${PassThru}, [Parameter(ValueFromPipelineByPropertyName=$true)] [pscredential] [System.Management.Automation.CredentialAttribute()] ${Credential}, [switch] ${Unbuffered}, [switch] ${Progress}) begin { try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Copy-Item', [System.Management.Automation.CommandTypes]::Cmdlet) if($PSBoundParameters['Unbuffered']) { $null = $PSBoundParameters.Remove('Unbuffered') $scriptCmd = {& xcopy.exe $PSBoundParameters["Path"] $PSBoundParameters["Destination"] /Y /J} if($PSBoundParameters['Progress']) { $null = $PSBoundParameters.Remove('Progress') $ScriptBlock = { $XcopyProcess = Start-Process -FilePath xcopy -ArgumentList "$($PSBoundParameters["Path"]) $($PSBoundParameters["Destination"]) /Y /J" -Wait:$false -NoNewWindow -PassThru Write-Verbose -Message "Xcopy process PID: $($XcopyProcess.Id)" $FileSize = (Get-Item $PSBoundParameters["Path"]).Length / 1MB function Get-IoCounterMb { $Runspace = [runspacefactory]::CreateRunspace() $PowerShell = [powershell]::Create() $PowerShell.runspace = $Runspace $Runspace.Open() [void]$PowerShell.AddScript({ [math]::Round((Get-Counter -Counter "\Process(xcopy)\IO Write Bytes/sec").CounterSamples.CookedValue / 1MB,0) }) $PowerShell.Invoke() $PowerShell.Dispose() } $MBSum = 0 do { Write-Verbose -Message "Waiting for xcopy process..." Start-Sleep -Seconds 1 } until (Get-Process xcopy -ErrorAction SilentlyContinue) while(Get-Process xcopy -ErrorAction SilentlyContinue) { $MB = Get-IoCounterMb $MBSum = $MBSum + [int]$MB Write-Verbose "MB copied: $MBSum, MB all: $FileSize" Write-Progress -activity "Copying file" -status "Percent finished: " -PercentComplete (($MBSum / $FileSize) * 100) Start-Sleep -Milliseconds 1 } } $scriptCmd = {Invoke-Command -ScriptBlock $ScriptBlock} } } else { $scriptCmd = {& $wrappedCmd @PSBoundParameters } } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } <# .ForwardHelpTargetName Microsoft.PowerShell.Management\Copy-Item .ForwardHelpCategory Cmdlet #> }
Jak widać, dzieje się i to dużo. Zaczynając od początku, to co widać powyżej to tzw. proxy function. Co bystrzejsze oko pewnie zauważyło, funkcja ma dokładnie taką samą nazwę jak jeden z domyślnych cmdletów powershellowych. Proxy function powstały właśnie po to, aby umożliwić modyfikowanie działania wbudowanych cmdletów, czy też rozbudowywanie ich o nowe możliwości. Kod wydaje się być dosyć skomplikowany, ale zdecydowana większość została wygenerowana automatycznie za pomocą poniższych komend:
$cmd = New-Object System.Management.Automation.CommandMetaData (Get-Command Copy-Item) [System.Management.Automation.ProxyCommand]::Create($cmd)
Generują one szkielet cmdletu, który chcemy modyfikować. Całość trzeba jedynie umieścić w dowolnie nazwanej funkcji. Jeśli nie nazwiemy jej tak samo jak istniejący cmdlet to stworzymy wtedy jego kopię z nowymi funkcjonalnościami oraz nazwą.
Aby zdefiniować nowe parametry, należy dodać ich definicję w sekcji param(), tak jak przypadku zwykłej funkcji. W tym przypadku, jeśli podany zostanie nowy parametr -Unbuffered, zmienna scriptCmd, która decyduje o tym co faktycznie się wykona, zostanie zmodyfikowana tak, żeby uruchamiać narzędzie xcopy, które oferuje kopiowanie bez użycia bufora (przełącznik /J). Cmdlet Copy-Item domyślnie nie posiada takiej możliwości. Ale naprawdę ciekawie zaczyna się robić, dopiero po użyciu parametru -Progress. Najpierw uruchamiane jest narzędzie xcopy z poleceniem -PassThrough, dzięki czemu można zapisać wynik w zmiennej, a potem odczytać chociażby id stworzonego procesu.
Jednak najbardziej interesującym fragmentem w tej części kodu jest funkcja Get-IoCounterMb. Dlaczego powstała i dlaczego wygląda tak, a nie inaczej? Jako, że xcopy nie udostępnia żadnych informacji nt. postępu procesu kopiowania, stwierdziłem, że sprawdzę wielkość kopiowanego pliku, a potem będę w pętli odczytywać z performance counterów ilość megabajtów zapisywanych przez proces xcopy w danym momencie. Użyłem runspace’ów, ponieważ z nieznanych mi obecnie powodów powershell nie widział szukanego countera i zwracał błąd. Dopiero utworzenie nowego wątku, podczas tworzenia runspace’a, pozwoliło mi na odczytanie wartości countera dla procesu xcopy. Dlaczego nie użyłem jobów? Ponieważ joby tworzą nowy proces, a nie wątek w obrębie istniejącego procesu, a tym samym zużywają więcej zasobów. Potem scriptblock już tylko czeka na uruchomienie xcopy i od tego momentu pokazuje postęp za pomocą Write-Progress, dopóki proces nie przestanie istnieć. Okazało się, że nie da się tym sposobem zilustrować postępu kopiowania, ponieważ cmdlet Get-Counter, mimo ustawionego czasu odstępu na 1ms w pętli, nie jest w stanie tak szybko zwracać wyników. I tak, dla pliku o wielkości 16GB, pasek postępu kończy się na 12GB. Postanowiłem zostawić jednak ten fragment w celach dydaktycznych. :) Poza tym, w tym przypadku akurat chodziło mi o orientacyjną informację.
Jedna ważna rzecz na koniec. Jeśli proxy function ma taką samą nazwę jak oryginalny cmdlet to go nadpisuje. Dopiero w nowej sesji w której funkcja nie została załadowana będzie możliwe użycie domyślnej wersji cmdletu.
Kacper.
.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.
jako komentarz dodam trick: jeśli w PS komenda zostanie przesilona, to można zmusić PS do skorzystania z oryginału, podając pełną ścieżkę wraz z modułem. w tym przypadku będzie to:
Microsoft.PowerShell.Management\copy-item
na podobny problem natrafiłem przy SCVMM, który nadpisuje standardowe commandlety hyper-V. i tak aby skorzystać z oryginalnego get-vm trzeba go uruchamiać:
Hyper-v\get-vm
aby usunąć taką funkcję wystarczy … ją usunąć:
rm function:copy-item
w Internetach częściej wykorzystywanym sposobem jest napisanie własnego copy. tworzy się strumień, odczytuje się w chunkach po kilka KB i wyświetla progress bar [np. tu http://stackoverflow.com/questions/2434133/progress-during-large-file-copy-copy-item-write-progress] .pomysł z odczytem liczników wydajności – niezłe (:
niemniej nie mam zaufania to takich metod i pozostaje kwestia mechanizmów cache itd. biblioteka systemowa daje pewne gwarancje. na koniec na wszelki wypadek wolałbym sprawdzić CRC (;
eN.