Copying files via PowerShell remoting channel

There are a few ways to do this, and in PowerShell 3.0 you can even just use the Copy-File cmdlet.

However, I came up with the following solution which is fairly reliable, and avoids any issues when transferring files larger than the session’s restriction on size of deserialised objects (by default 10MB).

Note that $localPath and $remotePath are set to what you’d expect. $Session is a PS Remoting Session created with, e.g. New-PSSession.

(ReportInfo and ReportError are just functions which output to either the console or to TeamCity depending on where the script is being run, this is part of our testing system…)

# Use .NET file handling for speed
$content = [Io.File]::ReadAllBytes( $localPath )
$contentsizeMB = $content.Count / 1MB + 1MB

ReportInfo "Copying $fileName from $localPath to $remotePath on $Connection.Name ..."

# Open local file
try
{
[IO.FileStream]$filestream = [IO.File]::OpenRead( $localPath )
ReportInfo "Opened local file for reading"
}
catch
{
ReportError "Could not open local file $localPath because:" $_.Exception.ToString()
Return $false
}

# Open remote file
try
{
Invoke-Command -Session $Session -ScriptBlock {
Param($remFile)
[IO.FileStream]$filestream = [IO.File]::OpenWrite( $remFile )
} -ArgumentList $remotePath
ReportInfo "Opened remote file for writing"
}
catch
{
ReportError "Could not open remote file $remotePath because:" $_.Exception.ToString()
Return $false
}

# Copy file in chunks
$chunksize = 1MB
[byte[]]$contentchunk = New-Object byte[] $chunksize
$bytesread = 0
while (($bytesread = $filestream.Read( $contentchunk, 0, $chunksize )) -ne 0)
{
Try
{
$percent = $filestream.Position / $filestream.Length
ReportInfo ("Copying {0}, {1:P2} complete, sending {2} bytes" -f $fileName, $percent, bytesread)
Invoke-Command -Session $Session -ScriptBlock {
Param($data, $bytes)
$filestream.Write( $data, 0, $bytes )
} -ArgumentList $contentchunk,$bytesread
}
Catch
{
ReportError "Could not copy $fileName to $($Connection.Name) because:" $_.Exception.ToString()
Return $false
}
}

# Close remote file
try
{
Invoke-Command -Session $Session -ScriptBlock {
$filestream.Close()
}
ReportInfo "Closed remote file, copy complete"
}
catch
{
ReportError "Could not close remote file $remotePath because:" $_.Exception.ToString()
Return $false
}

# Close local file
try
{
$filestream.Close()
ReportInfo "Closed local file, copy complete"
}
catch
{
ReportError "Could not close local file $localPath because:" $_.Exception.ToString()
Return $false
}

The chunk size is set to 1MB as it seems a good compromise given the 10MB restriction. Why not just pass through the IO.FileStream object and perform the loop remotely? Well, I’ve had issues in the past with doing that as the remote end tends to dial back to the local end in order to interact with the object rather than using the existing TCP connection. Safer to just chunk the contents over.

Counting things in WMI

So, say you’re trying to get a count of the number of SMS_Package objects in the SCCM WMI interface, perhaps matching some parameter. You can easily do this using WQL and the SELECT COUNT(*) function, e.g.:

SELECT COUNT(*) FROM SMS_Package WHERE Name='Blah'"

This can be executed on a WqlConnectionManager object’s QueryProcessor, via the ExecuteQuery method. The return from these is always an IResultObject (which is weird kind of object which can be both one or many objects at once – it wraps up other objects and presents a standard interface to permit you to enumerate them without being aware of their type, kind of like PSObject).

As detailed here in MSDN results from queries involving such WQL statements as COUNT come wrapped up in a __Generic class. This means in practice that the Count property (which contains the output of the COUNT(*)) is attached to the IResultObject’s first child.

So overall you get this:

    IResultObject packageWithName = Connection.QueryProcessor.ExecuteQuery(String.Format("SELECT COUNT(*) FROM SMS_Package WHERE Name='{0}'", PackageName));

int count = 0;

foreach (IResultObject collection in packageWithName)
{
count = collection["Count"].IntegerValue;
}

this.WriteDebug(String.Format("Count of existing packages is: {0}", count));

(This is code from inside a PSCmdlet derived class in case you’re wondering what this refers to).

The utility of this, of course, is to ensure that you don’t add more than one package with the same name since it should be unique. It’s slightly inelegant accessing the child by using foreach, but I haven’t worked out a better way to do it (documentation for IResultObject isn’t particularly great).