Client-free PowerShell Remoting - a Live Mesh Command Line
Once problem that often arises when trying to manage machines is when the management layer itself is the thing you need to diagnose. For example, trying to diagnose Remote Desktop connectivity issues when port 3389 is blocked, or using PowerShell Remoting when WSMAN is misconfigured.
Alternatively, you might not have the client you need to manage the machine – such as an SSH client, or a version of PowerShell V2 installed.
One way to get around both of these problems is another communication channel. It may be of lower fidelity, but can help you get your job done.
One perfect example of an alternative communication channel is any of the many synchronization tools out there: Live Mesh, Syncplicity, FolderShare, etc. In addition to managing connectivity, they let you broadcast messages (by way of files) between connected computers. Let’s use that as our communication protocol:
The core of this experience comes from a PowerShell Mesh Monitor. This script monitors a computer-specific directory for new scripts that appear. When it sees a new script, it processes it, captures the output, and then stores the output in a similarly named file. You can then retrieve the file, review its contents, and continue to manage the system this way:
#requires -version 2.0
## Assuming the script is located in the (Mesh-synched folder)
## D:\Documents\Mesh
## and the computer name is
## "MESH-CLIENT"
## We'll monitor D:\Documents\Mesh\Computers\MESH-CLIENT for PowerShell
## scripts to run.
##
## Run this script on any machines that you want listening for scriptable
## Mesh management commands:
##
## powershell -noprofile -file D:\Documents\Mesh\Start-MeshMonitor.ps1
##
## Commands that impact the shell's state (such as current location) are
## persisted for future commands. If you want to share data between scripts
## use global variables.
## Set up the environment
$host.UI.RawUI.WindowTitle = "PowerShell Mesh Monitor"
$myWindowHandle = (Get-Process -Id $pid).MainWindowHandle
## Determine the monitoring directory. Create it if we have to.
$scriptLocation = Split-Path -Parent $myInvocation.MyCommand.Definition
$monitorPath = Join-Path $scriptLocation "Computers\$($env:COMPUTERNAME)"
if(-not (Test-Path $monitorPath))
{
[void] (New-Item -Type Directory $monitorPath -Force)
}
## Create a P/Invoke type that gives us access to the window
## management functions (show / hide)
$windowUtils = Add-Type -memberDefinition @"
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
"@ -name "Win32ShowWindowAsync" -namespace Win32Functions -passThru
## Hide ourselves
[void] $windowUtils::ShowWindow($myWindowHandle, 0)
Set-Location $monitorPath
## Continuously go through all PowerShell scripts in the drop location,
## run them, and store their output back in the drop location.
while($true)
{
## Show the window if they create a file called "SHOW"
if(Test-Path SHOW)
{
[void] $windowUtils::ShowWindow($myWindowHandle, 1)
Remove-Item SHOW
}
foreach($inputFile in Get-ChildItem $monitorPath -Filter *.ps1 | Sort LastWriteTime)
{
$script = $inputFile.FullName
"Processing input script $script"
& { trap { $_; continue }; & $script } > "$script.output.txt" 2>&1
Move-Item $script "$script.processed"
}
Start-Sleep 1
}
With file synchronization as a back-end, you can now write a thin remoting client in nearly any language, and nearly any OS – as long as it can read and write files, and is supported by your file synchronization client. Taking this same approach, you could also use email, FTP, web pages, or nearly anything else as a communication mechanism.
On top of that, you might want to build a primitive interactive remoting experience. Once Live Mesh completes their Windows Mobile client, I’ll be doing that ASAP. Here’s a PowerShell script that demonstrates the approach:
## Assuming the script is located in the (Mesh-synched folder)
## D:\Documents\Mesh
## and the computer name is
## "MESH-CLIENT"
## We'll drop scripts in D:\Documents\Mesh\Computers\MESH-CLIENT and
## process the resulting output files.
##
## Commands that impact the shell's state (such as current location) are
## persisted for future commands. If you want to share data between scripts
## use global variables.
param($computerName)
## If they specified a computer name, change to that directory
if($computerName)
{
$scriptLocation = Split-Path -Parent $myInvocation.MyCommand.Definition
$monitorPath = Join-Path $scriptLocation "Computers\$computerName"
if(-not (Test-Path $monitorPath))
{
throw "$computerName does not have a Mesh Monitor directory"
}
Set-Location $monitorPath
}
$remotePromptText = "[MESH/$(Split-Path . -Leaf)] PS >"
## Invoke a command and retrieve its output
function InvokeCommand($command)
{
$filename = [GUID]::NewGuid().ToString() + ".ps1"
$outputFilename = "$filename.output.txt"
$processedFilename = "$filename.processed"
$command > $filename
while(-not (Test-Path $processedFilename)) { Start-Sleep -m 100 }
while(-not (Test-Path $outputFilename)) { Start-Sleep -m 100 }
Get-Content $outputFilename
## Clean up our temporary files
Remove-Item $outputFilename
Remove-Item $processedFilename
}
## Get a command from the user, invoke it, and display
## the results
while($true)
{
Write-Host -NoNewLine $remotePromptText
$command = $host.UI.ReadLine()
if($command -eq "exit")
{
break
}
InvokeCommand ($command + " | Out-String -Width $($host.UI.RawUI.BufferSize.Width - 1)")
}