--- a/luci-app-dockerman/root/usr/share/luci/menu.d/luci-app-dockerman.json +++ b/luci-app-dockerman/root/usr/share/luci/menu.d/luci-app-dockerman.json @@ -1,7 +1,7 @@ { - "admin/services/dockerman": { - "title": "Dockerman JS", - "order": "60", + "admin/dockerman": { + "title": "Docker", + "order": "40", "action": { "type": "firstchild" }, @@ -15,7 +15,7 @@ } }, - "admin/services/dockerman/overview": { + "admin/dockerman/overview": { "title": "Overview", "order": 1, "action": { @@ -27,7 +27,7 @@ } }, - "admin/services/dockerman/configuration": { + "admin/dockerman/configuration": { "title": "Configuration", "order": 2, "action": { @@ -39,17 +39,17 @@ } }, - "admin/services/dockerman/container/archive/*": { + "admin/dockerman/container/archive/*": { "action": { "type": "alias", - "path": "admin/services/dockerman/containers" + "path": "admin/dockerman/containers" }, "depends": { "acl": [ "luci-app-dockerman" ] } }, - "admin/services/dockerman/container/archive/put/*": { + "admin/dockerman/container/archive/put/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -64,7 +64,7 @@ } }, - "admin/services/dockerman/container/archive/get/*": { + "admin/dockerman/container/archive/get/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -79,7 +79,7 @@ } }, - "admin/services/dockerman/container/export/*": { + "admin/dockerman/container/export/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -94,7 +94,7 @@ } }, - "admin/services/dockerman/container/*": { + "admin/dockerman/container/*": { "title_hide": "Container", "action": { "type": "view", @@ -105,7 +105,7 @@ } }, - "admin/services/dockerman/containers": { + "admin/dockerman/containers": { "title": "Containers", "order": 3, "action": { @@ -117,7 +117,7 @@ } }, - "admin/services/dockerman/containers/prune": { + "admin/dockerman/containers/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -133,17 +133,17 @@ } }, - "admin/services/dockerman/container": { + "admin/dockerman/container": { "action": { "type": "alias", - "path": "admin/services/dockerman/containers" + "path": "admin/dockerman/containers" }, "depends": { "acl": [ "luci-app-dockerman" ] } }, - "admin/services/dockerman/container_new": { + "admin/dockerman/container_new": { "title_hide": "Container", "action": { "type": "view", @@ -154,7 +154,7 @@ } }, - "admin/services/dockerman/images/build": { + "admin/dockerman/images/build": { "action": { "type": "function", "module": "luci.controller.docker", @@ -170,7 +170,7 @@ } }, - "admin/services/dockerman/images/build/prune": { + "admin/dockerman/images/build/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -186,7 +186,7 @@ } }, - "admin/services/dockerman/images/get/*": { + "admin/dockerman/images/get/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -201,7 +201,7 @@ } }, - "admin/services/dockerman/images/load": { + "admin/dockerman/images/load": { "action": { "type": "function", "module": "luci.controller.docker", @@ -217,7 +217,7 @@ } }, - "admin/services/dockerman/images/prune": { + "admin/dockerman/images/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -233,7 +233,7 @@ } }, - "admin/services/dockerman/images": { + "admin/dockerman/images": { "title": "Images", "order": 4, "action": { @@ -242,7 +242,7 @@ } }, - "admin/services/dockerman/images/create": { + "admin/dockerman/images/create": { "action": { "type": "function", "module": "luci.controller.docker", @@ -258,7 +258,7 @@ } }, - "admin/services/dockerman/images/push/*": { + "admin/dockerman/images/push/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -274,17 +274,17 @@ } }, - "admin/services/dockerman/image": { + "admin/dockerman/image": { "action": { "type": "alias", - "path": "admin/services/dockerman/images" + "path": "admin/dockerman/images" }, "depends": { "acl": [ "luci-app-dockerman" ] } }, - "admin/services/dockerman/network/*": { + "admin/dockerman/network/*": { "title_hide": "Network", "action": { "type": "view", @@ -295,7 +295,7 @@ } }, - "admin/services/dockerman/networks": { + "admin/dockerman/networks": { "title": "Networks", "order": 5, "action": { @@ -307,7 +307,7 @@ } }, - "admin/services/dockerman/networks/prune": { + "admin/dockerman/networks/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -323,7 +323,7 @@ } }, - "admin/services/dockerman/network_new": { + "admin/dockerman/network_new": { "title_hide": "Network", "action": { "type": "view", @@ -334,17 +334,17 @@ } }, - "admin/services/dockerman/network": { + "admin/dockerman/network": { "action": { "type": "alias", - "path": "admin/services/dockerman/networks" + "path": "admin/dockerman/networks" }, "depends": { "acl": [ "luci-app-dockerman" ] } }, - "admin/services/dockerman/volumes": { + "admin/dockerman/volumes": { "title": "Volumes", "order": 6, "action": { @@ -356,7 +356,7 @@ } }, - "admin/services/dockerman/volumes/prune": { + "admin/dockerman/volumes/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -372,7 +372,7 @@ } }, - "admin/services/dockerman/docker/events": { + "admin/dockerman/docker/events": { "action": { "type": "function", "module": "luci.controller.docker", @@ -388,7 +388,7 @@ } }, - "admin/services/dockerman/events": { + "admin/dockerman/events": { "title": "Events", "order": 7, "action": { --- a/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js @@ -437,7 +437,7 @@ const dv = view.extend({ outputText: '', // Initialize output text get dockerman_url() { - return L.url('admin/services/dockerman'); + return L.url('admin/dockerman'); }, parseHeaders(headers, array) { @@ -1280,13 +1280,14 @@ const ansiToHtml = function(text) { // Escape HTML special characters const escapeHtml = (str) => { const map = { + ' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; - return str.replace(/[&<>"']/g, m => map[m]); + return str.replace(/[&<>"' ]/g, m => map[m]); }; // Split by ANSI escape sequences and process --- /dev/null +++ b/luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman @@ -0,0 +1,8 @@ +#!/bin/sh + +uci -q batch <<-EOF >/dev/null + set uhttpd.main.script_timeout="360" + commit uhttpd +EOF + +/etc/init.d/dockerd restart --- a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js @@ -66,6 +66,7 @@ return L.view.extend({ o.value('https://docker.io'); o.value('https://ghcr.io'); o.value('https://hub-mirror.c.163.com'); + o.value('https://ustc-edu-cn.mirror.aliyuncs.com'); o = s.taboption('globals', form.ListValue, 'log_level', _('Log Level'), --- a/luci-app-dockerman/htdocs/luci-static/resources/dockerman/api.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/dockerman/api.js @@ -116,6 +116,22 @@ function processLines(buffer, onChunk) { return buffer; } +// Parse the docker multiplexed stream format, https://docs.docker.com/reference/api/engine/version/v1.43/#stream-format +function parse_multiplexed_stream(buffer) { + const output = []; + + while (buffer.length > 0) { + if (buffer.length < 8) break; // Not enough data for header + let stream_type = buffer.charCodeAt(0); + let payload_length = (buffer.charCodeAt(4) << 24) | (buffer.charCodeAt(5) << 16) | (buffer.charCodeAt(6) << 8) | buffer.charCodeAt(7); + if (buffer.length < 8 + payload_length) break; // Not enough data for payload + let payload = buffer.slice(8, 8 + payload_length); + output.push({ type: stream_type, payload: payload }); + buffer = buffer.slice(8 + payload_length); + } + + return output; +} function call_docker(method, path, options = {}) { return loadPromise.then(() => { @@ -228,20 +244,24 @@ function call_docker(method, path, options = {}) { let parsed = safeText || text; const contentType = response.headers.get('content-type') || ''; - // Try normal JSON parse first - try { - parsed = JSON.parse(text); - } catch (err) { - // If the payload is newline-delimited JSON (Docker events), split and parse each line - if (['application/json', - 'application/x-ndjson', - 'application/json-seq'].includes(contentType) || safeText.includes('\n')) { - const lines = safeText.split(/\r?\n/).filter(Boolean); - try { - parsed = lines.map(l => JSON.parse(l)); - } catch (err2) { - // Fall back to raw text if parsing fails - parsed = text; + if ("application/vnd.docker.multiplexed-stream" == contentType) { + parsed = parse_multiplexed_stream(safeText); + } else { + // Try normal JSON parse first + try { + parsed = JSON.parse(text); + } catch (err) { + // If the payload is newline-delimited JSON (Docker events), split and parse each line + if (['application/json', + 'application/x-ndjson', + 'application/json-seq'].includes(contentType) || safeText.includes('\n')) { + const lines = safeText.split(/\r?\n/).filter(Boolean); + try { + parsed = lines.map(l => JSON.parse(l)); + } catch (err2) { + // Fall back to raw text if parsing fails + parsed = text; + } } } } --- a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js @@ -1685,7 +1685,10 @@ return dm2.dv.extend({ return false; } - const logText = response?.body || _('No logs available'); + let logText = response?.body || _('No logs available'); + if ("application/vnd.docker.multiplexed-stream" == response?.headers['content-type']) { + logText = (response?.body || []).map((frame)=>frame.payload).join(''); + } // Convert ANSI codes to HTML and set innerHTML logsDiv.innerHTML = dm2.ansiToHtml(logText); logsDiv.scrollTop = logsDiv.scrollHeight; --- a/luci-app-dockerman/root/usr/share/rpcd/ucode/docker_rpc.uc +++ b/luci-app-dockerman/root/usr/share/rpcd/ucode/docker_rpc.uc @@ -100,6 +100,23 @@ function coerce_values_to_string(obj) { return obj; }; +// Parse the docker multiplexed stream format, https://docs.docker.com/reference/api/engine/version/v1.43/#stream-format +function parse_multiplexed_stream(buffer) { + let output = []; + + while (length(buffer) > 0) { + if (length(buffer) < 8) break; // Not enough data for header + let stream_type = ord(buffer, 0); + let payload_length = ord(buffer, 4) << 24 | ord(buffer, 5) << 16 | ord(buffer, 6) << 8 | ord(buffer, 7); + if (length(buffer) < 8 + payload_length) break; // Not enough data for payload + let payload = substr(buffer, 8, payload_length); + push(output, { type: stream_type, payload: payload }); + buffer = substr(buffer, 8 + payload_length); + } + + return output; +}; + function call_docker(method, path, options) { options = options || {}; const headers = options.headers || {}; @@ -277,6 +294,8 @@ function call_docker(method, path, options) { } else { response_body = data; } + } else if (response_headers['content-type'] === 'application/vnd.docker.multiplexed-stream' && response_body) { + response_body = parse_multiplexed_stream(response_body); } return {