From 6fbdb228c0796f497a0894eaa0e39786860deaef Mon Sep 17 00:00:00 2001 From: Lars Windolf Date: Sun, 3 Sep 2023 20:48:49 +0200 Subject: [PATCH] Refactor to ES6 modules. --- backend/wurm | 6 +- frontend/css/styles.css | 7 +- frontend/index.html | 46 ++++++--- frontend/js/{main.js => app.js} | 125 +++++------------------- frontend/js/notebook.js | 95 ++++++++++++++++++ frontend/js/probeapi.js | 23 ++--- frontend/js/renderer/netmap.js | 22 ++--- frontend/js/renderer/perf-flamegraph.js | 8 +- frontend/js/settings.js | 13 ++- frontend/notebooks/default.md | 22 +++++ frontend/notebooks/systemd.md | 8 ++ frontend/worker.js | 4 +- 12 files changed, 227 insertions(+), 152 deletions(-) rename frontend/js/{main.js => app.js} (71%) create mode 100644 frontend/js/notebook.js create mode 100644 frontend/notebooks/default.md create mode 100644 frontend/notebooks/systemd.md diff --git a/backend/wurm b/backend/wurm index 5780860..b29ffcb 100755 --- a/backend/wurm +++ b/backend/wurm @@ -1,6 +1,8 @@ #!/bin/bash # Start nodejs server if needed, always start in background -if ! pgrep -f WurmTermBackend >/dev/null; then +if ! pgrep -f WurmTermBacken >/dev/null; then (nohup node server.js >/dev/null 2>&1 &) -fi \ No newline at end of file +else + echo "Already running." +fi diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 478b9d7..911d38e 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -36,7 +36,7 @@ img.emojione { #visualizedHost { font-weight: bold; - color: white; + color: #333; } #visualContainer { display: none; @@ -114,7 +114,7 @@ img.emojione { } #nodes .node .name { - color:white; + color:#333; font-weight:bold; cursor:pointer; } @@ -238,6 +238,7 @@ span.severity_warning { #menu a { color: #AAC; text-decoration: none; + cursor: pointer; } #menu a:hover { text-decoration: underline; @@ -280,7 +281,7 @@ span.severity_warning { #settings .probe { display: inline-block; - background: #444; + background: #bbb; margin:3px; padding:3px 6px; border-radius: 3px; diff --git a/frontend/index.html b/frontend/index.html index 7bd4819..c545e07 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,9 +13,9 @@
@@ -51,7 +51,14 @@ Host + + + Notebook + + +
@@ -79,11 +86,8 @@

Backend Endpoint

