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.

