diff --git a/pkg/api/provider.go b/pkg/api/provider.go index a3adbcd..59c01e7 100644 --- a/pkg/api/provider.go +++ b/pkg/api/provider.go @@ -92,6 +92,7 @@ func (p *ProcessProvider) RegisterRoutes(rg *gin.RouterGroup) { rg.GET("/daemons/:code/:daemon/health", p.healthCheck) rg.GET("/processes", p.listProcesses) rg.GET("/processes/:id", p.getProcess) + rg.GET("/processes/:id/output", p.getProcessOutput) rg.POST("/processes/:id/kill", p.killProcess) rg.POST("/pipelines/run", p.runPipeline) } @@ -213,6 +214,16 @@ func (p *ProcessProvider) Describe() []api.RouteDescription { }, }, }, + { + Method: "GET", + Path: "/processes/:id/output", + Summary: "Get process output", + Description: "Returns the captured stdout and stderr for a managed process.", + Tags: []string{"process"}, + Response: map[string]any{ + "type": "string", + }, + }, { Method: "POST", Path: "/processes/:id/kill", @@ -382,6 +393,25 @@ func (p *ProcessProvider) getProcess(c *gin.Context) { c.JSON(http.StatusOK, api.OK(proc.Info())) } +func (p *ProcessProvider) getProcessOutput(c *gin.Context) { + if p.service == nil { + c.JSON(http.StatusServiceUnavailable, api.Fail("service_unavailable", "process service is not configured")) + return + } + + output, err := p.service.Output(c.Param("id")) + if err != nil { + status := http.StatusInternalServerError + if err == process.ErrProcessNotFound { + status = http.StatusNotFound + } + c.JSON(status, api.Fail("not_found", err.Error())) + return + } + + c.JSON(http.StatusOK, api.OK(output)) +} + func (p *ProcessProvider) killProcess(c *gin.Context) { if p.service == nil { c.JSON(http.StatusServiceUnavailable, api.Fail("service_unavailable", "process service is not configured")) diff --git a/pkg/api/provider_test.go b/pkg/api/provider_test.go index 34de0af..4787372 100644 --- a/pkg/api/provider_test.go +++ b/pkg/api/provider_test.go @@ -253,6 +253,29 @@ func TestProcessProvider_GetProcess_Good(t *testing.T) { assert.Equal(t, "echo", resp.Data.Command) } +func TestProcessProvider_GetProcessOutput_Good(t *testing.T) { + svc := newTestProcessService(t) + proc, err := svc.Start(context.Background(), "echo", "output-check") + require.NoError(t, err) + <-proc.Done() + + p := processapi.NewProvider(nil, svc, nil) + r := setupRouter(p) + w := httptest.NewRecorder() + + req, err := http.NewRequest("GET", "/api/process/processes/"+proc.ID+"/output", nil) + require.NoError(t, err) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp goapi.Response[string] + err = json.Unmarshal(w.Body.Bytes(), &resp) + require.NoError(t, err) + require.True(t, resp.Success) + assert.Contains(t, resp.Data, "output-check") +} + func TestProcessProvider_KillProcess_Good(t *testing.T) { svc := newTestProcessService(t) proc, err := svc.Start(context.Background(), "sleep", "60") @@ -289,6 +312,7 @@ func TestProcessProvider_ProcessRoutes_Unavailable(t *testing.T) { cases := []string{ "/api/process/processes", "/api/process/processes/anything", + "/api/process/processes/anything/output", "/api/process/processes/anything/kill", } diff --git a/pkg/api/ui/dist/core-process.js b/pkg/api/ui/dist/core-process.js index e312e18..d276042 100644 --- a/pkg/api/ui/dist/core-process.js +++ b/pkg/api/ui/dist/core-process.js @@ -3,8 +3,8 @@ * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const V = globalThis, se = V.ShadowRoot && (V.ShadyCSS === void 0 || V.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, re = Symbol(), ne = /* @__PURE__ */ new WeakMap(); -let $e = class { +const V = globalThis, ie = V.ShadowRoot && (V.ShadyCSS === void 0 || V.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, re = Symbol(), ae = /* @__PURE__ */ new WeakMap(); +let ye = class { constructor(e, t, i) { if (this._$cssResult$ = !0, i !== re) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); this.cssText = e, this.t = t; @@ -12,9 +12,9 @@ let $e = class { get styleSheet() { let e = this.o; const t = this.t; - if (se && e === void 0) { + if (ie && e === void 0) { const i = t !== void 0 && t.length === 1; - i && (e = ne.get(t)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), i && ne.set(t, e)); + i && (e = ae.get(t)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), i && ae.set(t, e)); } return e; } @@ -22,20 +22,20 @@ let $e = class { return this.cssText; } }; -const ke = (s) => new $e(typeof s == "string" ? s : s + "", void 0, re), B = (s, ...e) => { +const ke = (s) => new ye(typeof s == "string" ? s : s + "", void 0, re), B = (s, ...e) => { const t = s.length === 1 ? s[0] : e.reduce((i, r, n) => i + ((o) => { if (o._$cssResult$ === !0) return o.cssText; if (typeof o == "number") return o; throw Error("Value passed to 'css' function must be a 'css' function result: " + o + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); })(r) + s[n + 1], s[0]); - return new $e(t, s, re); + return new ye(t, s, re); }, Se = (s, e) => { - if (se) s.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); + if (ie) s.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); else for (const t of e) { const i = document.createElement("style"), r = V.litNonce; r !== void 0 && i.setAttribute("nonce", r), i.textContent = t.cssText, s.appendChild(i); } -}, ae = se ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((e) => { +}, le = ie ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((e) => { let t = ""; for (const i of e.cssRules) t += i.cssText; return ke(t); @@ -45,7 +45,7 @@ const ke = (s) => new $e(typeof s == "string" ? s : s + "", void 0, re), B = (s, * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const { is: Pe, defineProperty: Ce, getOwnPropertyDescriptor: Ee, getOwnPropertyNames: Ue, getOwnPropertySymbols: Oe, getPrototypeOf: ze } = Object, A = globalThis, le = A.trustedTypes, Te = le ? le.emptyScript : "", X = A.reactiveElementPolyfillSupport, j = (s, e) => s, J = { toAttribute(s, e) { +const { is: Pe, defineProperty: Ce, getOwnPropertyDescriptor: Ee, getOwnPropertyNames: Ue, getOwnPropertySymbols: Oe, getPrototypeOf: ze } = Object, A = globalThis, ce = A.trustedTypes, Te = ce ? ce.emptyScript : "", Y = A.reactiveElementPolyfillSupport, j = (s, e) => s, J = { toAttribute(s, e) { switch (e) { case Boolean: s = s ? Te : null; @@ -73,7 +73,7 @@ const { is: Pe, defineProperty: Ce, getOwnPropertyDescriptor: Ee, getOwnProperty } } return t; -} }, ie = (s, e) => !Pe(s, e), ce = { attribute: !0, type: String, converter: J, reflect: !1, useDefault: !1, hasChanged: ie }; +} }, oe = (s, e) => !Pe(s, e), de = { attribute: !0, type: String, converter: J, reflect: !1, useDefault: !1, hasChanged: oe }; Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), A.litPropertyMetadata ?? (A.litPropertyMetadata = /* @__PURE__ */ new WeakMap()); let T = class extends HTMLElement { static addInitializer(e) { @@ -82,7 +82,7 @@ let T = class extends HTMLElement { static get observedAttributes() { return this.finalize(), this._$Eh && [...this._$Eh.keys()]; } - static createProperty(e, t = ce) { + static createProperty(e, t = de) { if (t.state && (t.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(e) && ((t = Object.create(t)).wrapped = !0), this.elementProperties.set(e, t), !t.noAccessor) { const i = Symbol(), r = this.getPropertyDescriptor(e, i, t); r !== void 0 && Ce(this.prototype, e, r); @@ -100,7 +100,7 @@ let T = class extends HTMLElement { }, configurable: !0, enumerable: !0 }; } static getPropertyOptions(e) { - return this.elementProperties.get(e) ?? ce; + return this.elementProperties.get(e) ?? de; } static _$Ei() { if (this.hasOwnProperty(j("elementProperties"))) return; @@ -129,8 +129,8 @@ let T = class extends HTMLElement { const t = []; if (Array.isArray(e)) { const i = new Set(e.flat(1 / 0).reverse()); - for (const r of i) t.unshift(ae(r)); - } else e !== void 0 && t.push(ae(e)); + for (const r of i) t.unshift(le(r)); + } else e !== void 0 && t.push(le(e)); return t; } static _$Eu(e, t) { @@ -202,7 +202,7 @@ let T = class extends HTMLElement { var o; if (e !== void 0) { const l = this.constructor; - if (r === !1 && (n = this[e]), i ?? (i = l.getPropertyOptions(e)), !((i.hasChanged ?? ie)(n, t) || i.useDefault && i.reflect && n === ((o = this._$Ej) == null ? void 0 : o.get(e)) && !this.hasAttribute(l._$Eu(e, i)))) return; + if (r === !1 && (n = this[e]), i ?? (i = l.getPropertyOptions(e)), !((i.hasChanged ?? oe)(n, t) || i.useDefault && i.reflect && n === ((o = this._$Ej) == null ? void 0 : o.get(e)) && !this.hasAttribute(l._$Eu(e, i)))) return; this.C(e, t, i); } this.isUpdatePending === !1 && (this._$ES = this._$EP()); @@ -278,30 +278,30 @@ let T = class extends HTMLElement { firstUpdated(e) { } }; -T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[j("elementProperties")] = /* @__PURE__ */ new Map(), T[j("finalized")] = /* @__PURE__ */ new Map(), X == null || X({ ReactiveElement: T }), (A.reactiveElementVersions ?? (A.reactiveElementVersions = [])).push("2.1.2"); +T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[j("elementProperties")] = /* @__PURE__ */ new Map(), T[j("finalized")] = /* @__PURE__ */ new Map(), Y == null || Y({ ReactiveElement: T }), (A.reactiveElementVersions ?? (A.reactiveElementVersions = [])).push("2.1.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const N = globalThis, de = (s) => s, Z = N.trustedTypes, he = Z ? Z.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, ye = "$lit$", x = `lit$${Math.random().toFixed(9).slice(2)}$`, ve = "?" + x, De = `<${ve}>`, E = document, I = () => E.createComment(""), L = (s) => s === null || typeof s != "object" && typeof s != "function", oe = Array.isArray, Me = (s) => oe(s) || typeof (s == null ? void 0 : s[Symbol.iterator]) == "function", Y = `[ -\f\r]`, R = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, pe = /-->/g, ue = />/g, S = RegExp(`>|${Y}(?:([^\\s"'>=/]+)(${Y}*=${Y}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`, "g"), me = /'/g, fe = /"/g, _e = /^(?:script|style|textarea|title)$/i, He = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), c = He(1), D = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), ge = /* @__PURE__ */ new WeakMap(), P = E.createTreeWalker(E, 129); -function we(s, e) { - if (!oe(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return he !== void 0 ? he.createHTML(e) : e; +const N = globalThis, he = (s) => s, Z = N.trustedTypes, pe = Z ? Z.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, ve = "$lit$", x = `lit$${Math.random().toFixed(9).slice(2)}$`, _e = "?" + x, De = `<${_e}>`, U = document, I = () => U.createComment(""), L = (s) => s === null || typeof s != "object" && typeof s != "function", ne = Array.isArray, Me = (s) => ne(s) || typeof (s == null ? void 0 : s[Symbol.iterator]) == "function", ee = `[ +\f\r]`, R = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, ue = /-->/g, me = />/g, P = RegExp(`>|${ee}(?:([^\\s"'>=/]+)(${ee}*=${ee}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`, "g"), fe = /'/g, ge = /"/g, we = /^(?:script|style|textarea|title)$/i, He = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), c = He(1), D = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), be = /* @__PURE__ */ new WeakMap(), C = U.createTreeWalker(U, 129); +function xe(s, e) { + if (!ne(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return pe !== void 0 ? pe.createHTML(e) : e; } const Re = (s, e) => { const t = s.length - 1, i = []; let r, n = e === 2 ? "" : e === 3 ? "" : "", o = R; for (let l = 0; l < t; l++) { const a = s[l]; - let p, m, h = -1, b = 0; - for (; b < a.length && (o.lastIndex = b, m = o.exec(a), m !== null); ) b = o.lastIndex, o === R ? m[1] === "!--" ? o = pe : m[1] !== void 0 ? o = ue : m[2] !== void 0 ? (_e.test(m[2]) && (r = RegExp("" ? (o = r ?? R, h = -1) : m[1] === void 0 ? h = -2 : (h = o.lastIndex - m[2].length, p = m[1], o = m[3] === void 0 ? S : m[3] === '"' ? fe : me) : o === fe || o === me ? o = S : o === pe || o === ue ? o = R : (o = S, r = void 0); - const w = o === S && s[l + 1].startsWith("/>") ? " " : ""; - n += o === R ? a + De : h >= 0 ? (i.push(p), a.slice(0, h) + ye + a.slice(h) + x + w) : a + x + (h === -2 ? l : w); + let p, m, h = -1, $ = 0; + for (; $ < a.length && (o.lastIndex = $, m = o.exec(a), m !== null); ) $ = o.lastIndex, o === R ? m[1] === "!--" ? o = ue : m[1] !== void 0 ? o = me : m[2] !== void 0 ? (we.test(m[2]) && (r = RegExp("" ? (o = r ?? R, h = -1) : m[1] === void 0 ? h = -2 : (h = o.lastIndex - m[2].length, p = m[1], o = m[3] === void 0 ? P : m[3] === '"' ? ge : fe) : o === ge || o === fe ? o = P : o === ue || o === me ? o = R : (o = P, r = void 0); + const w = o === P && s[l + 1].startsWith("/>") ? " " : ""; + n += o === R ? a + De : h >= 0 ? (i.push(p), a.slice(0, h) + ve + a.slice(h) + x + w) : a + x + (h === -2 ? l : w); } - return [we(s, n + (s[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), i]; + return [xe(s, n + (s[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), i]; }; class q { constructor({ strings: e, _$litType$: t }, i) { @@ -309,25 +309,25 @@ class q { this.parts = []; let n = 0, o = 0; const l = e.length - 1, a = this.parts, [p, m] = Re(e, t); - if (this.el = q.createElement(p, i), P.currentNode = this.el.content, t === 2 || t === 3) { + if (this.el = q.createElement(p, i), C.currentNode = this.el.content, t === 2 || t === 3) { const h = this.el.content.firstChild; h.replaceWith(...h.childNodes); } - for (; (r = P.nextNode()) !== null && a.length < l; ) { + for (; (r = C.nextNode()) !== null && a.length < l; ) { if (r.nodeType === 1) { - if (r.hasAttributes()) for (const h of r.getAttributeNames()) if (h.endsWith(ye)) { - const b = m[o++], w = r.getAttribute(h).split(x), K = /([.?@])?(.*)/.exec(b); - a.push({ type: 1, index: n, name: K[2], strings: w, ctor: K[1] === "." ? Ne : K[1] === "?" ? Ie : K[1] === "@" ? Le : G }), r.removeAttribute(h); + if (r.hasAttributes()) for (const h of r.getAttributeNames()) if (h.endsWith(ve)) { + const $ = m[o++], w = r.getAttribute(h).split(x), K = /([.?@])?(.*)/.exec($); + a.push({ type: 1, index: n, name: K[2], strings: w, ctor: K[1] === "." ? Ne : K[1] === "?" ? Ie : K[1] === "@" ? Le : Q }), r.removeAttribute(h); } else h.startsWith(x) && (a.push({ type: 6, index: n }), r.removeAttribute(h)); - if (_e.test(r.tagName)) { - const h = r.textContent.split(x), b = h.length - 1; - if (b > 0) { + if (we.test(r.tagName)) { + const h = r.textContent.split(x), $ = h.length - 1; + if ($ > 0) { r.textContent = Z ? Z.emptyScript : ""; - for (let w = 0; w < b; w++) r.append(h[w], I()), P.nextNode(), a.push({ type: 2, index: ++n }); - r.append(h[b], I()); + for (let w = 0; w < $; w++) r.append(h[w], I()), C.nextNode(), a.push({ type: 2, index: ++n }); + r.append(h[$], I()); } } - } else if (r.nodeType === 8) if (r.data === ve) a.push({ type: 2, index: n }); + } else if (r.nodeType === 8) if (r.data === _e) a.push({ type: 2, index: n }); else { let h = -1; for (; (h = r.data.indexOf(x, h + 1)) !== -1; ) a.push({ type: 7, index: n }), h += x.length - 1; @@ -336,7 +336,7 @@ class q { } } static createElement(e, t) { - const i = E.createElement("template"); + const i = U.createElement("template"); return i.innerHTML = e, i; } } @@ -358,24 +358,24 @@ class je { return this._$AM._$AU; } u(e) { - const { el: { content: t }, parts: i } = this._$AD, r = ((e == null ? void 0 : e.creationScope) ?? E).importNode(t, !0); - P.currentNode = r; - let n = P.nextNode(), o = 0, l = 0, a = i[0]; + const { el: { content: t }, parts: i } = this._$AD, r = ((e == null ? void 0 : e.creationScope) ?? U).importNode(t, !0); + C.currentNode = r; + let n = C.nextNode(), o = 0, l = 0, a = i[0]; for (; a !== void 0; ) { if (o === a.index) { let p; - a.type === 2 ? p = new W(n, n.nextSibling, this, e) : a.type === 1 ? p = new a.ctor(n, a.name, a.strings, this, e) : a.type === 6 && (p = new qe(n, this, e)), this._$AV.push(p), a = i[++l]; + a.type === 2 ? p = new F(n, n.nextSibling, this, e) : a.type === 1 ? p = new a.ctor(n, a.name, a.strings, this, e) : a.type === 6 && (p = new qe(n, this, e)), this._$AV.push(p), a = i[++l]; } - o !== (a == null ? void 0 : a.index) && (n = P.nextNode(), o++); + o !== (a == null ? void 0 : a.index) && (n = C.nextNode(), o++); } - return P.currentNode = E, r; + return C.currentNode = U, r; } p(e) { let t = 0; for (const i of this._$AV) i !== void 0 && (i.strings !== void 0 ? (i._$AI(e, i, t), t += i.strings.length - 2) : i._$AI(e[t])), t++; } } -class W { +class F { get _$AU() { var e; return ((e = this._$AM) == null ? void 0 : e._$AU) ?? this._$Cv; @@ -404,11 +404,11 @@ class W { this._$AH !== e && (this._$AR(), this._$AH = this.O(e)); } _(e) { - this._$AH !== d && L(this._$AH) ? this._$AA.nextSibling.data = e : this.T(E.createTextNode(e)), this._$AH = e; + this._$AH !== d && L(this._$AH) ? this._$AA.nextSibling.data = e : this.T(U.createTextNode(e)), this._$AH = e; } $(e) { var n; - const { values: t, _$litType$: i } = e, r = typeof i == "number" ? this._$AC(e) : (i.el === void 0 && (i.el = q.createElement(we(i.h, i.h[0]), this.options)), i); + const { values: t, _$litType$: i } = e, r = typeof i == "number" ? this._$AC(e) : (i.el === void 0 && (i.el = q.createElement(xe(i.h, i.h[0]), this.options)), i); if (((n = this._$AH) == null ? void 0 : n._$AD) === r) this._$AH.p(t); else { const o = new je(r, this), l = o.u(this.options); @@ -416,21 +416,21 @@ class W { } } _$AC(e) { - let t = ge.get(e.strings); - return t === void 0 && ge.set(e.strings, t = new q(e)), t; + let t = be.get(e.strings); + return t === void 0 && be.set(e.strings, t = new q(e)), t; } k(e) { - oe(this._$AH) || (this._$AH = [], this._$AR()); + ne(this._$AH) || (this._$AH = [], this._$AR()); const t = this._$AH; let i, r = 0; - for (const n of e) r === t.length ? t.push(i = new W(this.O(I()), this.O(I()), this, this.options)) : i = t[r], i._$AI(n), r++; + for (const n of e) r === t.length ? t.push(i = new F(this.O(I()), this.O(I()), this, this.options)) : i = t[r], i._$AI(n), r++; r < t.length && (this._$AR(i && i._$AB.nextSibling, r), t.length = r); } _$AR(e = this._$AA.nextSibling, t) { var i; for ((i = this._$AP) == null ? void 0 : i.call(this, !1, !0, t); e !== this._$AB; ) { - const r = de(e).nextSibling; - de(e).remove(), e = r; + const r = he(e).nextSibling; + he(e).remove(), e = r; } } setConnected(e) { @@ -438,7 +438,7 @@ class W { this._$AM === void 0 && (this._$Cv = e, (t = this._$AP) == null || t.call(this, e)); } } -class G { +class Q { get tagName() { return this.element.tagName; } @@ -463,7 +463,7 @@ class G { e === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, e ?? ""); } } -class Ne extends G { +class Ne extends Q { constructor() { super(...arguments), this.type = 3; } @@ -471,7 +471,7 @@ class Ne extends G { this.element[this.name] = e === d ? void 0 : e; } } -class Ie extends G { +class Ie extends Q { constructor() { super(...arguments), this.type = 4; } @@ -479,7 +479,7 @@ class Ie extends G { this.element.toggleAttribute(this.name, !!e && e !== d); } } -class Le extends G { +class Le extends Q { constructor(e, t, i, r, n) { super(e, t, i, r, n), this.type = 5; } @@ -504,14 +504,14 @@ class qe { M(this, e); } } -const ee = N.litHtmlPolyfillSupport; -ee == null || ee(q, W), (N.litHtmlVersions ?? (N.litHtmlVersions = [])).push("3.3.2"); +const te = N.litHtmlPolyfillSupport; +te == null || te(q, F), (N.litHtmlVersions ?? (N.litHtmlVersions = [])).push("3.3.2"); const Be = (s, e, t) => { const i = (t == null ? void 0 : t.renderBefore) ?? e; let r = i._$litPart$; if (r === void 0) { const n = (t == null ? void 0 : t.renderBefore) ?? null; - i._$litPart$ = r = new W(e.insertBefore(I(), n), n, void 0, t ?? {}); + i._$litPart$ = r = new F(e.insertBefore(I(), n), n, void 0, t ?? {}); } return r._$AI(s), r; }; @@ -520,8 +520,8 @@ const Be = (s, e, t) => { * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const C = globalThis; -class $ extends T { +const E = globalThis; +class y extends T { constructor() { super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; } @@ -546,17 +546,17 @@ class $ extends T { return D; } } -var be; -$._$litElement$ = !0, $.finalized = !0, (be = C.litElementHydrateSupport) == null || be.call(C, { LitElement: $ }); -const te = C.litElementPolyfillSupport; -te == null || te({ LitElement: $ }); -(C.litElementVersions ?? (C.litElementVersions = [])).push("4.2.2"); +var $e; +y._$litElement$ = !0, y.finalized = !0, ($e = E.litElementHydrateSupport) == null || $e.call(E, { LitElement: y }); +const se = E.litElementPolyfillSupport; +se == null || se({ LitElement: y }); +(E.litElementVersions ?? (E.litElementVersions = [])).push("4.2.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const F = (s) => (e, t) => { +const W = (s) => (e, t) => { t !== void 0 ? t.addInitializer(() => { customElements.define(s, e); }) : customElements.define(s, e); @@ -566,7 +566,7 @@ const F = (s) => (e, t) => { * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const We = { attribute: !0, type: String, converter: J, reflect: !1, hasChanged: ie }, Fe = (s = We, e, t) => { +const Fe = { attribute: !0, type: String, converter: J, reflect: !1, hasChanged: oe }, We = (s = Fe, e, t) => { const { kind: i, metadata: r } = t; let n = globalThis.litPropertyMetadata.get(r); if (n === void 0 && globalThis.litPropertyMetadata.set(r, n = /* @__PURE__ */ new Map()), i === "setter" && ((s = Object.create(s)).wrapped = !0), n.set(t.name, s), i === "accessor") { @@ -588,7 +588,7 @@ const We = { attribute: !0, type: String, converter: J, reflect: !1, hasChanged: throw Error("Unsupported decorator location: " + i); }; function f(s) { - return (e, t) => typeof t == "object" ? Fe(s, e, t) : ((i, r, n) => { + return (e, t) => typeof t == "object" ? We(s, e, t) : ((i, r, n) => { const o = r.hasOwnProperty(n); return r.constructor.createProperty(n, i), o ? Object.getOwnPropertyDescriptor(r, n) : void 0; })(s, e, t); @@ -601,7 +601,7 @@ function f(s) { function u(s) { return f({ ...s, state: !0, attribute: !1 }); } -function xe(s, e) { +function Ae(s, e) { const t = new WebSocket(s); return t.onmessage = (i) => { var r, n, o, l; @@ -612,7 +612,7 @@ function xe(s, e) { } }, t; } -class Ae { +class G { constructor(e = "") { this.baseUrl = e; } @@ -652,6 +652,10 @@ class Ae { getProcess(e) { return this.request(`/processes/${e}`); } + /** Get the captured stdout/stderr for a managed process by ID. */ + getProcessOutput(e) { + return this.request(`/processes/${e}/output`); + } /** Kill a managed process by ID. */ killProcess(e) { return this.request(`/processes/${e}/kill`, { @@ -672,12 +676,12 @@ var Ke = Object.defineProperty, Ve = Object.getOwnPropertyDescriptor, k = (s, e, (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); return i && r && Ke(e, t, r), r; }; -let g = class extends $ { +let g = class extends y { constructor() { super(...arguments), this.apiUrl = "", this.daemons = [], this.loading = !0, this.error = "", this.stopping = /* @__PURE__ */ new Set(), this.checking = /* @__PURE__ */ new Set(), this.healthResults = /* @__PURE__ */ new Map(); } connectedCallback() { - super.connectedCallback(), this.api = new Ae(this.apiUrl), this.loadDaemons(); + super.connectedCallback(), this.api = new G(this.apiUrl), this.loadDaemons(); } async loadDaemons() { this.loading = !0, this.error = ""; @@ -967,19 +971,19 @@ k([ u() ], g.prototype, "healthResults", 2); g = k([ - F("core-process-daemons") + W("core-process-daemons") ], g); -var Je = Object.defineProperty, Ze = Object.getOwnPropertyDescriptor, U = (s, e, t, i) => { +var Je = Object.defineProperty, Ze = Object.getOwnPropertyDescriptor, O = (s, e, t, i) => { for (var r = i > 1 ? void 0 : i ? Ze(e, t) : e, n = s.length - 1, o; n >= 0; n--) (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); return i && r && Je(e, t, r), r; }; -let y = class extends $ { +let v = class extends y { constructor() { super(...arguments), this.apiUrl = "", this.selectedId = "", this.processes = [], this.loading = !1, this.error = "", this.killing = /* @__PURE__ */ new Set(); } connectedCallback() { - super.connectedCallback(), this.api = new Ae(this.apiUrl), this.loadProcesses(); + super.connectedCallback(), this.api = new G(this.apiUrl), this.loadProcesses(); } async loadProcesses() { this.loading = !0, this.error = ""; @@ -1076,7 +1080,7 @@ let y = class extends $ { `; } }; -y.styles = B` +v.styles = B` :host { display: block; font-family: system-ui, -apple-system, sans-serif; @@ -1240,47 +1244,81 @@ y.styles = B` margin-bottom: 1rem; } `; -U([ +O([ f({ attribute: "api-url" }) -], y.prototype, "apiUrl", 2); -U([ +], v.prototype, "apiUrl", 2); +O([ f({ attribute: "selected-id" }) -], y.prototype, "selectedId", 2); -U([ +], v.prototype, "selectedId", 2); +O([ u() -], y.prototype, "processes", 2); -U([ +], v.prototype, "processes", 2); +O([ u() -], y.prototype, "loading", 2); -U([ +], v.prototype, "loading", 2); +O([ u() -], y.prototype, "error", 2); -U([ +], v.prototype, "error", 2); +O([ u() -], y.prototype, "killing", 2); -y = U([ - F("core-process-list") -], y); -var Ge = Object.defineProperty, Qe = Object.getOwnPropertyDescriptor, O = (s, e, t, i) => { +], v.prototype, "killing", 2); +v = O([ + W("core-process-list") +], v); +var Ge = Object.defineProperty, Qe = Object.getOwnPropertyDescriptor, S = (s, e, t, i) => { for (var r = i > 1 ? void 0 : i ? Qe(e, t) : e, n = s.length - 1, o; n >= 0; n--) (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); return i && r && Ge(e, t, r), r; }; -let v = class extends $ { +let b = class extends y { constructor() { - super(...arguments), this.apiUrl = "", this.wsUrl = "", this.processId = "", this.lines = [], this.autoScroll = !0, this.connected = !1, this.ws = null; + super(...arguments), this.apiUrl = "", this.wsUrl = "", this.processId = "", this.lines = [], this.autoScroll = !0, this.connected = !1, this.loadingSnapshot = !1, this.ws = null, this.api = new G(this.apiUrl), this.syncToken = 0; } connectedCallback() { - super.connectedCallback(), this.wsUrl && this.processId && this.connect(); + super.connectedCallback(), this.syncSources(); } disconnectedCallback() { super.disconnectedCallback(), this.disconnect(); } updated(s) { - (s.has("processId") || s.has("wsUrl")) && (this.disconnect(), this.lines = [], this.wsUrl && this.processId && this.connect()), this.autoScroll && this.scrollToBottom(); + s.has("apiUrl") && (this.api = new G(this.apiUrl)), (s.has("processId") || s.has("wsUrl") || s.has("apiUrl")) && this.syncSources(), this.autoScroll && this.scrollToBottom(); + } + syncSources() { + this.disconnect(), this.lines = [], this.processId && this.loadSnapshotAndConnect(); + } + async loadSnapshotAndConnect() { + const s = ++this.syncToken; + if (this.processId) { + if (this.apiUrl) { + this.loadingSnapshot = !0; + try { + const e = await this.api.getProcessOutput(this.processId); + if (s !== this.syncToken) + return; + const t = this.linesFromOutput(e); + t.length > 0 && (this.lines = t); + } catch { + } finally { + s === this.syncToken && (this.loadingSnapshot = !1); + } + } + s === this.syncToken && this.wsUrl && this.connect(); + } + } + linesFromOutput(s) { + if (!s) + return []; + const t = s.replace(/\r\n/g, ` +`).split(` +`); + return t.length > 0 && t[t.length - 1] === "" && t.pop(), t.map((i) => ({ + text: i, + stream: "stdout", + timestamp: Date.now() + })); } connect() { - this.ws = xe(this.wsUrl, (s) => { + this.ws = Ae(this.wsUrl, (s) => { const e = s.data; if (!e) return; (s.channel ?? s.type ?? "") === "process.output" && e.id === this.processId && (this.lines = [ @@ -1328,7 +1366,7 @@ let v = class extends $ {
- ${this.lines.length === 0 ? c`
Waiting for output\u2026
` : this.lines.map( + ${this.loadingSnapshot && this.lines.length === 0 ? c`
Loading snapshot\u2026
` : this.lines.length === 0 ? c`
Waiting for output\u2026
` : this.lines.map( (s) => c`
${s.stream}${s.text} @@ -1339,7 +1377,7 @@ let v = class extends $ { ` : c`
Select a process to view its output.
`; } }; -v.styles = B` +b.styles = B` :host { display: block; font-family: system-ui, -apple-system, sans-serif; @@ -1443,33 +1481,36 @@ v.styles = B` font-size: 0.8125rem; } `; -O([ +S([ f({ attribute: "api-url" }) -], v.prototype, "apiUrl", 2); -O([ +], b.prototype, "apiUrl", 2); +S([ f({ attribute: "ws-url" }) -], v.prototype, "wsUrl", 2); -O([ +], b.prototype, "wsUrl", 2); +S([ f({ attribute: "process-id" }) -], v.prototype, "processId", 2); -O([ +], b.prototype, "processId", 2); +S([ u() -], v.prototype, "lines", 2); -O([ +], b.prototype, "lines", 2); +S([ u() -], v.prototype, "autoScroll", 2); -O([ +], b.prototype, "autoScroll", 2); +S([ u() -], v.prototype, "connected", 2); -v = O([ - F("core-process-output") -], v); -var Xe = Object.defineProperty, Ye = Object.getOwnPropertyDescriptor, Q = (s, e, t, i) => { +], b.prototype, "connected", 2); +S([ + u() +], b.prototype, "loadingSnapshot", 2); +b = S([ + W("core-process-output") +], b); +var Xe = Object.defineProperty, Ye = Object.getOwnPropertyDescriptor, X = (s, e, t, i) => { for (var r = i > 1 ? void 0 : i ? Ye(e, t) : e, n = s.length - 1, o; n >= 0; n--) (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); return i && r && Xe(e, t, r), r; }; -let H = class extends $ { +let H = class extends y { constructor() { super(...arguments), this.apiUrl = "", this.result = null, this.expandedOutputs = /* @__PURE__ */ new Set(); } @@ -1740,24 +1781,24 @@ H.styles = B` margin-bottom: 1rem; } `; -Q([ +X([ f({ attribute: "api-url" }) ], H.prototype, "apiUrl", 2); -Q([ +X([ f({ type: Object }) ], H.prototype, "result", 2); -Q([ +X([ u() ], H.prototype, "expandedOutputs", 2); -H = Q([ - F("core-process-runner") +H = X([ + W("core-process-runner") ], H); var et = Object.defineProperty, tt = Object.getOwnPropertyDescriptor, z = (s, e, t, i) => { for (var r = i > 1 ? void 0 : i ? tt(e, t) : e, n = s.length - 1, o; n >= 0; n--) (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); return i && r && et(e, t, r), r; }; -let _ = class extends $ { +let _ = class extends y { constructor() { super(...arguments), this.apiUrl = "", this.wsUrl = "", this.activeTab = "daemons", this.wsConnected = !1, this.lastEvent = "", this.selectedProcessId = "", this.ws = null, this.tabs = [ { id: "daemons", label: "Daemons" }, @@ -1772,7 +1813,7 @@ let _ = class extends $ { super.disconnectedCallback(), this.ws && (this.ws.close(), this.ws = null); } connectWs() { - this.ws = xe(this.wsUrl, (s) => { + this.ws = Ae(this.wsUrl, (s) => { this.lastEvent = s.channel ?? s.type ?? "", this.requestUpdate(); }), this.ws.onopen = () => { this.wsConnected = !0; @@ -1982,14 +2023,14 @@ z([ u() ], _.prototype, "selectedProcessId", 2); _ = z([ - F("core-process-panel") + W("core-process-panel") ], _); export { - Ae as ProcessApi, + G as ProcessApi, g as ProcessDaemons, - y as ProcessList, - v as ProcessOutput, + v as ProcessList, + b as ProcessOutput, _ as ProcessPanel, H as ProcessRunner, - xe as connectProcessEvents + Ae as connectProcessEvents }; diff --git a/ui/src/process-output.ts b/ui/src/process-output.ts index e16fcbc..91948bb 100644 --- a/ui/src/process-output.ts +++ b/ui/src/process-output.ts @@ -3,6 +3,7 @@ import { LitElement, html, css, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { connectProcessEvents, type ProcessEvent } from './shared/events.js'; +import { ProcessApi } from './shared/api.js'; interface OutputLine { text: string; @@ -131,14 +132,15 @@ export class ProcessOutput extends LitElement { @state() private lines: OutputLine[] = []; @state() private autoScroll = true; @state() private connected = false; + @state() private loadingSnapshot = false; private ws: WebSocket | null = null; + private api = new ProcessApi(this.apiUrl); + private syncToken = 0; connectedCallback() { super.connectedCallback(); - if (this.wsUrl && this.processId) { - this.connect(); - } + this.syncSources(); } disconnectedCallback() { @@ -147,12 +149,12 @@ export class ProcessOutput extends LitElement { } updated(changed: Map) { - if (changed.has('processId') || changed.has('wsUrl')) { - this.disconnect(); - this.lines = []; - if (this.wsUrl && this.processId) { - this.connect(); - } + if (changed.has('apiUrl')) { + this.api = new ProcessApi(this.apiUrl); + } + + if (changed.has('processId') || changed.has('wsUrl') || changed.has('apiUrl')) { + this.syncSources(); } if (this.autoScroll) { @@ -160,6 +162,66 @@ export class ProcessOutput extends LitElement { } } + private syncSources() { + this.disconnect(); + this.lines = []; + if (!this.processId) { + return; + } + + void this.loadSnapshotAndConnect(); + } + + private async loadSnapshotAndConnect() { + const token = ++this.syncToken; + + if (!this.processId) { + return; + } + + if (this.apiUrl) { + this.loadingSnapshot = true; + try { + const output = await this.api.getProcessOutput(this.processId); + if (token !== this.syncToken) { + return; + } + const snapshot = this.linesFromOutput(output); + if (snapshot.length > 0) { + this.lines = snapshot; + } + } catch { + // Ignore missing snapshot data and continue with live streaming. + } finally { + if (token === this.syncToken) { + this.loadingSnapshot = false; + } + } + } + + if (token === this.syncToken && this.wsUrl) { + this.connect(); + } + } + + private linesFromOutput(output: string): OutputLine[] { + if (!output) { + return []; + } + + const normalized = output.replace(/\r\n/g, '\n'); + const parts = normalized.split('\n'); + if (parts.length > 0 && parts[parts.length - 1] === '') { + parts.pop(); + } + + return parts.map((text) => ({ + text, + stream: 'stdout' as const, + timestamp: Date.now(), + })); + } + private connect() { this.ws = connectProcessEvents(this.wsUrl, (event: ProcessEvent) => { const data = event.data; @@ -231,7 +293,9 @@ export class ProcessOutput extends LitElement {
- ${this.lines.length === 0 + ${this.loadingSnapshot && this.lines.length === 0 + ? html`
Loading snapshot\u2026
` + : this.lines.length === 0 ? html`
Waiting for output\u2026
` : this.lines.map( (line) => html` diff --git a/ui/src/shared/api.ts b/ui/src/shared/api.ts index 56e616c..0d05c87 100644 --- a/ui/src/shared/api.ts +++ b/ui/src/shared/api.ts @@ -31,6 +31,7 @@ export interface ProcessInfo { args: string[]; dir: string; startedAt: string; + running: boolean; status: 'pending' | 'running' | 'exited' | 'failed' | 'killed'; exitCode: number; duration: number; @@ -126,6 +127,11 @@ export class ProcessApi { return this.request(`/processes/${id}`); } + /** Get the captured stdout/stderr for a managed process by ID. */ + getProcessOutput(id: string): Promise { + return this.request(`/processes/${id}/output`); + } + /** Kill a managed process by ID. */ killProcess(id: string): Promise<{ killed: boolean }> { return this.request<{ killed: boolean }>(`/processes/${id}/kill`, {