- - - - - + + (function() { + 'use strict'; + + setupApp(); + })(); + diff --git a/frontend/js/main.js b/frontend/js/app.js similarity index 71% rename from frontend/js/main.js rename to frontend/js/app.js index 3527ca4..c6f8730 100644 --- a/frontend/js/main.js +++ b/frontend/js/app.js @@ -1,12 +1,19 @@ // vim: set ts=4 sw=4: -/*jshint esversion: 6 */ +/* jshint esversion: 6 */ -//import sb{ StarboardNotebookElement } from "./lib/starboard-notebook.js"; +import { registerStarboardShellCellType } from './notebook.js'; +import { settingsDialog, settingsLoad } from './settings.js'; +import { ProbeAPI } from './probeapi.js'; + +import { perfRenderer } from './renderer/perf-flamegraph.js'; +import { netmapRenderer } from './renderer/netmap.js'; -var settings; var hosts = {}; +var renderers = { + 'netmap': netmapRenderer, + 'perfFlameGraph': perfRenderer +}; var extraHosts = []; // list of hosts manually added -var pAPI; function multiMatch(text, severities) { var matchResult; @@ -64,7 +71,7 @@ function renderTable(d) { } function triggerProbe(host, name) { - pAPI.probe(host, name, probeResultCb, probeErrorCb); + ProbeAPI.probe(host, name, probeResultCb, probeErrorCb); } function probeErrorCb(e, probe, h) { @@ -112,8 +119,7 @@ function visualizeHost(host, renderer) { $('#renderer').val(renderer); $('#visual').empty().height(600); try { - var r = new renderers[renderer](); - r.render(pAPI, '#visual', host); + renderers[renderer](ProbeAPI, '#visual', host); } catch(e) { $('#visual').html('ERROR: Rendering failed!'); console.error(`render Error: host=${host} ${e}`); @@ -121,7 +127,7 @@ function visualizeHost(host, renderer) { } function addHost(h) { - pAPI.start(h, probeResultCb, probeErrorCb); + ProbeAPI.start(h, probeResultCb, probeErrorCb); var hId = strToId(h); if(!$(`#${hId}`).length) { @@ -234,7 +240,7 @@ function updateHosts(d) { var h = $(n).data('host'); if(!d.includes(h) && !extraHosts.includes(h) && (h !== 'localhost')) { console.log('stopping '+h); - pAPI.stop(h); + ProbeAPI.stop(h); $(n).addClass('disconnected'); } }); @@ -294,105 +300,22 @@ function view(id) { $('starboard-notebook').show(); else $('starboard-notebook').hide(); -} - -// Starboard-Notebook has no native shell type, so we register one -// see CoffeeScript example: https://starboard.gg/gz/coffeescript-custom-cell-type-n1VJRGC -function registerStarboardShellCellType() { - const StarboardTextEditor = runtime.exports.elements.StarboardTextEditor; - const ConsoleOutputElement = runtime.exports.elements.ConsoleOutputElement; - const cellControlsTemplate = runtime.exports.templates.cellControls; - - const SHELL_CELL_TYPE_DEFINITION = { - name: "Shell", - cellType: ["shell"], - createHandler: (cell, runtime) => new ShellCellHandler(cell, runtime), - } - - class ShellCellHandler { - constructor(cell, runtime) { - this.cell = cell; - this.runtime = runtime; - } - - getControls() { - const runButton = { - icon: "bi bi-play-circle", - tooltip: "Run", - callback: () => this.runtime.controls.emit({id: this.cell.id, type: "RUN_CELL"}), - }; - return cellControlsTemplate({ buttons: [runButton] }); - } - - attach(params) { - this.elements = params.elements; - const topElement = this.elements.topElement; - lit.render(this.getControls(), this.elements.topControlsElement); - - this.editor = new StarboardTextEditor(this.cell, this.runtime, {language: "shell"}); - topElement.appendChild(this.editor); - } - async run() { - const cmd = this.cell.textContent; - this.outputElement = new ConsoleOutputElement(); - - lit.render(html`${this.outputElement}`, this.elements.bottomElement); - - pAPI.run($('#notebook-host').val(), 5, cmd).then((d) => { - const val = d.stdout+"\n"+d.stderr; - window.$_ = val; - this.outputElement.addEntry({ - method: "result", - data: [val] - }); - return val - }).catch((d) => { - this.outputElement.addEntry({ - method: "error", - data: [d.error] - }); - }); - - return undefined; - } - - focusEditor() { - this.editor.focus(); - } - - async dispose() { - this.editor.remove(); - } - } - - runtime.definitions.cellTypes.map.delete('esm'); - runtime.definitions.cellTypes.map.delete('js'); - runtime.definitions.cellTypes.map.delete('javascript'); - runtime.definitions.cellTypes.map.delete('css'); - runtime.definitions.cellTypes.map.delete('html'); - runtime.definitions.cellTypes.map.delete('python'); - runtime.definitions.cellTypes.map.delete('python3'); - runtime.definitions.cellTypes.map.delete('ipython3'); - runtime.definitions.cellTypes.map.delete('py'); - runtime.definitions.cellTypes.map.delete('pypy'); - runtime.definitions.cellTypes.map.delete('latex'); - - runtime.definitions.cellTypes.register("shell", SHELL_CELL_TYPE_DEFINITION); - console.log(runtime.definitions.cellTypes); + if('settings' === id) + settingsDialog(); } -(function() { - 'use strict'; - +function setupApp() { if('serviceWorker' in navigator) navigator.serviceWorker.register('./worker.js'); navigator.storage.persist(); settingsLoad().then(() => { - pAPI = new ProbeAPI(updateHosts, addHistory); - pAPI.connect(); + ProbeAPI.connect(); + $('#menu a').on('click', function() { + view($(this).attr('data-view')); + }); view('main'); registerStarboardShellCellType(); @@ -403,4 +326,6 @@ function registerStarboardShellCellType() { console.error(info); setInfo(info); }); -})(); \ No newline at end of file +} + +export { setupApp, updateHosts, addHistory, setInfo, view }; diff --git a/frontend/js/notebook.js b/frontend/js/notebook.js new file mode 100644 index 0000000..2fca87f --- /dev/null +++ b/frontend/js/notebook.js @@ -0,0 +1,95 @@ +// vim: set ts=4 sw=4: +/* jshint esversion: 6 */ + +import { ProbeAPI } from './probeapi.js'; +// import * as runtime from '../starboard-notebook.js'; + +// Starboard-Notebook has no native shell type, so we register one +// see CoffeeScript example: https://starboard.gg/gz/coffeescript-custom-cell-type-n1VJRGC +function registerStarboardShellCellType() { + const StarboardTextEditor = runtime.exports.elements.StarboardTextEditor; + const ConsoleOutputElement = runtime.exports.elements.ConsoleOutputElement; + const cellControlsTemplate = runtime.exports.templates.cellControls; + + const SHELL_CELL_TYPE_DEFINITION = { + name: "Shell", + cellType: ["shell"], + createHandler: (cell, runtime) => new ShellCellHandler(cell, runtime), + } + + class ShellCellHandler { + constructor(cell, runtime) { + this.cell = cell; + this.runtime = runtime; + } + + getControls() { + const runButton = { + icon: "bi bi-play-circle", + tooltip: "Run", + callback: () => this.runtime.controls.emit({id: this.cell.id, type: "RUN_CELL"}), + }; + return cellControlsTemplate({ buttons: [runButton] }); + } + + attach(params) { + this.elements = params.elements; + const topElement = this.elements.topElement; + lit.render(this.getControls(), this.elements.topControlsElement); + + this.editor = new StarboardTextEditor(this.cell, this.runtime, {language: "shell"}); + topElement.appendChild(this.editor); + } + + async run() { + var cmd = this.cell.textContent; + this.outputElement = new ConsoleOutputElement(); + + // For now support only single line commands + cmd = cmd.replace(/\n.*/, ''); + + lit.render(html`${this.outputElement}`, this.elements.bottomElement); + + ProbeAPI.run($('#notebook-host').val(), 5, cmd).then((d) => { + const val = d.stdout+"\n"+d.stderr; + window.$_ = val; + this.outputElement.addEntry({ + method: "result", + data: [val] + }); + return val + }).catch((d) => { + this.outputElement.addEntry({ + method: "error", + data: [d.error] + }); + }); + + return undefined; + } + + focusEditor() { + this.editor.focus(); + } + + async dispose() { + this.editor.remove(); + } + } + + runtime.definitions.cellTypes.map.delete('esm'); + runtime.definitions.cellTypes.map.delete('js'); + runtime.definitions.cellTypes.map.delete('javascript'); + runtime.definitions.cellTypes.map.delete('css'); + runtime.definitions.cellTypes.map.delete('html'); + runtime.definitions.cellTypes.map.delete('python'); + runtime.definitions.cellTypes.map.delete('python3'); + runtime.definitions.cellTypes.map.delete('ipython3'); + runtime.definitions.cellTypes.map.delete('py'); + runtime.definitions.cellTypes.map.delete('pypy'); + runtime.definitions.cellTypes.map.delete('latex'); + + runtime.definitions.cellTypes.register("shell", SHELL_CELL_TYPE_DEFINITION); +} + +export { registerStarboardShellCellType }; \ No newline at end of file diff --git a/frontend/js/probeapi.js b/frontend/js/probeapi.js index bf1295e..a8def45 100644 --- a/frontend/js/probeapi.js +++ b/frontend/js/probeapi.js @@ -1,16 +1,13 @@ // vim: set ts=4 sw=4: /* jshint esversion: 6 */ +import { updateHosts, addHistory, setInfo } from './app.js'; +import { settings, settingsDialog } from './settings.js'; + /* Probe API singleton, allowing one host being probed at a time. Manages auto-updates, host discovery and probe dependency tree */ -function ProbeAPI(updateHostsCb, updateHistoryCb) { - if(arguments.callee._singletonInstance) { - return arguments.callee._singletonInstance; - } - - arguments.callee._singletonInstance = this; - +function _ProbeAPI() { var a = this; a.probes = {}; // definition of probes a.hosts = {}; // callbacks for probe commands @@ -33,9 +30,9 @@ function ProbeAPI(updateHostsCb, updateHistoryCb) { try { var d = JSON.parse(e.data); if(d.cmd === 'history') - a._updateHistoryCb(d.result); + addHistory(d.result); if(d.cmd === 'hosts') - a._updateHostsCb(d.result); + updateHosts(d.result); if(d.cmd === 'probes') { a.probes = d.result; settingsDialog(); @@ -98,11 +95,7 @@ function ProbeAPI(updateHostsCb, updateHistoryCb) { return a.probes[name]; }; - // History CB is a one-shot only - a._updateHistoryCb = updateHistoryCb; - // Setup periodic host update fetch and callback - a._updateHostsCb = updateHostsCb; a._updateHosts = function() { try { a.ws.send(`hosts`); @@ -193,3 +186,7 @@ function ProbeAPI(updateHostsCb, updateHistoryCb) { }); }; } + +const ProbeAPI = new _ProbeAPI(); + +export { ProbeAPI }; \ No newline at end of file diff --git a/frontend/js/renderer/netmap.js b/frontend/js/renderer/netmap.js index f5a7926..4a7707b 100644 --- a/frontend/js/renderer/netmap.js +++ b/frontend/js/renderer/netmap.js @@ -1,18 +1,18 @@ // vim: set ts=4 sw=4: /* jshint esversion: 6 */ +import * as mermaid from '../lib/mermaid.min.js'; + +window.mermaid.initialize({ startOnLoad: false }); + /* IPv4 only netmap renderer A view showing per-service connections for a single host in a directed graph with inbound and outbound connections for the host allowing to traverse the connections */ -renderers.netmap = function netmapRenderer() { - mermaid.initialize({ startOnLoad: false }); -}; - // parse netstat output -renderers.netmap.prototype.parse = function(results) { +function parse(results) { var listen_port_to_program = {}; results = results.split(/\n/) @@ -61,7 +61,7 @@ renderers.netmap.prototype.parse = function(results) { }; // IP lookup popup helper -renderers.netmap.prototype.lookupIp = function(ip) { +function lookup(ip) { $.getJSON('http://ipinfo.io/'+ip, function(data){ alert("IP: "+data.ip+ "\nName: "+data.hostname+ @@ -74,13 +74,11 @@ renderers.netmap.prototype.lookupIp = function(ip) { }); }; -renderers.netmap.prototype.render = function(pAPI, id, host) { - var r = this; - +function netmapRenderer(pAPI, id, host) { $(id).html('Loading connections...'); pAPI.probe(host, 'netstat-a', function(probe, h, input) { - var data = r.parse(input.stdout); + var data = parse(input.stdout); if(0 === data.results.length) { $(id).html(`

