PowerShell Universal (y su predecesor Universal Dashboard) siempre ha sido complicado de depurar. Casi toda la funcionalidad se realiza mediante runspaces en segundo plano, así que avanzar paso a paso con un depurador exige lidiar con varias herramientas. Además, no funcionaba en remoto: hasta ahora.
Depurador de PowerShell Universal para VS Code
Con la versión 4.2 de PowerShell Universal y la extensión Universal para VS Code, añadimos la posibilidad de depurar scripts, Apps y API directamente desde VS Code. Puede establecer puntos de interrupción (normalmente con Wait-Debugger), avanzar por el código e inspeccionar variables. Funciona en remoto mediante una conexión WebSocket al servidor de PowerShell Universal.
Una nueva vista Processes bajo el árbol Platform ofrece una conexión a todos los runspaces del servidor gestionados por PowerShell Universal. Puede seleccionar un proceso y pulsar Attach para adjuntar el depurador al proceso.
Una vez adjunto, dispondrá de la experiencia completa de depuración de PowerShell en Visual Studio Code. Puede ver el código, avanzarlo paso a paso e inspeccionar variables.
Aunque la experiencia es bastante impresionante, se debe sobre todo a alojar una instancia de PowerShell Editor Services dentro del servicio PSU y comunicarse con ella directamente por WebSocket. Entramos en los detalles técnicos a continuación.
Debug Adapter Protocol
El Debug Adapter Protocol es una especificación para implementar depuradores. Está integrado en Visual Studio Code y Visual Studio, de modo que una sola implementación del protocolo funciona en ambos editores. En el mundo PowerShell, eso es PowerShell Editor Services.
VS Code envía solicitudes con formato JSON al proceso de PowerShell Editor Services, y PowerShell Editor Services responde con respuestas JSON. En lugar de reescribir por completo un depurador, ahora podemos aprovecharnos de él para ofrecer servicios de depuración.
PowerShell Editor Services
PowerShell Editor Services es una implementación de servidor del Debug Adapter Protocol. Se entrega de hecho como un módulo de PowerShell que se carga en un proceso de PowerShell y luego se comunica por RPC. Es la misma implementación que usa la extensión de PowerShell para Visual Studio Code. Para trabajar con los servicios, se inicia un proceso de PowerShell con los argumentos de línea de comandos adecuados de editor services.
Suele tener este aspecto.
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"
A partir de ahí, podemos leer el archivo session.json que escribe el servicio. De forma predeterminada, incluye nombres de named pipes utilizables para el Language Service Protocol y el Debug Adapter Protocol. Una vez conectada una named pipe, podemos enviar y recibir mensajes JSON para controlar el depurador y obtener el estado actual del runspace.
Conectar PowerShell Universal
En lugar de exponer una named pipe al exterior, es mucho más sencillo exponer un WebSocket que reutilice la autenticación, la autorización y el puerto del servidor web. Hemos añadido un hub de SignalR que recibe cadenas de un cliente y las envía al proceso de PowerShell Editor Services.
De hecho, canalizamos los mensajes. Hizo falta algo de trabajo para adaptarse al protocolo DAP y a las comunicaciones por named pipe, pero fue mínimo. Iniciamos PSES en un proceso nuevo y solo el servicio de depuración, no el de lenguaje.
Conectar el depurador de VS Code
Ahora que tenemos un WebSocket y un proceso de PowerShell Editor Services, podemos integrarlo en herramientas como VS Code. En la práctica, hemos implementado un adaptador de depuración personalizado. Normalmente sería mucho trabajo, pero como nos comunicamos realmente con PSES por el WebSocket, podemos reenviar solicitudes y respuestas sin implementar todo el protocolo.
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();
}
}
Una vez implementado el adaptador de depuración, añadimos una vista de árbol para seleccionar un proceso y un runspace y adjuntarse a ellos. Ya mostrábamos esta información en la consola de administración de PowerShell Universal, así que mostrarla también en el árbol de VS Code fue trivial.
Cuando el usuario selecciona un proceso y pulsa adjuntar, enviamos una solicitud al adaptador para adjuntarse al proceso y PSES se encarga 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
});
};
Conclusión
Esta nueva función será muy útil para quienes trabajen con cualquier tipo de script de PowerShell en Universal. Aunque el uso es un poco avanzado, ofrece una experiencia de depuración mucho mejor que la anterior. Esta función se publicará como complemento en PowerShell Universal v4.2. Dejaremos obsoleta la experiencia de depuración en el navegador y la eliminaremos en una versión futura.
Sería posible ampliar esta funcionalidad más allá de depurar scripts de PowerShell Universal. Podría abrir todo un mundo de depuración remota de PowerShell a través de WebSockets.
¿Listo para crear? Descargue PowerShell Universal.

Adam Driscoll