PowerShell Universal (e il suo predecessore Universal Dashboard) è sempre stato ostico da debuggare. Quasi tutta la funzionalità avviene tramite runspace in background, quindi procedere passo-passo con un debugger richiede un po’ di acrobazie con gli strumenti. Inoltre non funzionava in remoto: fino a ora.
Debugger PowerShell Universal per VS Code
Con la versione 4.2 di PowerShell Universal e l’estensione Universal per VS Code, aggiungiamo la possibilità di eseguire il debug di script, Apps e API direttamente da VS Code. Può impostare breakpoint (in genere con Wait-Debugger), scorrere il codice passo-passo e ispezionare le variabili. Funziona in remoto su una connessione WebSocket verso il server PowerShell Universal.
Una nuova vista Processes sotto l’albero Platform offre una connessione a tutti i runspace del server gestiti da PowerShell Universal. Può selezionare un processo e fare clic su Attach per collegare il debugger al processo.
Una volta collegato, avrà l’esperienza completa di debug PowerShell in Visual Studio Code. Può visualizzare il codice, avanzare passo-passo e ispezionare le variabili.
Per quanto l’esperienza sia notevole, è dovuta soprattutto all’hosting di un’istanza di PowerShell Editor Services nel servizio PSU e alla comunicazione diretta via WebSocket. Entriamo nei dettagli tecnici di seguito.
Debug Adapter Protocol
Il Debug Adapter Protocol è una specifica per implementare debugger. È integrato sia in Visual Studio Code sia in Visual Studio, così una sola implementazione del protocollo funziona in entrambi gli editor. Nel mondo PowerShell, si tratta di PowerShell Editor Services.
VS Code invia richieste in formato JSON al processo PowerShell Editor Services e PowerShell Editor Services risponde con risposte JSON. Invece di riscrivere completamente un debugger, ora possiamo sfruttarlo per offrire servizi di debug.
PowerShell Editor Services
PowerShell Editor Services è un’implementazione server del Debug Adapter Protocol. Viene di fatto distribuito come modulo PowerShell caricato in un processo PowerShell e poi comunica via RPC. È la stessa implementazione usata nell’estensione PowerShell per Visual Studio Code. Per lavorare con i servizi, si avvia un processo PowerShell con gli argomenti da riga di comando appropriati di editor services.
In genere ha un aspetto simile a questo.
pwsh -NoLogo -NoProfile -Command "$PSES_BUNDLE_PATH/PowerShellEditorServices/Start-EditorServices.ps1 -BundledModulesPath $PSES_BUNDLE_PATH -LogPath $SESSION_TEMP_PATH/logs.log -SessionDetailsPath $SESSION_TEMP_PATH/session.json -FeatureFlags @() -AdditionalModules @() -HostName 'My Client' -HostProfileId 'myclient' -HostVersion 1.0.0 -LogLevel Normal"
Da lì possiamo leggere il file session.json scritto dal servizio. Per impostazione predefinita include i nomi delle named pipe utilizzabili per il Language Service Protocol e il Debug Adapter Protocol. Una volta connessa una named pipe, possiamo inviare e ricevere messaggi JSON per controllare il debugger e ottenere lo stato attuale del runspace.
Collegare PowerShell Universal
Invece di esporre una named pipe all’esterno, è molto più semplice esporre un WebSocket che riusa autenticazione, autorizzazione e porta del server web. Abbiamo aggiunto un hub SignalR che riceve stringhe da un client e le invia al processo PowerShell Editor Services.
Di fatto incanaliamo i messaggi. È stato necessario un po’ di lavoro per adattarsi al protocollo DAP e alle comunicazioni named pipe, ma è stato minimo. Avviamo PSES in un nuovo processo e solo il servizio debugger, non quello del linguaggio.
Collegare il debugger VS Code
Ora che abbiamo un WebSocket e un processo PowerShell Editor Services, possiamo integrarlo in strumenti come VS Code. In pratica abbiamo implementato un adattatore di debug personalizzato. Di solito sarebbe molto lavoro, ma poiché comunichiamo davvero con PSES sul WebSocket, possiamo instradare richieste e risposte senza implementare l’intero protocollo.
export class UniversalDebugAdapter implements vscode.DebugAdapter {
constructor(context: vscode.ExtensionContext) {
const connectionName = context.globalState.get("universal.connection");
const settings = load();
var appToken = settings.appToken;
var url = settings.url;
var rejectUnauthorized = true;
var windowsAuth = false;
if (connectionName && connectionName !== 'Default') {
const connection = settings.connections.find(m => m.name === connectionName);
if (connection) {
appToken = connection.appToken;
url = connection.url;
rejectUnauthorized = !connection.allowInvalidCertificate;
}
}
this.hubConnection = new HubConnectionBuilder()
.withUrl(`${url}/debuggerhub`, { accessTokenFactory: () => appToken })
.configureLogging(LogLevel.Information)
.build();
this.hubConnection.on("message", (message: string) => {
const protocolMessage = JSON.parse(message) as DebugProtocol.ProtocolMessage;
this.handleMessage(protocolMessage);
});
this.hubConnection.onclose(() => {
vscode.window.showInformationMessage("Disconnected from PowerShell Universal Debugger.");
});
}
private hubConnection: HubConnection;
private sendMessage = new vscode.EventEmitter<DebugProtocol.ProtocolMessage>();
readonly onDidSendMessage: vscode.Event<DebugProtocol.ProtocolMessage> = this.sendMessage.event;
handleMessage(message: DebugProtocol.ProtocolMessage): void {
if (this.hubConnection.state === 'Disconnected') {
this.hubConnection.start().then(() => {
this.handleMessage(message);
});
return;
}
switch (message.type) {
case 'request':
this.hubConnection.send("message", JSON.stringify(message));
break;
case 'response':
this.sendMessage.fire(message);
break;
case 'event':
this.sendMessage.fire(message);
break;
}
}
dispose() {
this.hubConnection.stop();
}
}
Dopo aver implementato l’adattatore di debug, abbiamo aggiunto una vista ad albero per selezionare un processo e un runspace e collegarsi. Queste informazioni le mostravamo già nella console di amministrazione di PowerShell Universal, quindi mostrarle anche nell’albero VS Code è stato banale.
Quando l’utente seleziona un processo e fa clic su collega, inviamo una richiesta all’adattatore per collegarsi al processo e PSES si occupa del resto.
export const attachRunspace = async (runspace: RunspaceTreeItem, context: vscode.ExtensionContext) => {
await vscode.debug.startDebugging(undefined, {
name: "PowerShell Universal",
type: "powershelluniversal",
request: "attach",
processId: runspace.runspace.processId,
runspaceId: runspace.runspace.id
});
};
Conclusione
Questa nuova funzionalità sarà molto utile per chi lavora con qualsiasi tipo di script PowerShell in Universal. Anche se l’uso è un po’ avanzato, offre un’esperienza di debug molto migliore di quella precedente. La funzionalità verrà fornita come plugin in PowerShell Universal v4.2. Renderemo obsoleta l’esperienza di debug nel browser e la rimuoveremo in una versione futura.
Sarebbe possibile estendere questa funzionalità oltre il debug degli script PowerShell Universal. Potrebbe aprire un intero mondo di debug remoto di PowerShell su WebSocket.
Pronto a iniziare a creare? Scarichi PowerShell Universal.

Adam Driscoll