Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / assets / strict-csp.svg
Author[]Andy Green <andy@warmcat.com> 2026-06-21 07:41 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-06-21 10:17 UTC
Treee5c9c8c697bf5b1d5e6211b24b4a8a5a18c45ad5   Raw Patch
 
terminal-emulator
terminal-emulator
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 === '') ? '&nbsp;' : this.escapeHtml(cell.char); + } + html += '<br>'; + } + if (lastStyle !== null) html += '</span>'; + return html; + } + + escapeHtml(s) { + return s.replace(/[&<>"']/g, function(m) { + return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[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).
Page fetched 0s ago, creation time: 11ms (vhost etag hits: 0%, cache hits: 0%)