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

@ -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);
});
}