feat: RDG qwinsta/logoff via WinRM on client workstation (0.11.6)

SAC resolves internal_ip to a registered Windows host and runs qwinsta/logoff synchronously over WinRM instead of queueing commands to the gateway agent.
This commit is contained in:
PTah
2026-06-20 15:36:12 +10:00
parent 2eb06acb5b
commit 75f4c475df
13 changed files with 612 additions and 174 deletions
+33 -19
View File
@@ -305,7 +305,8 @@
<div v-if="qwinstaModal.open" class="modal-backdrop" @click.self="closeQwinstaModal">
<div class="modal-card qwinsta-modal" role="dialog" aria-modal="true">
<h3>qwinsta событие #{{ qwinstaModal.eventId }}</h3>
<h3>qwinsta {{ qwinstaModal.clientHostname || qwinstaModal.target || `#${qwinstaModal.eventId}` }}</h3>
<p v-if="qwinstaModal.internalIp" class="muted qwinsta-meta">Клиентский ПК: {{ qwinstaModal.internalIp }}</p>
<p v-if="qwinstaModal.error" class="error">{{ qwinstaModal.error }}</p>
<p v-else-if="qwinstaModal.loading">Ожидание ответа агента</p>
<template v-else>
@@ -412,6 +413,9 @@ const qwinstaModal = ref({
eventId: 0,
actorUser: "",
commandUuid: "",
target: "",
clientHostname: "",
internalIp: "",
loading: false,
error: "",
stdout: "",
@@ -494,6 +498,12 @@ function pollQwinstaCommand(eventId: number, commandUuid: string, actorUser: str
}, 2000);
}
function applyCommandMeta(modal: typeof qwinstaModal.value, cmd: AgentCommandResponse) {
modal.target = cmd.target || "";
modal.clientHostname = cmd.client_hostname || "";
modal.internalIp = cmd.internal_ip || "";
}
async function runQwinsta(event: EventSummary) {
qwinstaLoadingId.value = event.id;
qwinstaModal.value = {
@@ -501,6 +511,9 @@ async function runQwinsta(event: EventSummary) {
eventId: event.id,
actorUser: event.actor_user || "",
commandUuid: "",
target: "",
clientHostname: "",
internalIp: "",
loading: true,
error: "",
stdout: "",
@@ -509,6 +522,7 @@ async function runQwinsta(event: EventSummary) {
};
try {
const cmd = await postEventQwinsta(event.id);
applyCommandMeta(qwinstaModal.value, cmd);
qwinstaModal.value.commandUuid = cmd.command_uuid;
if (applyCommandResult(event.id, event.actor_user || "", cmd)) {
return;
@@ -530,25 +544,20 @@ async function runLogoff(sessionId: number) {
modal.error = "";
try {
const cmd = await postEventLogoff(modal.eventId, sessionId);
if (cmd.status === "pending") {
const deadline = Date.now() + 60000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 2000));
const polled = await fetchEventAction(modal.eventId, cmd.command_uuid);
if (polled.status !== "pending") {
if (polled.status === "failed") {
modal.error = polled.result_stderr || "logoff failed";
} else {
modal.loading = true;
modal.stdout = "";
modal.sessions = [];
const again = await postEventQwinsta(modal.eventId);
pollQwinstaCommand(modal.eventId, again.command_uuid, modal.actorUser);
}
break;
}
}
applyCommandMeta(modal, cmd);
if (cmd.status === "failed") {
modal.error = cmd.result_stderr || cmd.result_stdout || "logoff failed";
return;
}
modal.loading = true;
modal.stdout = "";
modal.sessions = [];
const again = await postEventQwinsta(modal.eventId);
applyCommandMeta(modal, again);
if (applyCommandResult(modal.eventId, modal.actorUser, again)) {
return;
}
pollQwinstaCommand(modal.eventId, again.command_uuid, modal.actorUser);
} catch (e) {
modal.error = e instanceof Error ? e.message : "logoff error";
} finally {
@@ -849,6 +858,11 @@ onUnmounted(() => {
padding: 1rem 1.25rem;
}
.qwinsta-meta {
margin: 0 0 0.75rem;
font-size: 0.9rem;
}
.qwinsta-raw {
font-size: 0.8rem;
max-height: 12rem;