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:
parent
746ebcd9e3
commit
c19215068e
12 changed files with 383 additions and 217 deletions
|
|
@ -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
1
internal/dis/component/server/assets/dist/Admin.1f7e8ff8.js
vendored
Normal file
1
internal/dis/component/server/assets/dist/Admin.1f7e8ff8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
internal/dis/component/server/assets/dist/Admin.9f3b0d76.js
vendored
Normal file
1
internal/dis/component/server/assets/dist/Admin.9f3b0d76.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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" })
|
||||
})
|
||||
}
|
||||
72
internal/dis/component/server/assets/src/lib/remote/proto.ts
Normal file
72
internal/dis/component/server/assets/src/lib/remote/proto.ts
Normal 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue