diff --git a/assets/index.html b/assets/index.html
index ee8c0ec..14e9102 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -5,6 +5,8 @@
<link rel="stylesheet" type="text/css" href="sai.css"/>
<link rel="icon" href="sai-icon.svg" sizes="any" type="image/svg+xml"/>
<script type='text/javascript' src='sai.js'></script>
+ <script type='text/javascript' src='terminal-core.js'></script>
+ <script type='text/javascript' src='sai-terminal.js'></script>
<script type='text/javascript' src='lws-login.js'></script>
<script type='text/javascript' src="lws-common.js"></script>
<title>Sai</title>
diff --git a/assets/sai-terminal.js b/assets/sai-terminal.js
new file mode 100644
index 0000000..eb16f79
--- /dev/null
+++ b/assets/sai-terminal.js
@@ -0,0 +1,177 @@
+/*
+ * Custom Terminal Emulator for Sai
+ *
+ * Provides an abstract interface for passing buffers of data in and out,
+ * marked with stdxxx.
+ */
+
+class SaiTerminal {
+ constructor(container, options = {}) {
+ this.container = container;
+ this.options = Object.assign({
+ onData: (data) => {},
+ onClose: () => {}
+ }, options);
+
+ this.el = document.createElement('div');
+ this.el.className = 'sai-terminal';
+
+ // Window furniture
+ this.titleBar = document.createElement('div');
+ this.titleBar.className = 'sai-terminal-titlebar';
+
+ this.titleText = document.createElement('span');
+ this.titleText.className = 'sai-terminal-title';
+ this.titleText.textContent = this.options.title || "Terminal";
+ this.titleBar.appendChild(this.titleText);
+
+ this.closeBtn = document.createElement('button');
+ this.closeBtn.className = 'sai-terminal-close';
+ this.closeBtn.textContent = 'X';
+ this.closeBtn.onclick = () => {
+ this.options.onClose();
+ this.destroy();
+ };
+ this.titleBar.appendChild(this.closeBtn);
+ this.el.appendChild(this.titleBar);
+
+ this.contentEl = document.createElement('div');
+ this.contentEl.className = 'sai-terminal-content';
+
+ this.outputEl = document.createElement('div');
+ this.outputEl.className = 'sai-terminal-output';
+ this.contentEl.appendChild(this.outputEl);
+
+ this.inputEl = document.createElement('input');
+ this.inputEl.className = 'sai-terminal-input';
+ this.inputEl.type = 'text';
+ // Hide the input, we use it just for focus/typing
+ this.inputEl.style.opacity = '0';
+ this.inputEl.style.position = 'absolute';
+ this.inputEl.style.left = '-9999px';
+ this.contentEl.appendChild(this.inputEl);
+
+ this.el.appendChild(this.contentEl);
+
+ this.container.appendChild(this.el);
+
+ this.core = new TerminalCore(80, 24);
+
+ // Setup ResizeObserver to dynamically resize
+ this.resizeObserver = new ResizeObserver(entries => {
+ for (let entry of entries) {
+ // Approximate character size (e.g., 8x16)
+ // This would be better if dynamically measured
+ const cols = Math.max(10, Math.floor(entry.contentRect.width / 8.5));
+ const rows = Math.max(5, Math.floor(entry.contentRect.height / 16));
+ if (cols !== this.core.cols || rows !== this.core.rows) {
+ this.core.resize(cols, rows);
+ this.render();
+ }
+ }
+ });
+ this.resizeObserver.observe(this.contentEl);
+
+ // Dragging logic
+ let isDragging = false;
+ let dragStartX, dragStartY;
+ let startLeft, startTop;
+
+ this.titleBar.addEventListener('mousedown', (e) => {
+ if (e.target === this.closeBtn) return;
+ isDragging = true;
+ dragStartX = e.clientX;
+ dragStartY = e.clientY;
+
+ const rect = this.el.getBoundingClientRect();
+ startLeft = rect.left;
+ startTop = rect.top;
+
+ this.el.style.left = startLeft + 'px';
+ this.el.style.top = startTop + 'px';
+ this.el.style.transform = 'none';
+ });
+
+ document.addEventListener('mousemove', (e) => {
+ if (!isDragging) return;
+ const dx = e.clientX - dragStartX;
+ const dy = e.clientY - dragStartY;
+ this.el.style.left = (startLeft + dx) + 'px';
+ this.el.style.top = (startTop + dy) + 'px';
+ });
+
+ document.addEventListener('mouseup', () => {
+ isDragging = false;
+ });
+
+ this.inputEl.addEventListener('keydown', (e) => {
+ if (e.ctrlKey) {
+ if (e.key === 'c') {
+ this.options.onData('\x03');
+ e.preventDefault();
+ } else if (e.key === 'd') {
+ this.options.onData('\x04');
+ e.preventDefault();
+ } else if (e.key === 'z') {
+ this.options.onData('\x1a');
+ e.preventDefault();
+ } else if (e.key === 'l') {
+ this.options.onData('\x0c');
+ e.preventDefault();
+ }
+ } else if (e.key === 'Enter') {
+ this.options.onData('\r');
+ e.preventDefault();
+ } else if (e.key === 'Backspace') {
+ this.options.onData('\x7f');
+ e.preventDefault();
+ } else if (e.key === 'Tab') {
+ this.options.onData('\t');
+ e.preventDefault();
+ } else if (e.key === 'Escape') {
+ this.options.onData('\x1b');
+ e.preventDefault();
+ } else if (e.key.startsWith('Arrow')) {
+ if (e.key === 'ArrowUp') this.options.onData('\x1b[A');
+ else if (e.key === 'ArrowDown') this.options.onData('\x1b[B');
+ else if (e.key === 'ArrowRight') this.options.onData('\x1b[C');
+ else if (e.key === 'ArrowLeft') this.options.onData('\x1b[D');
+ e.preventDefault();
+ } else if (e.key.length === 1 && !e.metaKey && !e.altKey) {
+ // Send regular characters immediately
+ this.options.onData(e.key);
+ e.preventDefault();
+ }
+ });
+
+ // click to focus input
+ this.el.addEventListener('click', () => {
+ const selection = window.getSelection();
+ if (!selection.toString()) {
+ this.inputEl.focus();
+ }
+ });
+ }
+
+ write(channel, data) {
+ if (channel === 1 || channel === 2 || channel === 0) {
+ this.core.write(data);
+ this.render();
+ }
+ }
+
+ render() {
+ this.outputEl.innerHTML = this.core.renderHtml();
+ }
+
+ focus() {
+ this.inputEl.focus();
+ }
+
+ destroy() {
+ this.resizeObserver.disconnect();
+ if (this.el.parentNode) {
+ this.el.parentNode.removeChild(this.el);
+ }
+ }
+}
diff --git a/assets/sai.css b/assets/sai.css
index 8ce9d40..2f32543 100644
--- a/assets/sai.css
+++ b/assets/sai.css
@@ -1508,3 +1508,103 @@ canvas.power-graph {
div.ibuil.vm-builder {
background: #e6ce8a;
}
+
+.sai-terminal {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 600px;
+ height: 400px;
+ background: #1e1e1e;
+ color: #00ff00;
+ font-family: 'Iosevka', monospace;
+ font-size: 14px;
+ display: flex;
+ flex-direction: column;
+ border: 2px solid #555;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
+ z-index: 9999;
+ resize: both;
+ overflow: hidden;
+}
+
+.sai-terminal-titlebar {
+ background: #333;
+ color: #fff;
+ padding: 5px 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ cursor: move;
+ user-select: none;
+ font-family: sans-serif;
+ border-bottom: 1px solid #555;
+}
+
+.sai-terminal-title {
+ font-weight: bold;
+}
+
+.sai-terminal-content {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ overflow: hidden;
+}
+
+.sai-terminal-output {
+ flex-grow: 1;
+ overflow: hidden;
+ margin: 0;
+ white-space: pre;
+ color: #ddd;
+ line-height: 16px;
+}
+
+.sai-terminal-input {
+ background: transparent;
+ color: #fff;
+ border: none;
+ outline: none;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ padding: 0;
+ min-width: 200px;
+}
+
+.sai-terminal-err {
+ color: #ff5555;
+}
+
+.builder-delete-btn {
+ color: #e74c3c;
+ font-weight: bold;
+}
+
+.builder-shell-btn {
+ color: #3498db;
+ font-weight: bold;
+}
+
+.sai-terminal-close {
+ background: #e74c3c;
+ border: none;
+ color: #fff;
+ border-radius: 3px;
+ cursor: pointer;
+ font-weight: bold;
+ padding: 2px 6px;
+}
+
+.sai-terminal-close:hover {
+ background: #c0392b;
+}
+
+.term-cursor {
+ background-color: rgba(0, 255, 0, 0.7);
+ color: #1e1e1e;
+}
diff --git a/assets/sai.js b/assets/sai.js
index 6869e0c..ff18543 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -395,6 +395,7 @@ var lang_zhs = "{" +
"}}";
var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, auth_is_admin = 0, auth_grant_level = -1, exptimer, auth_user = "",
+active_terminals = {};
logAnsiState = {}, logs_pending = "", lines_pending = "", times_pending = "",
ongoing_task_activities = {}, last_log_timestamp = 0, spreadsheet_data_cache = {}, loadreport_data_cache = {},
watcher_services = [],
@@ -1655,7 +1656,7 @@ function createBuilderDiv(plat) {
if (authd && auth_is_admin && !plat.online) {
menuItems.push({
- label: "<span style='color:#e74c3c; font-weight:bold;'>Delete Builder</span>",
+ label: "<span class='builder-delete-btn'>Delete Builder</span>",
callback: () => {
if (confirm("Are you sure you want to delete builder " + plat.name + "?")) {
const msg = {
@@ -1668,6 +1669,47 @@ function createBuilderDiv(plat) {
});
}
+ if (authd && auth_is_admin && plat.online) {
+ menuItems.push({
+ label: "<span class='builder-shell-btn'>Open Shell</span>",
+ callback: () => {
+ const task_uuid = Array.from(crypto.getRandomValues(new Uint8Array(8)))
+ .map(b => b.toString(16).padStart(2, '0')).join('');
+
+ const msg = {
+ schema: "com.warmcat.sai.openshell",
+ builder_name: plat.name,
+ task_uuid: task_uuid
+ };
+ sai.send(JSON.stringify(msg));
+
+ const term = new SaiTerminal(document.body, {
+ title: "Terminal: " + plat.name,
+ onData: (data) => {
+ const ptyMsg = {
+ schema: "com.warmcat.sai.ptydata",
+ builder_name: plat.name,
+ task_uuid: task_uuid,
+ channel: 0,
+ data: btoa(data),
+ len: data.length
+ };
+ sai.send(JSON.stringify(ptyMsg));
+ },
+ onClose: () => {
+ const closeMsg = {
+ schema: "com.warmcat.sai.closeshell",
+ task_uuid: task_uuid
+ };
+ sai.send(JSON.stringify(closeMsg));
+ delete active_terminals[task_uuid];
+ }
+ });
+ active_terminals[task_uuid] = term;
+ }
+ });
+ }
+
platDiv.addEventListener("contextmenu", function(event) {
if (!authd)
return;
@@ -2382,6 +2424,45 @@ function ws_open_sai()
watcher_services = jso;
break;
+ case "com.warmcat.sai.ptydata":
+ if (jso.task_uuid) {
+ const term = active_terminals[jso.task_uuid];
+ if (!term) {
+ term = new SaiTerminal(document.body, {
+ onData: function(input) {
+ const msg = {
+ schema: "com.warmcat.sai.ptydata",
+ task_uuid: jso.task_uuid,
+ channel: 0,
+ len: input.length,
+ data: btoa(input)
+ };
+ sai.send(JSON.stringify(msg));
+ },
+ onClose: function() {
+ const msg = {
+ schema: "com.warmcat.sai.closeshell",
+ task_uuid: jso.task_uuid
+ };
+ sai.send(JSON.stringify(msg));
+ delete active_terminals[jso.task_uuid];
+ }
+ });
+
+ active_terminals[jso.task_uuid] = term;
+ }
+ if (jso.data) {
+ const binString = atob(jso.data);
+ const bytes = new Uint8Array(binString.length);
+ for (let i = 0; i < binString.length; i++) {
+ bytes[i] = binString.charCodeAt(i);
+ }
+ const text = new TextDecoder().decode(bytes);
+ term.write(jso.channel, text);
+ }
+ }
+ break;
+
case "sai.warmcat.com.overview":
/*
* Sent with an array of e[] to start, but also
diff --git a/assets/terminal-core.js b/assets/terminal-core.js
new file mode 100644
index 0000000..71dc3fd
--- /dev/null
+++ b/assets/terminal-core.js
@@ -0,0 +1,222 @@
+/*
+ * Lightweight JS Terminal Emulator Core
+ */
+
+class TerminalCore {
+ constructor(cols = 80, rows = 24) {
+ this.cols = cols;
+ this.rows = rows;
+ this.buffer = []; // Array of { char: ' ', fg: null, bg: null, bold: false }
+ this.cx = 0;
+ this.cy = 0;
+
+ this.defaultCell = { char: ' ', fg: null, bg: null, bold: false };
+ this.currentStyle = { fg: null, bg: null, bold: false };
+
+ this.initBuffer();
+ }
+
+ initBuffer() {
+ this.buffer = [];
+ for (let r = 0; r < this.rows; r++) {
+ let row = [];
+ for (let c = 0; c < this.cols; c++) {
+ row.push({...this.defaultCell});
+ }
+ this.buffer.push(row);
+ }
+ }
+
+ resize(cols, rows) {
+ let newBuffer = [];
+ for (let r = 0; r < rows; r++) {
+ let row = [];
+ for (let c = 0; c < cols; c++) {
+ if (r < this.rows && c < this.cols) {
+ row.push(this.buffer[r][c]);
+ } else {
+ row.push({...this.defaultCell});
+ }
+ }
+ newBuffer.push(row);
+ }
+ this.buffer = newBuffer;
+ this.cols = cols;
+ this.rows = rows;
+ if (this.cx >= cols) this.cx = cols - 1;
+ if (this.cy >= rows) this.cy = rows - 1;
+ }
+
+ write(data) {
+ for (let i = 0; i < data.length; i++) {
+ let char = data[i];
+
+ if (char === '\n') {
+ this.cy++;
+ this.cx = 0;
+ if (this.cy >= this.rows) {
+ this.scrollUp();
+ this.cy = this.rows - 1;
+ }
+ } else if (char === '\r') {
+ this.cx = 0;
+ } else if (char === '\b') {
+ if (this.cx > 0) this.cx--;
+ } else if (char === '\t') {
+ this.cx = (this.cx + 8) - (this.cx % 8);
+ if (this.cx >= this.cols) this.cx = this.cols - 1;
+ } else if (char === '\x07') {
+ // Bell
+ } else if (char === '\x1b') { // ESC
+ if (data[i + 1] === '[') {
+ // CSI
+ let j = i + 2;
+ let paramStr = "";
+ while (j < data.length && (data[j] >= '0' && data[j] <= '9' || data[j] === ';' || data[j] === '?')) {
+ paramStr += data[j];
+ j++;
+ }
+ if (j < data.length) {
+ let cmd = data[j];
+ this.handleCSI(cmd, paramStr);
+ i = j;
+ }
+ } else if (data[i+1] === ']') {
+ // OSC
+ let j = i + 2;
+ while (j < data.length && data[j] !== '\x07' && data[j] !== '\x1b') j++;
+ if (data[j] === '\x1b' && data[j+1] === '\\') j++;
+ i = j;
+ } else {
+ i++; // Skip unknown ESC sequence
+ }
+ } else {
+ this.buffer[this.cy][this.cx] = {
+ char: char,
+ fg: this.currentStyle.fg,
+ bg: this.currentStyle.bg,
+ bold: this.currentStyle.bold
+ };
+ this.cx++;
+ if (this.cx >= this.cols) {
+ this.cx = 0;
+ this.cy++;
+ if (this.cy >= this.rows) {
+ this.scrollUp();
+ this.cy = this.rows - 1;
+ }
+ }
+ }
+ }
+ }
+
+ handleCSI(cmd, paramStr) {
+ if (paramStr.startsWith('?')) paramStr = paramStr.substring(1);
+ let params = paramStr.split(';').map(p => parseInt(p, 10));
+ switch (cmd) {
+ case 'A': // Cursor Up
+ this.cy = Math.max(0, this.cy - (params[0] || 1)); break;
+ case 'B': // Cursor Down
+ this.cy = Math.min(this.rows - 1, this.cy + (params[0] || 1)); break;
+ case 'C': // Cursor Forward
+ this.cx = Math.min(this.cols - 1, this.cx + (params[0] || 1)); break;
+ case 'D': // Cursor Back
+ this.cx = Math.max(0, this.cx - (params[0] || 1)); break;
+ case 'G': // Cursor Horizontal Absolute
+ case '`':
+ this.cx = Math.min(this.cols - 1, Math.max(0, (params[0] || 1) - 1)); break;
+ case 'd': // Vertical Position Absolute
+ this.cy = Math.min(this.rows - 1, Math.max(0, (params[0] || 1) - 1)); break;
+ case 'H':
+ case 'f': // Cursor Position
+ this.cy = Math.min(this.rows - 1, Math.max(0, (params[0] || 1) - 1));
+ this.cx = Math.min(this.cols - 1, Math.max(0, (params[1] || 1) - 1));
+ break;
+ case 'J': // Erase in Display
+ let modeJ = params[0] || 0;
+ if (modeJ === 0) {
+ for (let c = this.cx; c < this.cols; c++) this.buffer[this.cy][c] = {...this.defaultCell};
+ for (let r = this.cy + 1; r < this.rows; r++) {
+ for (let c = 0; c < this.cols; c++) this.buffer[r][c] = {...this.defaultCell};
+ }
+ } else if (modeJ === 1) {
+ for (let c = 0; c <= this.cx; c++) this.buffer[this.cy][c] = {...this.defaultCell};
+ for (let r = 0; r < this.cy; r++) {
+ for (let c = 0; c < this.cols; c++) this.buffer[r][c] = {...this.defaultCell};
+ }
+ } else if (modeJ === 2) {
+ this.initBuffer();
+ }
+ break;
+ case 'K': // Erase in Line
+ let modeK = params[0] || 0;
+ if (modeK === 0) {
+ for (let c = this.cx; c < this.cols; c++) this.buffer[this.cy][c] = {...this.defaultCell};
+ } else if (modeK === 1) {
+ for (let c = 0; c <= this.cx; c++) this.buffer[this.cy][c] = {...this.defaultCell};
+ } else if (modeK === 2) {
+ for (let c = 0; c < this.cols; c++) this.buffer[this.cy][c] = {...this.defaultCell};
+ }
+ break;
+ case 'm': // SGR
+ for (let i = 0; i < params.length; i++) {
+ let code = params[i] || 0;
+ if (code === 0) this.currentStyle = { fg: null, bg: null, bold: false };
+ else if (code === 1) this.currentStyle.bold = true;
+ else if (code >= 30 && code <= 37) this.currentStyle.fg = code - 30;
+ else if (code >= 40 && code <= 47) this.currentStyle.bg = code - 40;
+ }
+ break;
+ }
+ }
+
+ scrollUp() {
+ this.buffer.shift();
+ let row = [];
+ for (let c = 0; c < this.cols; c++) row.push({...this.defaultCell});
+ this.buffer.push(row);
+ }
+
+ renderHtml() {
+ let html = '';
+ let lastStyle = null;
+
+ const colorMap = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
+
+ for (let r = 0; r < this.rows; r++) {
+ for (let c = 0; c < this.cols; c++) {
+ let cell = this.buffer[r][c];
+
+ let isCursor = (r === this.cy && c === this.cx);
+ let styleKey = `${cell.fg}-${cell.bg}-${cell.bold}-${isCursor}`;
+
+ if (styleKey !== lastStyle) {
+ if (lastStyle !== null) html += '</span>';
+
+ let classes = [];
+ if (cell.fg !== null) classes.push(`ansi-fg-${colorMap[cell.fg]}`);
+ if (cell.bg !== null) classes.push(`ansi-bg-${colorMap[cell.bg]}`);
+ if (cell.bold) classes.push(`ansi-bold`);
+ if (isCursor) classes.push(`term-cursor`);
+
+ if (classes.length > 0) {
+ html += `<span class="${classes.join(' ')}">`;
+ } else {
+ html += `<span>`;
+ }
+ lastStyle = styleKey;
+ }
+ html += (cell.char === ' ' || cell.char === '') ? ' ' : this.escapeHtml(cell.char);
+ }
+ html += '<br>';
+ }
+ if (lastStyle !== null) html += '</span>';
+ return html;
+ }
+
+ escapeHtml(s) {
+ return s.replace(/[&<>"']/g, function(m) {
+ return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
+ });
+ }
+}
diff --git a/src/builder/b-nspawn.c b/src/builder/b-nspawn.c
index 2d289f7..fa361e5 100644
--- a/src/builder/b-nspawn.c
+++ b/src/builder/b-nspawn.c
@@ -685,3 +685,137 @@ saib_spawn_script(struct sai_nspawn *ns)
return 0;
}
+static void
+sai_shell_reap_cb(void *opaque, const lws_spawn_resource_us_t *res, siginfo_t *si,
+ int we_killed_him)
+{
+ struct sai_shell *sh = (struct sai_shell *)opaque;
+
+ lwsl_notice("%s: shell exited\n", __func__);
+
+ if (sh) {
+ lws_dll2_remove(&sh->list);
+ free(sh);
+ }
+}
+
+static int
+callback_sai_shell_stdwsi(struct lws *wsi, enum lws_callback_reasons reason,
+ void *user, void *in, size_t len)
+{
+ struct sai_shell *sh = (struct sai_shell *)lws_get_opaque_user_data(wsi);
+ uint8_t buf[1024];
+ int ilen;
+
+ switch (reason) {
+ case LWS_CALLBACK_RAW_CLOSE_FILE:
+ {
+ int ch = lws_spawn_get_stdfd(wsi);
+ if (ch == 0) ch = 1;
+ if (sh && ch < 3)
+ sh->stdwsi[ch] = NULL;
+ break;
+ }
+
+ case LWS_CALLBACK_RAW_RX_FILE:
+#if defined(WIN32)
+ {
+ DWORD rb;
+ if (!ReadFile((HANDLE)lws_get_socket_fd(wsi), buf, sizeof(buf) - 1, &rb, NULL))
+ return -1;
+ ilen = (int)rb;
+ }
+#else
+ ilen = (int)read((int)(intptr_t)lws_get_socket_fd(wsi), buf, sizeof(buf) - 1);
+ if (ilen < 1)
+ return -1;
+#endif
+ if (!sh || !sh->spm)
+ return -1;
+
+ len = (size_t)ilen;
+ int ch = lws_spawn_get_stdfd(wsi);
+ if (ch == 0) ch = 1;
+
+ lwsl_notice("%s: read %d bytes from shell %s (ch %d)\n", __func__, ilen, sh->task_uuid, ch);
+
+ if (ch < 3)
+ sh->stdwsi[ch] = wsi;
+
+ char lj[4000 + LWS_PRE];
+ int n = lws_snprintf(lj + LWS_PRE, sizeof(lj) - LWS_PRE,
+ "{\"schema\":\"com.warmcat.sai.ptydata\","
+ "\"task_uuid\":\"%s\", \"channel\": %d, \"len\": %d, \"data\":\"",
+ sh->task_uuid, ch, (int)len);
+
+ n += lws_b64_encode_string((char *)buf, (int)len, lj + LWS_PRE + n,
+ (int)sizeof(lj) - LWS_PRE - n - 5);
+ lj[LWS_PRE + n++] = '\"';
+ lj[LWS_PRE + n++] = '}';
+ lj[LWS_PRE + n] = '\0';
+
+ lwsl_notice("%s: PTYDATA queueing %d bytes from shell %s to server\n", __func__, (int)len, sh->task_uuid);
+
+ saib_srv_queue_tx(sh->spm->ss, lj + LWS_PRE, (size_t)n, LWSSS_FLAG_SOM | LWSSS_FLAG_EOM);
+ break;
+
+ default:
+ break;
+ }
+ return 0;
+}
+
+struct lws_protocols protocol_saishell = {
+ .name = "sai-saishell",
+ .callback = callback_sai_shell_stdwsi,
+ .per_session_data_size = 0,
+ .rx_buffer_size = 0,
+};
+
+int
+saib_shell_spawn(struct sai_plat_server *spm, const char *task_uuid)
+{
+ struct lws_spawn_piped_info info;
+ struct sai_shell *sh;
+ const char *cmd[] = { "/bin/bash", "-i", NULL };
+ const char *env[] = {
+ "PATH=/usr/local/bin:/usr/bin:/bin",
+ "LANG=en_US.UTF-8",
+ "TERM=xterm-256color",
+ NULL
+ };
+
+ sh = malloc(sizeof(*sh));
+ if (!sh)
+ return 1;
+ memset(sh, 0, sizeof(*sh));
+
+ lws_strncpy(sh->task_uuid, task_uuid, sizeof(sh->task_uuid));
+ sh->spm = spm;
+
+ memset(&info, 0, sizeof(info));
+ info.vh = builder.vhost;
+ info.env_array = (const char **)env;
+ info.exec_array = cmd;
+ info.protocol_name = "sai-saishell";
+ info.max_log_lines = 10000;
+ info.timeout_us = 30 * 60 * LWS_US_PER_SEC;
+ info.reap_cb = sai_shell_reap_cb;
+ info.pty_mode = 1;
+ info.opaque = sh;
+ info.owner = &builder.lsp_owner;
+ info.plsp = &sh->lsp;
+
+ lws_dll2_add_tail(&sh->list, &builder.shell_owner);
+
+ lws_spawn_piped(&info);
+ if (!sh->lsp) {
+ lwsl_err("%s: Failed to spawn shell for %s\n", __func__, task_uuid);
+ lws_dll2_remove(&sh->list);
+ free(sh);
+ return 1;
+ }
+
+ lwsl_notice("%s: Spawned shell successfully for %s\n", __func__, task_uuid);
+ return 0;
+}
diff --git a/src/builder/b-private.h b/src/builder/b-private.h
index 8d2dd51..0562a71 100644
--- a/src/builder/b-private.h
+++ b/src/builder/b-private.h
@@ -98,11 +98,21 @@ enum nsstate {
* This represents this builder process as a whole
*/
+struct sai_shell {
+ struct lws_dll2 list;
+ char task_uuid[33];
+ struct sai_plat_server *spm;
+ struct lws_spawn_piped *lsp;
+ struct lws *stdwsi[3];
+ int user_cancel;
+};
+
struct sai_builder {
lws_dll2_owner_t sai_plat_owner; /* list of platforms we offer */
lws_dll2_owner_t sai_plat_server_owner; /* servers we connect to */
lws_dll2_owner_t devices_owner; /* sai_serial_t */
lws_dll2_owner_t lsp_owner; /* list of lws_spawn_piped */
+ lws_dll2_owner_t shell_owner; /* list of sai_shell */
struct lws_ss_handle *ss_stay;
struct lws_ss_handle *ss_power_off;
diff --git a/src/builder/b-sai.c b/src/builder/b-sai.c
index dca14fb..5ff35c8 100644
--- a/src/builder/b-sai.c
+++ b/src/builder/b-sai.c
@@ -100,6 +100,7 @@ static lws_state_notify_link_t nl;
struct sai_builder builder;
extern struct lws_protocols protocol_stdxxx;
+extern struct lws_protocols protocol_saishell;
extern struct lws_protocols protocol_suspender_stdxxx;
extern struct lws_protocols protocol_deletion_stdxxx;
@@ -201,6 +202,7 @@ static const char * const default_ss_policy =
static const struct lws_protocols *pprotocols[] = {
&protocol_stdxxx,
+ &protocol_saishell,
&protocol_logproxy,
&protocol_resproxy,
&protocol_suspender_stdxxx,
diff --git a/src/builder/b-ws-server.c b/src/builder/b-ws-server.c
index 20fba3d..9644f20 100644
--- a/src/builder/b-ws-server.c
+++ b/src/builder/b-ws-server.c
@@ -48,7 +48,9 @@ const lws_struct_map_t lsm_schema_map_m_to_b[] = {
LSM_SCHEMA (sai_viewer_state_t, NULL, lsm_viewerstate_members,
"com.warmcat.sai.viewerstate"),
LSM_SCHEMA (sai_resource_t, NULL, lsm_resource, "com-warmcat-sai-resource"),
- LSM_SCHEMA (sai_rebuild_t, NULL, lsm_rebuild, "com.warmcat.sai.rebuild")
+ LSM_SCHEMA (sai_rebuild_t, NULL, lsm_rebuild, "com.warmcat.sai.rebuild"),
+ LSM_SCHEMA (sai_openshell_t, NULL, lsm_openshell, "com.warmcat.sai.openshell"),
+ LSM_SCHEMA (sai_ptydata_t, NULL, lsm_ptydata, "com.warmcat.sai.ptydata")
};
enum {
@@ -56,7 +58,9 @@ enum {
SAIB_RX_TASK_CANCEL,
SAIB_RX_VIEWERSTATE,
SAIB_RX_RESOURCE_REPLY,
- SAIB_RX_REBUILD
+ SAIB_RX_REBUILD,
+ SAIB_RX_OPENSHELL,
+ SAIB_RX_PTYDATA
};
/*
@@ -72,11 +76,14 @@ enum {
*/
int
-saib_srv_queue_tx(struct lws_ss_handle *h, void *buf, size_t len, unsigned int ss_flags)
+saib_srv_queue_tx(struct lws_ss_handle *h, void *buf, size_t len,
+ unsigned int ss_flags)
{
struct sai_plat_server *spm = (struct sai_plat_server *)lws_ss_to_user_object(h);
unsigned int *pi = (unsigned int *)((const char *)buf - sizeof(int));
+ lwsl_notice("%s: queuing %d bytes to server\n", __func__, (int)len);
+
*pi = ss_flags;
// lwsl_ss_notice(h, "Queuing builder -> sai-server");
@@ -234,6 +241,18 @@ saib_m_rx(void *userobj, const uint8_t *in, size_t len, int flags)
} lws_end_foreach_dll_safe(p, p1);
} lws_end_foreach_dll_safe(mp, mp1);
+
+ lws_start_foreach_dll_safe(struct lws_dll2 *, shd, shd1,
+ builder.shell_owner.head) {
+ struct sai_shell *sh = lws_container_of(shd, struct sai_shell, list);
+
+ if (!strcmp(can->task_uuid, sh->task_uuid)) {
+ lwsl_notice("%s: cancelling shell %s\n", __func__, can->task_uuid);
+ if (sh->lsp)
+ lws_spawn_piped_kill_child_process(sh->lsp);
+ sh->user_cancel = 1;
+ }
+ } lws_end_foreach_dll_safe(shd, shd1);
break;
case SAIB_RX_VIEWERSTATE:
@@ -281,6 +300,45 @@ saib_m_rx(void *userobj, const uint8_t *in, size_t len, int flags)
}
break;
+ case SAIB_RX_OPENSHELL:
+ {
+ extern int saib_shell_spawn(struct sai_plat_server *spm, const char *task_uuid);
+ sai_openshell_t *os = (sai_openshell_t *)a.dest;
+ lwsl_notice("%s: OPENSHELL received from server for task %s\n", __func__, os->task_uuid);
+ saib_shell_spawn(spm, os->task_uuid);
+ break;
+ }
+
+ case SAIB_RX_PTYDATA:
+ {
+ sai_ptydata_t *pd = (sai_ptydata_t *)a.dest;
+ struct sai_shell *sh = NULL;
+
+ lws_start_foreach_dll(struct lws_dll2 *, d, builder.shell_owner.head) {
+ struct sai_shell *s = lws_container_of(d, struct sai_shell, list);
+ if (!strcmp(s->task_uuid, pd->task_uuid)) {
+ sh = s;
+ break;
+ }
+ } lws_end_foreach_dll(d);
+
+ if (sh && sh->lsp && pd->data) {
+ int fd = lws_spawn_get_fd_stdxxx(sh->lsp, 0);
+ if (fd >= 0) {
+ char dec[1024];
+ int dl = lws_b64_decode_string(pd->data, dec, sizeof(dec));
+ if (dl > 0) {
+ lwsl_notice("%s: PTYDATA received from server (len %d), writing to shell %s\n", __func__, dl, pd->task_uuid);
+ if (write(fd, dec, LWS_POSIX_LENGTH_CAST(dl)) < 0)
+ lwsl_err("%s: failed to write to pty\n", __func__);
+ }
+ }
+ } else {
+ lwsl_warn("%s: PTYDATA for unknown shell %s\n", __func__, pd->task_uuid);
+ }
+ break;
+ }
+
case SAIB_RX_RESOURCE_REPLY:
reso = (sai_resource_t *)a.dest;
@@ -358,6 +416,8 @@ saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,
if (!used && !som && !final && fsl > 0)
return LWSSSSRET_TX_DONT_SEND;
+ lwsl_notice("%s: sending %d bytes to server (fsl %d, len %d, flags 0x%x)\n", __func__, (int)used, (int)fsl, (int)*len, spm->tx_flags);
+
*len = used;
*flags = (som ? LWSSS_FLAG_SOM : 0) | (final ? LWSSS_FLAG_EOM : 0);
diff --git a/src/common/include/private.h b/src/common/include/private.h
index 19e461a..ed827b6 100644
--- a/src/common/include/private.h
+++ b/src/common/include/private.h
@@ -335,6 +335,32 @@ typedef struct sai_cancel {
} sai_cancel_t;
/*
+ * Browser -> sai-web -> sai-server
+ *
+ * Open an ad-hoc shell on a builder
+ */
+
+typedef struct sai_openshell {
+ lws_dll2_t list;
+ char builder_name[96];
+ char task_uuid[65];
+} sai_openshell_t;
+
+typedef struct sai_closeshell {
+ lws_dll2_t list;
+ char task_uuid[65];
+} sai_closeshell_t;
+
+typedef struct sai_ptydata {
+ lws_dll2_t list;
+ char builder_name[96];
+ char task_uuid[65];
+ int channel;
+ char *data;
+ size_t len;
+} sai_ptydata_t;
+
+/*
* Browser is asking a builder to rebuild
*/
@@ -828,6 +854,12 @@ extern const lws_struct_map_t
lsm_schema_json_map_event[1],
lsm_resource[4],
lsm_power_state[3],
+ lsm_openshell[2],
+ lsm_schema_openshell[1],
+ lsm_closeshell[1],
+ lsm_schema_closeshell[1],
+ lsm_ptydata[5],
+ lsm_schema_ptydata[1],
lsm_rebuild[1],
lsm_schema_rebuild[1],
lsm_schema_build_metric[1],
diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c
index 52f6229..43f0496 100644
--- a/src/common/struct-metadata.c
+++ b/src/common/struct-metadata.c
@@ -212,6 +212,38 @@ const lws_struct_map_t lsm_task_cancel[] = {
LSM_UNSIGNED (sai_cancel_t, erase, "erase"),
};
+const lws_struct_map_t lsm_openshell[] = {
+ LSM_CARRAY (sai_openshell_t, builder_name, "builder_name"),
+ LSM_CARRAY (sai_openshell_t, task_uuid, "task_uuid"),
+};
+
+const lws_struct_map_t lsm_schema_openshell[] = {
+ LSM_SCHEMA (sai_openshell_t, NULL, lsm_openshell,
+ "com.warmcat.sai.openshell")
+};
+
+const lws_struct_map_t lsm_closeshell[] = {
+ LSM_CARRAY (sai_closeshell_t, task_uuid, "task_uuid"),
+};
+
+const lws_struct_map_t lsm_schema_closeshell[] = {
+ LSM_SCHEMA (sai_closeshell_t, NULL, lsm_closeshell,
+ "com.warmcat.sai.closeshell")
+};
+
+const lws_struct_map_t lsm_ptydata[] = {
+ LSM_CARRAY (sai_ptydata_t, builder_name, "builder_name"),
+ LSM_CARRAY (sai_ptydata_t, task_uuid, "task_uuid"),
+ LSM_SIGNED (sai_ptydata_t, channel, "channel"),
+ LSM_STRING_PTR (sai_ptydata_t, data, "data"),
+ LSM_UNSIGNED (sai_ptydata_t, len, "len"),
+};
+
+const lws_struct_map_t lsm_schema_ptydata[] = {
+ LSM_SCHEMA (sai_ptydata_t, NULL, lsm_ptydata,
+ "com.warmcat.sai.ptydata")
+};
+
const lws_struct_map_t lsm_rebuild[] = {
LSM_CARRAY (sai_rebuild_t, builder_name, "builder_name"),
};
diff --git a/src/server/s-private.h b/src/server/s-private.h
index ce46d52..2540887 100644
--- a/src/server/s-private.h
+++ b/src/server/s-private.h
@@ -144,6 +144,8 @@ struct pss {
lws_dll2_owner_t rebuild_owner;
lws_dll2_owner_t stay_owner;
lws_dll2_owner_t pcon_control_owner;
+ lws_dll2_owner_t openshell_owner;
+ lws_dll2_owner_t ptydata_owner;
lws_dll2_owner_t aft_owner; /* for statefully spooling artifact info */
lws_dll2_owner_t res_owner; /* sai_resource_requisition_t
* owner of resource objects related
diff --git a/src/server/s-ws-builder.c b/src/server/s-ws-builder.c
index d7e0f11..2699112 100644
--- a/src/server/s-ws-builder.c
+++ b/src/server/s-ws-builder.c
@@ -72,6 +72,8 @@ static const lws_struct_map_t lsm_schema_map_ba[] = {
"com-warmcat-sai-resource"),
LSM_SCHEMA (sai_build_metric_t, NULL, lsm_build_metric,
"com.warmcat.sai.build-metric"),
+ LSM_SCHEMA (sai_ptydata_t, NULL, lsm_ptydata,
+ "com.warmcat.sai.ptydata"),
};
enum {
@@ -82,6 +84,7 @@ enum {
SAIM_WSSCH_BUILDER_LOADREPORT,
SAIM_WSSCH_BUILDER_RESOURCE_REQ,
SAIM_WSSCH_BUILDER_METRIC,
+ SAIM_WSSCH_BUILDER_PTYDATA,
};
static void
@@ -1404,6 +1407,41 @@ sais_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t b
lwsac_free(&pss->a.ac);
break;
+
+ case SAIM_WSSCH_BUILDER_PTYDATA:
+ {
+ sai_ptydata_t *pd = (sai_ptydata_t *)pss->a.dest;
+
+ js = lws_struct_json_serialize_create(lsm_schema_ptydata,
+ LWS_ARRAY_SIZE(lsm_schema_ptydata), 0, pd);
+ if (!js) {
+ lwsac_free(&pss->a.ac);
+ break;
+ }
+
+ switch (lws_struct_json_serialize(js, xbuf + LWS_PRE,
+ sizeof(xbuf) - LWS_PRE, &used)) {
+ case LSJS_RESULT_CONTINUE:
+ case LSJS_RESULT_ERROR:
+ break;
+ case LSJS_RESULT_FINISH:
+ memset(&info, 0, sizeof(info));
+
+ info.private_source_idx = SAI_WEBSRV_PB__PROXIED_FROM_BUILDER;
+ info.buf = xbuf + LWS_PRE;
+ info.len = used;
+ info.ss_flags = LWSSS_FLAG_SOM | LWSSS_FLAG_EOM;
+
+ lwsl_notice("%s: PTYDATA received from builder shell %s, relaying %d bytes to web\n", __func__, pd->task_uuid, (int)used);
+
+ if (sais_websrv_broadcast_REQUIRES_LWS_PRE(vhd->h_ss_websrv, &info) < 0)
+ lwsl_warn("%s: unable to broadcast to web\n", __func__);
+ break;
+ }
+ lws_struct_json_serialize_destroy(&js);
+ lwsac_free(&pss->a.ac);
+ break;
+ }
}
buf += ((int)bl - m);
@@ -1519,6 +1557,44 @@ sais_ws_json_tx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf,
goto send_json;
}
+ if (pss->openshell_owner.head) {
+ sai_openshell_t *os = lws_container_of(pss->openshell_owner.head,
+ sai_openshell_t, list);
+
+ js = lws_struct_json_serialize_create(lsm_schema_openshell,
+ LWS_ARRAY_SIZE(lsm_schema_openshell), 0, os);
+ if (!js)
+ return 1;
+
+ n = (int)lws_struct_json_serialize(js, p, lws_ptr_diff_size_t(end, p), &w);
+ lws_struct_json_serialize_destroy(&js);
+
+ lws_dll2_remove(&os->list);
+ free(os);
+
+ goto send_json;
+ }
+
+ if (pss->ptydata_owner.head) {
+ sai_ptydata_t *pd = lws_container_of(pss->ptydata_owner.head,
+ sai_ptydata_t, list);
+
+ js = lws_struct_json_serialize_create(lsm_schema_ptydata,
+ LWS_ARRAY_SIZE(lsm_schema_ptydata), 0, pd);
+ if (!js)
+ return 1;
+
+ n = (int)lws_struct_json_serialize(js, p, lws_ptr_diff_size_t(end, p), &w);
+ lws_struct_json_serialize_destroy(&js);
+
+ lws_dll2_remove(&pd->list);
+ if (pd->data)
+ free(pd->data);
+ free(pd);
+
+ goto send_json;
+ }
+
/*
* resource response?
*/
@@ -1595,7 +1671,8 @@ send_json:
if (pss->viewer_state_owner.head || pss->task_cancel_owner.head ||
pss->res_pending_reply_owner.count ||
- pss->issue_task_owner.count)
+ pss->issue_task_owner.count || pss->openshell_owner.head ||
+ pss->ptydata_owner.head)
lws_callback_on_writable(pss->wsi);
return 0;
diff --git a/src/server/s-ws-web.c b/src/server/s-ws-web.c
index 5b3a6d7..517e79b 100644
--- a/src/server/s-ws-web.c
+++ b/src/server/s-ws-web.c
@@ -111,7 +111,13 @@ static const lws_struct_map_t lsm_schema_json_map[] = {
LSM_SCHEMA (sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,
/* shares struct */ "com.warmcat.sai.taskresume"),
LSM_SCHEMA (sai_browse_rx_builderdelete_t, NULL, lsm_browser_builderdelete,
- "com.warmcat.sai.builderdelete")
+ "com.warmcat.sai.builderdelete"),
+ LSM_SCHEMA (sai_openshell_t, NULL, lsm_openshell,
+ "com.warmcat.sai.openshell"),
+ LSM_SCHEMA (sai_closeshell_t, NULL, lsm_closeshell,
+ "com.warmcat.sai.closeshell"),
+ LSM_SCHEMA (sai_ptydata_t, NULL, lsm_ptydata,
+ "com.warmcat.sai.ptydata")
};
enum {
@@ -129,6 +135,9 @@ enum {
SAIS_WS_WEBSRV_RX_TASKPAUSE,
SAIS_WS_WEBSRV_RX_TASKRESUME,
SAIS_WS_WEBSRV_RX_BUILDERDELETE,
+ SAIS_WS_WEBSRV_RX_OPENSHELL,
+ SAIS_WS_WEBSRV_RX_CLOSESHELL,
+ SAIS_WS_WEBSRV_RX_PTYDATA,
};
static int
@@ -677,6 +686,90 @@ websrvss_ws_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
lwsac_free(&a.ac);
break;
}
+ case SAIS_WS_WEBSRV_RX_OPENSHELL:
+ {
+ sai_openshell_t *os = (sai_openshell_t *)a.dest;
+ sai_plat_t *sp;
+
+ lwsl_notice("%s: OPENSHELL received from web for %s, passing to builder\n", __func__, os->builder_name);
+
+ sp = sais_builder_from_uuid(m->vhd, os->builder_name);
+ if (!sp) {
+ lwsl_err("%s: unknown builder %s for openshell\n",
+ __func__, os->builder_name);
+ lwsac_free(&a.ac);
+ break;
+ }
+
+ if (!os->task_uuid[0])
+ sai_uuid16_create(m->vhd->context, os->task_uuid);
+
+ lws_start_foreach_dll(struct lws_dll2 *, p, m->vhd->builders.head) {
+ struct pss *pss = lws_container_of(p, struct pss, same);
+
+ if (pss->wsi == sp->wsi) {
+ sai_openshell_t *s = malloc(sizeof(*s));
+ if (s) {
+ *s = *os;
+ lws_dll2_add_tail(&s->list, &pss->openshell_owner);
+ lws_callback_on_writable(pss->wsi);
+ }
+ break;
+ }
+ } lws_end_foreach_dll(p);
+
+
+
+ lwsac_free(&a.ac);
+ break;
+ }
+
+ case SAIS_WS_WEBSRV_RX_CLOSESHELL:
+ {
+ sai_closeshell_t *cs = (sai_closeshell_t *)a.dest;
+ sais_task_cancel(m->vhd, cs->task_uuid, 0);
+ lwsac_free(&a.ac);
+ break;
+ }
+
+ case SAIS_WS_WEBSRV_RX_PTYDATA:
+ {
+ sai_ptydata_t *pd = (sai_ptydata_t *)a.dest;
+ sai_plat_t *sp;
+
+ sp = sais_builder_from_uuid(m->vhd, pd->builder_name);
+ if (!sp) {
+ lwsl_err("%s: PTYDATA from web: unknown builder %s\n", __func__, pd->builder_name);
+ lwsac_free(&a.ac);
+ break;
+ }
+
+ lwsl_notice("%s: PTYDATA received from web (len %d), passing to builder %s\n", __func__, (int)pd->len, pd->builder_name);
+
+ lws_start_foreach_dll(struct lws_dll2 *, p, m->vhd->builders.head) {
+ struct pss *pss = lws_container_of(p, struct pss, same);
+
+ if (pss->wsi == sp->wsi) {
+ sai_ptydata_t *s = malloc(sizeof(*s));
+ if (s) {
+ size_t slen = strlen(pd->data);
+ *s = *pd;
+ s->data = malloc(slen + 1);
+ if (s->data) {
+ memcpy((char *)s->data, pd->data, slen + 1);
+ lws_dll2_add_tail(&s->list, &pss->ptydata_owner);
+ lws_callback_on_writable(pss->wsi);
+ } else
+ free(s);
+ }
+ break;
+ }
+ } lws_end_foreach_dll(p);
+
+ lwsac_free(&a.ac);
+ break;
+ }
+
case SAIS_WS_WEBSRV_RX_BUILDERDELETE:
{
sai_browse_rx_builderdelete_t *bd = (sai_browse_rx_builderdelete_t *)a.dest;
diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt
index 703fc0f..4f37deb 100644
--- a/src/web/CMakeLists.txt
+++ b/src/web/CMakeLists.txt
@@ -83,6 +83,8 @@ add_dependencies(${SUB} sai_git_hash)
../../assets/coverity.svg
../../assets/sai.css
../../assets/sai.js
+ ../../assets/terminal-core.js
+ ../../assets/sai-terminal.js
../../assets/sai.svg
../../assets/sai-icon.svg
../../assets/builder-instance.svg
diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c
index 2be29fe..cfa5d40 100644
--- a/src/web/w-ws-browser.c
+++ b/src/web/w-ws-browser.c
@@ -102,6 +102,12 @@ static const lws_struct_map_t lsm_schema_json_map_bwsrx[] = {
"com.warmcat.sai.watcher_services"),
LSM_SCHEMA (sai_browse_rx_builderdelete_t, NULL, lsm_browser_builderdelete,
"com.warmcat.sai.builderdelete"),
+ LSM_SCHEMA (sai_openshell_t, NULL, lsm_openshell,
+ "com.warmcat.sai.openshell"),
+ LSM_SCHEMA (sai_closeshell_t, NULL, lsm_closeshell,
+ "com.warmcat.sai.closeshell"),
+ LSM_SCHEMA (sai_ptydata_t, NULL, lsm_ptydata,
+ "com.warmcat.sai.ptydata"),
};
enum {
@@ -119,6 +125,9 @@ enum {
SAIM_WS_BROWSER_RX_PCON_CONTROL,
SAIM_WS_BROWSER_RX_WATCHER_SERVICES,
SAIM_WS_BROWSER_RX_BUILDERDELETE,
+ SAIM_WS_BROWSER_RX_OPENSHELL,
+ SAIM_WS_BROWSER_RX_CLOSESHELL,
+ SAIM_WS_BROWSER_RX_PTYDATA,
};
@@ -785,6 +794,9 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf,
break;
case SAIM_WS_BROWSER_RX_WATCHER_SERVICES:
+ case SAIM_WS_BROWSER_RX_OPENSHELL:
+ case SAIM_WS_BROWSER_RX_CLOSESHELL:
+ case SAIM_WS_BROWSER_RX_PTYDATA:
break;
default:
diff --git a/src/web/w-ws-server.c b/src/web/w-ws-server.c
index a994150..ecd466b 100644
--- a/src/web/w-ws-server.c
+++ b/src/web/w-ws-server.c
@@ -61,6 +61,8 @@ const lws_struct_map_t lsm_schema_json_map[] = {
"com.warmcat.sai.power_managed_builders"),
LSM_SCHEMA (sai_pcon_energy_report_t, NULL, lsm_pcon_energy_report,
/* shares struct */ "com.warmcat.sai.pcon_energy"),
+ LSM_SCHEMA (sai_ptydata_t, NULL, lsm_ptydata,
+ "com.warmcat.sai.ptydata"),
};
enum {
@@ -74,6 +76,7 @@ enum {
SAIS_WS_WEBSRV_RX_BUILD_METRIC,
SAIS_WS_WEBSRV_RX_POWER_MANAGED_BUILDERS,
SAIS_WS_WEBSRV_RX_PCON_ENERGY,
+ SAIS_WS_WEBSRV_RX_PTYDATA,
};
/*
@@ -126,19 +129,34 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
*/
switch (m->a.top_schema_index) {
case SAIS_WS_WEBSRV_RX_TASKACTIVITY:
- saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, p, rem,
- lws_write_ws_flags(LWS_WRITE_TEXT,
- is_start,
- 0)); /* Not EOM */
+ case SAIS_WS_WEBSRV_RX_PTYDATA:
+ {
+ uint8_t *tmp = malloc(LWS_PRE + rem);
+ if (tmp) {
+ memcpy(tmp + LWS_PRE, p, rem);
+ saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, tmp + LWS_PRE, rem,
+ lws_write_ws_flags(LWS_WRITE_TEXT,
+ is_start,
+ 0)); /* Not EOM */
+ free(tmp);
+ }
break;
+ }
case SAIS_WS_WEBSRV_RX_LOADREPORT:
- lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) {
- struct pss *pss = lws_container_of(pt, struct pss, same);
- if (!pss->is_gitohashi)
- saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, p, rem,
- lws_write_ws_flags(LWS_WRITE_TEXT, is_start, 0));
- } lws_end_foreach_dll(pt);
+ {
+ uint8_t *tmp = malloc(LWS_PRE + rem);
+ if (tmp) {
+ memcpy(tmp + LWS_PRE, p, rem);
+ lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) {
+ struct pss *pss = lws_container_of(pt, struct pss, same);
+ if (!pss->is_gitohashi)
+ saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, tmp + LWS_PRE, rem,
+ lws_write_ws_flags(LWS_WRITE_TEXT, is_start, 0));
+ } lws_end_foreach_dll(pt);
+ free(tmp);
+ }
break;
+ }
default:
// lwsl_err("%s: SWALLOWING %.*s\n", __func__, (int)len, buf);
break;
@@ -156,20 +174,35 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
case SAIS_WS_WEBSRV_RX_TASKCHANGE:
case SAIS_WS_WEBSRV_RX_EVENTCHANGE:
case SAIS_WS_WEBSRV_RX_TASKACTIVITY:
- saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, p, consumed,
- lws_write_ws_flags(LWS_WRITE_TEXT,
- is_start,
- 1)); /* Force EOM */
+ case SAIS_WS_WEBSRV_RX_PTYDATA:
+ {
+ uint8_t *tmp = malloc(LWS_PRE + consumed);
+ if (tmp) {
+ memcpy(tmp + LWS_PRE, p, consumed);
+ saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, tmp + LWS_PRE, consumed,
+ lws_write_ws_flags(LWS_WRITE_TEXT,
+ is_start,
+ 1)); /* Force EOM */
+ free(tmp);
+ }
break;
+ }
case SAIS_WS_WEBSRV_RX_LOADREPORT:
- lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) {
- struct pss *pss = lws_container_of(pt, struct pss, same);
- if (!pss->is_gitohashi)
- saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, p, consumed,
- lws_write_ws_flags(LWS_WRITE_TEXT, is_start, 1));
- } lws_end_foreach_dll(pt);
+ {
+ uint8_t *tmp = malloc(LWS_PRE + consumed);
+ if (tmp) {
+ memcpy(tmp + LWS_PRE, p, consumed);
+ lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) {
+ struct pss *pss = lws_container_of(pt, struct pss, same);
+ if (!pss->is_gitohashi)
+ saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss, tmp + LWS_PRE, consumed,
+ lws_write_ws_flags(LWS_WRITE_TEXT, is_start, 1));
+ } lws_end_foreach_dll(pt);
+ free(tmp);
+ }
break;
}
+ }
/*
* If we get here, the message is fully parsed (n >= 0).