There are currently no connections on this host!

Connection data:

${
 				input.replace(/&/g, "&")
@@ -140,7 +138,7 @@ renderers.netmap.prototype.render = function(pAPI, id, host) {
 		try {
 			console.log(t);
 			$(id).html(`
${t}
`); - mermaid.run({ querySelector: `${id} pre.mermaid` }); + window.mermaid.run({ querySelector: `${id} pre.mermaid` }); } catch(e) { console.error(`Failed mermaid rendering: ${e}`); } @@ -149,3 +147,5 @@ renderers.netmap.prototype.render = function(pAPI, id, host) { console.error(`probe Error: host=${h} probe=${probe} ${e}`); }); }; + +export { netmapRenderer }; \ No newline at end of file diff --git a/frontend/js/renderer/perf-flamegraph.js b/frontend/js/renderer/perf-flamegraph.js index a7244c5..3ee015d 100644 --- a/frontend/js/renderer/perf-flamegraph.js +++ b/frontend/js/renderer/perf-flamegraph.js @@ -1,13 +1,11 @@ // vim: set ts=4 sw=4: /* jshint esversion: 6 */ + /* perf based flamegraphs A view allowing you to start a remote perf tool run and process the result into a SVG to be displayed by the renderer. */ -renderers.perfFlameGraph = function perfFlameGraphRenderer() { -}; - -renderers.perfFlameGraph.prototype.render = function(pAPI, id, host) { +function perfRenderer(pAPI, id, host) { var r = this; $(id).html(` @@ -33,3 +31,5 @@ renderers.perfFlameGraph.prototype.render = function(pAPI, id, host) { }); }); }; + +export { perfRenderer }; \ No newline at end of file diff --git a/frontend/js/settings.js b/frontend/js/settings.js index 177ef92..d65170a 100644 --- a/frontend/js/settings.js +++ b/frontend/js/settings.js @@ -1,6 +1,8 @@ // vim: set ts=4 sw=4: /*jshint esversion: 8 */ +import { ProbeAPI } from "./probeapi.js"; + /* ------------------------------------------------------------------------- Persistent settings using IndexedDB ------------------------------------------------------------------------- */ @@ -75,7 +77,6 @@ function settingsLoad() { .then(() => settingsGet('refreshInterval', '5')) .then(() => settingsGet('probeBlacklist', [])) .then(() => { - settingsDialog(); // fill values in GUI resolve(); }).catch(function(e) { reject(`Error loading settings: ${e}`); @@ -90,7 +91,7 @@ function settingsInputChanged(ev) { settingSet(id, $(i).val()); if(id === 'backendEndpoint') - pAPI.connect(); + ProbeAPI.connect(); } function settingsProbeToggle(ev) { @@ -112,10 +113,10 @@ function settingsDialog() { } }); - if(pAPI && pAPI.probes) { + if(ProbeAPI.probes) { $('#probesDisabled').empty(); $('#probesEnabled').empty(); - $.each(pAPI.probes, function(name, p) { + $.each(ProbeAPI.probes, function(name, p) { document.getElementById( (settings.probeBlacklist.includes(name)?'probesDisabled':'probesEnabled') ).innerHTML += `
${name}
`; @@ -127,4 +128,6 @@ function settingsDialog() { settingsSet($(ev.target).attr('id'), $(ev.target).val()); }); $('#settings .probe').click(settingsProbeToggle); -} \ No newline at end of file +} + +export { settingsGet, settingsSet, settingsLoad, settingsDialog, settings }; \ No newline at end of file diff --git a/frontend/notebooks/default.md b/frontend/notebooks/default.md new file mode 100644 index 0000000..39b2c26 --- /dev/null +++ b/frontend/notebooks/default.md @@ -0,0 +1,22 @@ +# %% [markdown] +## Running Commands with Wurmterm + +This is a WurmTerm interactive notebook (similar to Jupyter notebooks). Try to trigger a shell +command using the following cell, by clicking the play button on the left. + +# %% [shell] +echo "Enter a command here!" + +# %% [markdown] +## Examples + +Running processes + +# %% [shell] +ps -xawf | head -20 + +# %% [markdown] +Failed systemd units + +# %% [shell] +systemctl --failed \ No newline at end of file diff --git a/frontend/notebooks/systemd.md b/frontend/notebooks/systemd.md new file mode 100644 index 0000000..4e08060 --- /dev/null +++ b/frontend/notebooks/systemd.md @@ -0,0 +1,8 @@ +# %% [markdown] +## Debugging Systemd Problems + +# %% [markdown] +Failed systemd units + +# %% [shell] +systemctl --failed \ No newline at end of file diff --git a/frontend/worker.js b/frontend/worker.js index 43f2746..8962be5 100644 --- a/frontend/worker.js +++ b/frontend/worker.js @@ -4,7 +4,9 @@ var filesToCache = [ '/', '/index.html', '/css/styles.css', - '/js/main.js', + 'starboard-notebook.css', + '/js/app.js', + '/js/notebook.js', '/js/probeapi.js', '/js/settings.js', '/js/renderer/netmap.js',