remote: Allow protocol input & cancellation

This commit reworks the protocol being used on top of websockets. It now
permits sending input to the server, and interrupting the remote
process.
This commit is contained in:
Tom Wiesing 2023-02-28 21:34:58 +01:00
parent 746ebcd9e3
commit c19215068e
No known key found for this signature in database
12 changed files with 383 additions and 217 deletions

View file

@ -24,12 +24,12 @@ var AssetsUser = Assets{
// AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/this-is-fine/User.b2f9a57c.js"></script><script type="module" src="/this-is-fine/User.e0367d79.js"></script><script type="module" src="/this-is-fine/Default.38d394c2.js"></script><script src="/this-is-fine/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/this-is-fine/Admin.15462d3d.js"></script><script src="/this-is-fine/Admin.2889b18a.js" nomodule="" defer></script>`,
Scripts: `<script nomodule="" defer src="/this-is-fine/User.b2f9a57c.js"></script><script type="module" src="/this-is-fine/User.e0367d79.js"></script><script type="module" src="/this-is-fine/Default.38d394c2.js"></script><script src="/this-is-fine/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/this-is-fine/Admin.1f7e8ff8.js"></script><script src="/this-is-fine/Admin.9f3b0d76.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/this-is-fine/Default.938b4407.css"><link rel="stylesheet" href="/this-is-fine/Admin.a1e05c23.css"><link rel="stylesheet" href="/this-is-fine/User.840de3b4.css"><link rel="stylesheet" href="/this-is-fine/User.68febbf8.css"><link rel="stylesheet" href="/this-is-fine/Admin.6d2ae968.css">`,
}
// AssetsAdminProvision contains assets for the 'AdminProvision' entrypoint.
var AssetsAdminProvision = Assets{
Scripts: `<script nomodule="" defer src="/this-is-fine/User.b2f9a57c.js"></script><script nomodule="" defer src="/this-is-fine/Admin.2889b18a.js"></script><script type="module" src="/this-is-fine/User.e0367d79.js"></script><script type="module" src="/this-is-fine/Admin.15462d3d.js"></script><script type="module" src="/this-is-fine/Default.38d394c2.js"></script><script src="/this-is-fine/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/this-is-fine/AdminProvision.3cf9e19e.js"></script><script src="/this-is-fine/AdminProvision.d195fd59.js" nomodule="" defer></script>`,
Scripts: `<script nomodule="" defer src="/this-is-fine/User.b2f9a57c.js"></script><script nomodule="" defer src="/this-is-fine/Admin.9f3b0d76.js"></script><script type="module" src="/this-is-fine/User.e0367d79.js"></script><script type="module" src="/this-is-fine/Admin.1f7e8ff8.js"></script><script type="module" src="/this-is-fine/Default.38d394c2.js"></script><script src="/this-is-fine/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/this-is-fine/AdminProvision.3cf9e19e.js"></script><script src="/this-is-fine/AdminProvision.d195fd59.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/this-is-fine/Default.938b4407.css"><link rel="stylesheet" href="/this-is-fine/Admin.a1e05c23.css"><link rel="stylesheet" href="/this-is-fine/User.840de3b4.css"><link rel="stylesheet" href="/this-is-fine/User.68febbf8.css"><link rel="stylesheet" href="/this-is-fine/Admin.6d2ae968.css"><link rel="stylesheet" href="/this-is-fine/AdminProvision.38d394c2.css">`,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,5 @@
import "./index.css"
import connectSocket from './socket'
import { Mutex } from 'async-mutex'
import callServerAction, { ResultMessage } from './proto'
type Println = ((line: string, flush?: boolean) => void) & {
paintedFrames: number;
@ -120,15 +119,16 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
modal.append(target)
// create a button to eventually close everything
const button = document.createElement("button")
button.className = "pure-button pure-button-success"
button.append(typeof opts?.onClose === 'function' ? "Close & Finish" : "Close")
const finishButton = document.createElement("button")
finishButton.className = "pure-button pure-button-success"
finishButton.append(typeof opts?.onClose === 'function' ? "Close & Finish" : "Close")
let result = {success: false, error: "unknown error"};
button.addEventListener('click', function (event) {
finishButton.addEventListener('click', (event) => {
event.preventDefault();
if (typeof opts?.onClose === 'function') {
button.setAttribute('disabled', 'disabled')
finishButton.setAttribute('disabled', 'disabled')
target.innerHTML = 'Finishing up ...'
opts.onClose(result.success)
return;
@ -136,18 +136,28 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
modal.parentNode?.removeChild(modal);
})
const cancelButton = document.createElement("button")
cancelButton.className = "pure-button pure-button-danger"
cancelButton.setAttribute("disabled", "disabled")
cancelButton.append("Cancel")
modal.append(cancelButton)
const onbeforeunload = window.onbeforeunload;
window.onbeforeunload = () => "A remote session is in progress. Are you sure you want to leave?";
// when closing, add a button to the modal!
let didClose = false
const close = function () {
if (didClose) return
didClose = true
const close = (result: ResultMessage) => {
if (result.success) {
println('Process completed successfully. ', true);
} else {
println('Process reported error: ' + result.message, true);
}
window.onbeforeunload = onbeforeunload;
modal.append(button)
modal.removeChild(cancelButton)
modal.append(finishButton)
// DEBUG: print terminal stats!
// const quota = (println.paintedFrames / (println.missedFrames + println.paintedFrames)) * 100
// println(`Terminal: painted=${println.paintedFrames} missed=${println.missedFrames} (${quota}%)`, true)
@ -155,38 +165,28 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
println("Connecting ...", true)
const mutex = new Mutex();
// connect to the socket and send the action
connectSocket((socket) => {
println("Connected", true)
socket.send(action)
params.forEach(p => socket.send(p))
}, (data) => {
mutex.runExclusive(async () => {
if (data instanceof Blob) {
result = JSON.parse(await data.text());
return
}
callServerAction(
{
'name': action,
'params': params,
},
(
send: (text: string) => void,
cancel: () => void,
) => {
cancelButton.removeAttribute("disabled")
cancelButton.addEventListener("click", (event) => {
event.preventDefault()
println(data);
})
}).then(() => {
mutex.runExclusive(async () => {
if(result.success) {
println("Process finished successfully. ")
} else {
println("Process failed: " + result.error)
}
println("Connection closed. ", true)
close();
})
}).catch(() => {
mutex.runExclusive(async () => {
println("Connection errored. ", true)
result = { success: false, error: "connection errored" }
close();
})
});
cancel()
})
println("Connected", true)
},
println
).then(close)
.catch(() => {
close({ 'success': false, 'message': "connection closed unexpectedly" })
})
}

View file

@ -0,0 +1,72 @@
import { Mutex } from "async-mutex";
export type CallMessage = { name: string; params?: string[] | null }
export type ResultMessage = { success: boolean; message?: string; }
export type SignalMessage = { signal: string; }
function isResultMessage(value: any): value is ResultMessage {
return typeof value === 'object' &&
Object.prototype.hasOwnProperty.call(value, 'success') && typeof value['success'] === 'boolean' &&
(!Object.prototype.hasOwnProperty.call(value, 'message') || (value['message'] === undefined || typeof value['message'] === 'string'));
}
/**
* Opens a WebSocket connection and calls a server action
* @param call Function to call
* @param onOpen callback for once the connection is opened. The send function can be used to send additional text to the server.
* @param onText called when the connection receives some text
* @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors.
*/
export default async function callServerAction(
call: CallMessage,
onOpen: (send: (text: string) => void, cancel: () => void) => void,
onText: (text: string) => void,
): Promise<ResultMessage> {
return new Promise((rs, rj) => {
const mutex = new Mutex();
const socket = new WebSocket(location.href.replace('http', 'ws'));
let result: ResultMessage;
socket.onmessage = (msg) => {
mutex.runExclusive(async () => {
if (typeof msg.data === 'string') {
onText(msg.data)
return
}
if (msg.data instanceof Blob) {
const object = JSON.parse(await msg.data.text())
if (isResultMessage(object)) {
result = {'success': object['success'], 'message': object['message']}
return
}
}
console.warn('Unknown message', msg)
})
}
socket.onclose = () => {
mutex.runExclusive(() => rs(result));
};
socket.onerror = rj; // if an error occurs, close the socket
socket.onopen = () => {
const blob = new Blob([JSON.stringify(call)]);
socket.send(blob)
onOpen(
(text: string) => {
if (typeof text !== 'string') {
console.warn('Ignoring send() call with unknown type', text)
return
}
socket.send(text)
},
() => {
const blob = new Blob([JSON.stringify({'signal': 'cancel'})]);
socket.send(blob)
}
)
}
})
}

View file

@ -1,11 +0,0 @@
export default function connectSocket(onOpen: (socket: WebSocket) => void, onData: (data: any) => void): Promise<CloseEvent> {
return new Promise((rs, rj) => {
const socket = new WebSocket(location.href.replace('http', 'ws'));
socket.onclose = rs;
socket.onerror = rj;
socket.onmessage = (ev) => onData(ev.data)
socket.onopen = () => onOpen(socket);
});
}