execweave 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execweave/__init__.py +3 -0
- execweave/__main__.py +5 -0
- execweave/analysis.py +406 -0
- execweave/backends.py +63 -0
- execweave/benchmark.py +80 -0
- execweave/claude_adapter.py +448 -0
- execweave/claude_hook_cli.py +101 -0
- execweave/claude_record.py +106 -0
- execweave/cli.py +588 -0
- execweave/codex_adapter.py +314 -0
- execweave/codex_hook_cli.py +98 -0
- execweave/codex_record.py +111 -0
- execweave/collector.py +301 -0
- execweave/correlation.py +604 -0
- execweave/cursor_adapter.py +347 -0
- execweave/cursor_hook_cli.py +82 -0
- execweave/cursor_record.py +96 -0
- execweave/filesystem.py +103 -0
- execweave/focus.py +118 -0
- execweave/gemini_adapter.py +265 -0
- execweave/gemini_hook_cli.py +77 -0
- execweave/gemini_record.py +94 -0
- execweave/graph.py +300 -0
- execweave/graph_ops.py +446 -0
- execweave/inference_gateway.py +422 -0
- execweave/inference_gateway_cli.py +106 -0
- execweave/inference_identity.py +76 -0
- execweave/inference_identity_cli.py +60 -0
- execweave/live.py +275 -0
- execweave/model_runtime.py +535 -0
- execweave/model_runtime_cli.py +154 -0
- execweave/opencode_adapter.py +316 -0
- execweave/opencode_hook_cli.py +57 -0
- execweave/opencode_plugin_cli.py +110 -0
- execweave/opencode_record.py +96 -0
- execweave/overhead_benchmark.py +440 -0
- execweave/provider_record.py +215 -0
- execweave/schema.py +62 -0
- execweave/semantic.py +346 -0
- execweave/sink.py +33 -0
- execweave/strace_backend.py +682 -0
- execweave/validate.py +193 -0
- execweave/viewer.py +283 -0
- execweave/workflow.py +114 -0
- execweave-0.6.0.dist-info/METADATA +356 -0
- execweave-0.6.0.dist-info/RECORD +49 -0
- execweave-0.6.0.dist-info/WHEEL +4 -0
- execweave-0.6.0.dist-info/entry_points.txt +17 -0
- execweave-0.6.0.dist-info/licenses/LICENSE +21 -0
execweave/live.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import webbrowser
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from socketserver import TCPServer
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from .backends import create_collector
|
|
15
|
+
from .graph import build_execution_graph, write_execution_graph
|
|
16
|
+
from .sink import JsonlSink
|
|
17
|
+
from .validate import validate_event_stream
|
|
18
|
+
from .viewer import render_graph_html, write_graph_html
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_LIVE_HTML = r"""<!doctype html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8">
|
|
25
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
26
|
+
<title>ExecWeave Live</title>
|
|
27
|
+
<style>
|
|
28
|
+
:root{color-scheme:dark;--bg:#0b0f14;--panel:#111821;--panel2:#18222e;--text:#e8edf3;--muted:#8ea0b5;--border:#2a3949;--edge:#72869c;--causal:#70d6a6;--noncausal:#f2b76d;--selected:#73b7ff}
|
|
29
|
+
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--text);font:14px/1.4 system-ui,sans-serif}
|
|
30
|
+
#app{display:grid;grid-template-columns:minmax(0,1fr) 320px;grid-template-rows:54px minmax(0,1fr);width:100%;height:100%}
|
|
31
|
+
header{grid-column:1/3;display:flex;align-items:center;gap:12px;padding:0 14px;background:var(--panel);border-bottom:1px solid var(--border)}
|
|
32
|
+
#status{border:1px solid var(--border);border-radius:999px;padding:3px 8px;color:var(--causal)}#stats{color:var(--muted);white-space:nowrap}
|
|
33
|
+
header input{margin-left:auto;width:min(420px,36vw);padding:7px 9px;border:1px solid var(--border);border-radius:7px;background:var(--panel2);color:var(--text)}
|
|
34
|
+
#wrap{position:relative;overflow:hidden}svg{width:100%;height:100%;display:block;cursor:grab;user-select:none}svg.panning{cursor:grabbing}
|
|
35
|
+
aside{overflow:auto;border-left:1px solid var(--border);background:var(--panel);padding:13px}aside pre{white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.45 ui-monospace,monospace}
|
|
36
|
+
.controls{position:absolute;left:10px;top:10px;z-index:3}.controls button{padding:6px 9px;border:1px solid var(--border);border-radius:7px;background:var(--panel);color:var(--text);cursor:pointer}
|
|
37
|
+
.node rect{stroke:var(--border);stroke-width:1.1;rx:8;ry:8}.node text{fill:var(--text);pointer-events:none}.node .type{fill:var(--muted);font-size:9px}.node.dim{opacity:.12}
|
|
38
|
+
.edge{fill:none;stroke:var(--edge);stroke-width:1.3;opacity:.72;marker-end:url(#arrow)}.edge.causal{stroke:var(--causal)}.edge.noncausal{stroke:var(--noncausal);stroke-dasharray:6 5}.edge.dim{opacity:.06}.edge-hit{fill:none;stroke:transparent;stroke-width:10;cursor:pointer}.label{fill:var(--muted);font-size:8.5px;pointer-events:none}
|
|
39
|
+
@media(max-width:820px){#app{grid-template-columns:1fr;grid-template-rows:54px minmax(0,1fr) 190px}header{grid-column:1}aside{border-left:0;border-top:1px solid var(--border)}}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div id="app">
|
|
44
|
+
<header><strong>ExecWeave Live</strong><span id="status">LIVE</span><span id="stats">Waiting for events…</span><input id="search" placeholder="Search node id, name, type…"></header>
|
|
45
|
+
<div id="wrap"><div class="controls"><button id="fit">Fit</button></div><svg id="svg"><defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" fill="context-stroke"></path></marker></defs><g id="viewport"><g id="edges"></g><g id="labels"></g><g id="nodes"></g></g></svg></div>
|
|
46
|
+
<aside><strong>Selection</strong><div id="details" style="color:var(--muted);margin-top:10px">Click a node or edge.</div></aside>
|
|
47
|
+
</div>
|
|
48
|
+
<script>
|
|
49
|
+
(()=>{
|
|
50
|
+
const svg=document.getElementById('svg'),viewport=document.getElementById('viewport'),edgeLayer=document.getElementById('edges'),labelLayer=document.getElementById('labels'),nodeLayer=document.getElementById('nodes'),details=document.getElementById('details'),search=document.getElementById('search'),stats=document.getElementById('stats'),status=document.getElementById('status');
|
|
51
|
+
let graph={nodes:[],edges:[]},positions=new Map(),transform={x:35,y:35,scale:1},pan=null,lastSignature='';
|
|
52
|
+
function color(type){let h=0;for(const c of String(type||'unknown'))h=((h<<5)-h+c.charCodeAt(0))|0;return `hsl(${Math.abs(h)%360} 38% 27%)`}
|
|
53
|
+
function short(node){const value=node.name||node.id||node.type||'node';return value.length>28?value.slice(0,25)+'…':value}
|
|
54
|
+
function layout(){const ids=graph.nodes.map(n=>n.id),nodeMap=new Map(graph.nodes.map(n=>[n.id,n])),incoming=new Map(ids.map(id=>[id,0])),outgoing=new Map(ids.map(id=>[id,[]]));for(const e of graph.edges){if(!nodeMap.has(e.source)||!nodeMap.has(e.target))continue;incoming.set(e.target,(incoming.get(e.target)||0)+1);outgoing.get(e.source).push(e.target)}let roots=ids.filter(id=>(incoming.get(id)||0)===0);if(!roots.length&&ids.length)roots=[ids[0]];const depth=new Map(),queue=roots.map(id=>[id,0]);for(let i=0;i<queue.length;i++){const [id,d]=queue[i];if(depth.has(id))continue;depth.set(id,d);for(const next of outgoing.get(id)||[])queue.push([next,d+1])}const max=Math.max(0,...depth.values());for(const id of ids)if(!depth.has(id))depth.set(id,max+1);const layers=new Map();for(const id of ids){const d=depth.get(id);if(!layers.has(d))layers.set(d,[]);layers.get(d).push(id)}for(const [d,list] of layers){list.sort((a,b)=>String(nodeMap.get(a)?.type).localeCompare(String(nodeMap.get(b)?.type))||a.localeCompare(b));list.forEach((id,i)=>{if(!positions.has(id))positions.set(id,{x:d*235,y:i*76})})}}
|
|
55
|
+
function anchor(id,right){const p=positions.get(id)||{x:0,y:0};return{x:p.x+(right?160:0),y:p.y+25}}
|
|
56
|
+
function curve(e){const a=anchor(e.source,true),b=anchor(e.target,false),bend=Math.max(38,Math.abs(b.x-a.x)*.42);return `M ${a.x} ${a.y} C ${a.x+bend} ${a.y}, ${b.x-bend} ${b.y}, ${b.x} ${b.y}`}
|
|
57
|
+
function show(value){details.innerHTML='';const pre=document.createElement('pre');pre.textContent=JSON.stringify(value,null,2);details.appendChild(pre)}
|
|
58
|
+
function render(){layout();edgeLayer.replaceChildren();labelLayer.replaceChildren();nodeLayer.replaceChildren();for(const e of graph.edges){if(!positions.has(e.source)||!positions.has(e.target))continue;const visible=document.createElementNS('http://www.w3.org/2000/svg','path');visible.setAttribute('d',curve(e));visible.classList.add('edge');if(e.causal===true)visible.classList.add('causal');else if(e.causal===false)visible.classList.add('noncausal');visible.dataset.source=e.source;visible.dataset.target=e.target;visible.dataset.relation=String(e.relation||'').toLowerCase();edgeLayer.appendChild(visible);const hit=document.createElementNS('http://www.w3.org/2000/svg','path');hit.setAttribute('d',curve(e));hit.classList.add('edge-hit');hit.onclick=ev=>{ev.stopPropagation();show(e)};edgeLayer.appendChild(hit);const a=anchor(e.source,true),b=anchor(e.target,false),label=document.createElementNS('http://www.w3.org/2000/svg','text');label.setAttribute('x',(a.x+b.x)/2);label.setAttribute('y',(a.y+b.y)/2-6);label.setAttribute('text-anchor','middle');label.classList.add('label');label.textContent=e.count>1?`${e.relation} ×${e.count}`:e.relation;labelLayer.appendChild(label)}for(const n of graph.nodes){const p=positions.get(n.id);if(!p)continue;const group=document.createElementNS('http://www.w3.org/2000/svg','g');group.classList.add('node');group.dataset.id=n.id;group.dataset.search=`${n.id} ${n.name||''} ${n.type||''}`.toLowerCase();group.setAttribute('transform',`translate(${p.x} ${p.y})`);const rect=document.createElementNS('http://www.w3.org/2000/svg','rect');rect.setAttribute('width',160);rect.setAttribute('height',50);rect.setAttribute('fill',color(n.type));group.appendChild(rect);const type=document.createElementNS('http://www.w3.org/2000/svg','text');type.setAttribute('x',10);type.setAttribute('y',15);type.classList.add('type');type.textContent=n.type||'unknown';group.appendChild(type);const label=document.createElementNS('http://www.w3.org/2000/svg','text');label.setAttribute('x',10);label.setAttribute('y',34);label.textContent=short(n);group.appendChild(label);group.onclick=ev=>{ev.stopPropagation();show(n)};nodeLayer.appendChild(group)}applySearch()}
|
|
59
|
+
function applyTransform(){viewport.setAttribute('transform',`translate(${transform.x} ${transform.y}) scale(${transform.scale})`)}
|
|
60
|
+
function fit(){if(!positions.size)return;const xs=[...positions.values()].map(p=>p.x),ys=[...positions.values()].map(p=>p.y),minX=Math.min(...xs),maxX=Math.max(...xs)+160,minY=Math.min(...ys),maxY=Math.max(...ys)+50,box=svg.getBoundingClientRect(),w=Math.max(1,maxX-minX),h=Math.max(1,maxY-minY),scale=Math.min(1.2,Math.max(.07,Math.min((box.width-60)/w,(box.height-60)/h)));transform={x:30-minX*scale,y:30-minY*scale,scale};applyTransform()}
|
|
61
|
+
function applySearch(){const q=search.value.trim().toLowerCase();const matched=new Set();document.querySelectorAll('.node').forEach(el=>{const ok=!q||el.dataset.search.includes(q);el.classList.toggle('dim',!ok);if(ok)matched.add(el.dataset.id)});document.querySelectorAll('.edge').forEach(el=>{const keep=!q||matched.has(el.dataset.source)||matched.has(el.dataset.target)||el.dataset.relation.includes(q);el.classList.toggle('dim',!keep)})}
|
|
62
|
+
async function poll(){try{const response=await fetch('/graph.json',{cache:'no-store'});if(!response.ok)throw new Error(String(response.status));const data=await response.json(),finished=!!data.live_finished;delete data.live_finished;const signature=`${data.node_count||0}:${data.edge_count||0}:${data.event_count||0}`;graph=data;stats.textContent=`${data.node_count||0} nodes · ${data.edge_count||0} edges · ${data.event_count||0} events`;status.textContent=finished?'FINISHED':'LIVE';if(signature!==lastSignature){lastSignature=signature;render();if(positions.size<4)fit()}if(finished){setTimeout(()=>{location.href='/final'},250);return}}catch(_){status.textContent='RECONNECTING'}setTimeout(poll,500)}
|
|
63
|
+
svg.onpointerdown=e=>{if(e.target.closest?.('.node'))return;pan={x:e.clientX,y:e.clientY,tx:transform.x,ty:transform.y};svg.classList.add('panning');svg.setPointerCapture(e.pointerId)};svg.onpointermove=e=>{if(!pan)return;transform.x=pan.tx+e.clientX-pan.x;transform.y=pan.ty+e.clientY-pan.y;applyTransform()};svg.onpointerup=e=>{pan=null;svg.classList.remove('panning');try{svg.releasePointerCapture(e.pointerId)}catch(_){}};svg.addEventListener('wheel',e=>{e.preventDefault();const rect=svg.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,old=transform.scale,next=Math.min(4,Math.max(.07,old*Math.exp(-e.deltaY*.0012))),gx=(mx-transform.x)/old,gy=(my-transform.y)/old;transform.scale=next;transform.x=mx-gx*next;transform.y=my-gy*next;applyTransform()},{passive:false});search.oninput=applySearch;document.getElementById('fit').onclick=fit;window.onresize=fit;applyTransform();poll();
|
|
64
|
+
})();
|
|
65
|
+
</script>
|
|
66
|
+
</body>
|
|
67
|
+
</html>"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class LiveResult:
|
|
72
|
+
session_id: str
|
|
73
|
+
return_code: int
|
|
74
|
+
live_url: str
|
|
75
|
+
output_dir: Path
|
|
76
|
+
event_stream: Path
|
|
77
|
+
graph: Path
|
|
78
|
+
viewer: Path
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, object]:
|
|
81
|
+
return {
|
|
82
|
+
"session_id": self.session_id,
|
|
83
|
+
"return_code": self.return_code,
|
|
84
|
+
"live_url": self.live_url,
|
|
85
|
+
"output_dir": str(self.output_dir),
|
|
86
|
+
"event_stream": str(self.event_stream),
|
|
87
|
+
"graph": str(self.graph),
|
|
88
|
+
"viewer": str(self.viewer),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _LiveState:
|
|
93
|
+
def __init__(self, session_id: str, event_path: Path) -> None:
|
|
94
|
+
self.session_id = session_id
|
|
95
|
+
self.event_path = event_path
|
|
96
|
+
self._lock = threading.Lock()
|
|
97
|
+
self._last_graph: dict[str, object] = self._empty_graph()
|
|
98
|
+
self._finished = False
|
|
99
|
+
self._final_html: str | None = None
|
|
100
|
+
|
|
101
|
+
def _empty_graph(self) -> dict[str, object]:
|
|
102
|
+
return {
|
|
103
|
+
"graph_schema_version": "0.1",
|
|
104
|
+
"session_id": self.session_id,
|
|
105
|
+
"event_count": 0,
|
|
106
|
+
"node_count": 0,
|
|
107
|
+
"edge_count": 0,
|
|
108
|
+
"nodes": [],
|
|
109
|
+
"edges": [],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def snapshot(self) -> dict[str, object]:
|
|
113
|
+
if self.event_path.exists() and self.event_path.stat().st_size > 0:
|
|
114
|
+
try:
|
|
115
|
+
graph = build_execution_graph(self.event_path, allow_incomplete=True).to_dict()
|
|
116
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
117
|
+
graph = None
|
|
118
|
+
if graph is not None:
|
|
119
|
+
with self._lock:
|
|
120
|
+
self._last_graph = graph
|
|
121
|
+
with self._lock:
|
|
122
|
+
payload = dict(self._last_graph)
|
|
123
|
+
payload["live_finished"] = self._finished
|
|
124
|
+
return payload
|
|
125
|
+
|
|
126
|
+
def finish(self, graph: dict[str, object]) -> None:
|
|
127
|
+
with self._lock:
|
|
128
|
+
self._last_graph = graph
|
|
129
|
+
self._final_html = render_graph_html(graph)
|
|
130
|
+
self._finished = True
|
|
131
|
+
|
|
132
|
+
def final_html(self) -> str | None:
|
|
133
|
+
with self._lock:
|
|
134
|
+
return self._final_html
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _handler_factory(state: _LiveState):
|
|
138
|
+
class Handler(BaseHTTPRequestHandler):
|
|
139
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
def _send(self, body: bytes, content_type: str, status: int = 200) -> None:
|
|
143
|
+
self.send_response(status)
|
|
144
|
+
self.send_header("Content-Type", content_type)
|
|
145
|
+
self.send_header("Content-Length", str(len(body)))
|
|
146
|
+
self.send_header("Cache-Control", "no-store")
|
|
147
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
148
|
+
self.end_headers()
|
|
149
|
+
self.wfile.write(body)
|
|
150
|
+
|
|
151
|
+
def do_GET(self) -> None:
|
|
152
|
+
path = self.path.split("?", 1)[0]
|
|
153
|
+
if path == "/":
|
|
154
|
+
self._send(_LIVE_HTML.encode("utf-8"), "text/html; charset=utf-8")
|
|
155
|
+
return
|
|
156
|
+
if path == "/graph.json":
|
|
157
|
+
payload = json.dumps(
|
|
158
|
+
state.snapshot(), ensure_ascii=False, separators=(",", ":")
|
|
159
|
+
).encode("utf-8")
|
|
160
|
+
self._send(payload, "application/json; charset=utf-8")
|
|
161
|
+
return
|
|
162
|
+
if path == "/final":
|
|
163
|
+
final = state.final_html()
|
|
164
|
+
if final is None:
|
|
165
|
+
self._send(b"Final graph is not ready", "text/plain; charset=utf-8", 404)
|
|
166
|
+
else:
|
|
167
|
+
self._send(final.encode("utf-8"), "text/html; charset=utf-8")
|
|
168
|
+
return
|
|
169
|
+
self._send(b"Not found", "text/plain; charset=utf-8", 404)
|
|
170
|
+
|
|
171
|
+
return Handler
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _LocalThreadingHTTPServer(ThreadingHTTPServer):
|
|
175
|
+
"""Local HTTP server that skips HTTPServer's reverse-DNS lookup during bind."""
|
|
176
|
+
|
|
177
|
+
def server_bind(self) -> None:
|
|
178
|
+
TCPServer.server_bind(self)
|
|
179
|
+
host, port = self.server_address[:2]
|
|
180
|
+
self.server_name = str(host)
|
|
181
|
+
self.server_port = int(port)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_live(
|
|
185
|
+
command: list[str],
|
|
186
|
+
*,
|
|
187
|
+
watch_root: str | Path,
|
|
188
|
+
output_dir: str | Path | None = None,
|
|
189
|
+
poll_interval: float = 0.10,
|
|
190
|
+
collect_filesystem: bool = True,
|
|
191
|
+
collect_network: bool = True,
|
|
192
|
+
port: int = 0,
|
|
193
|
+
open_browser: bool = True,
|
|
194
|
+
linger_seconds: float = 2.0,
|
|
195
|
+
announce: Callable[[str], None] | None = None,
|
|
196
|
+
) -> LiveResult:
|
|
197
|
+
"""Run a command with the portable collector and expose a localhost live graph."""
|
|
198
|
+
if not command:
|
|
199
|
+
raise ValueError("command must not be empty")
|
|
200
|
+
if port < 0 or port > 65535:
|
|
201
|
+
raise ValueError("port must be between 0 and 65535")
|
|
202
|
+
if linger_seconds < 0:
|
|
203
|
+
raise ValueError("linger_seconds must be >= 0")
|
|
204
|
+
|
|
205
|
+
session_id = uuid4().hex
|
|
206
|
+
root = Path(watch_root).expanduser().resolve()
|
|
207
|
+
run_dir = (
|
|
208
|
+
Path(output_dir).expanduser().resolve()
|
|
209
|
+
if output_dir is not None
|
|
210
|
+
else root / ".execweave" / "runs" / session_id
|
|
211
|
+
)
|
|
212
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
event_path = run_dir / "events.jsonl"
|
|
214
|
+
graph_path = run_dir / "graph.json"
|
|
215
|
+
viewer_path = run_dir / "viewer.html"
|
|
216
|
+
for artifact in (event_path, graph_path, viewer_path):
|
|
217
|
+
if artifact.exists() and artifact.stat().st_size > 0:
|
|
218
|
+
raise FileExistsError(f"ExecWeave live artifact already exists: {artifact}")
|
|
219
|
+
|
|
220
|
+
sink = JsonlSink(event_path)
|
|
221
|
+
collector = create_collector(
|
|
222
|
+
backend="portable",
|
|
223
|
+
session_id=session_id,
|
|
224
|
+
sink=sink,
|
|
225
|
+
watch_root=root,
|
|
226
|
+
poll_interval=poll_interval,
|
|
227
|
+
collect_filesystem=collect_filesystem,
|
|
228
|
+
collect_network=collect_network,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
state = _LiveState(session_id, event_path)
|
|
232
|
+
server = _LocalThreadingHTTPServer(("127.0.0.1", port), _handler_factory(state))
|
|
233
|
+
server.daemon_threads = True
|
|
234
|
+
server_thread = threading.Thread(
|
|
235
|
+
target=server.serve_forever,
|
|
236
|
+
name="execweave-live",
|
|
237
|
+
daemon=True,
|
|
238
|
+
)
|
|
239
|
+
server_thread.start()
|
|
240
|
+
host, selected_port = server.server_address[:2]
|
|
241
|
+
live_url = f"http://{host}:{selected_port}/"
|
|
242
|
+
if announce is not None:
|
|
243
|
+
announce(live_url)
|
|
244
|
+
if open_browser:
|
|
245
|
+
webbrowser.open(live_url)
|
|
246
|
+
|
|
247
|
+
return_code = 1
|
|
248
|
+
try:
|
|
249
|
+
return_code = collector.run(command)
|
|
250
|
+
validation = validate_event_stream(event_path)
|
|
251
|
+
if not validation.valid:
|
|
252
|
+
details = "; ".join(validation.errors)
|
|
253
|
+
raise RuntimeError(f"live event stream failed validation: {details}")
|
|
254
|
+
|
|
255
|
+
execution_graph = build_execution_graph(event_path)
|
|
256
|
+
graph_payload = execution_graph.to_dict()
|
|
257
|
+
write_execution_graph(execution_graph, graph_path)
|
|
258
|
+
write_graph_html(graph_payload, viewer_path, open_browser=False)
|
|
259
|
+
state.finish(graph_payload)
|
|
260
|
+
if linger_seconds:
|
|
261
|
+
time.sleep(linger_seconds)
|
|
262
|
+
|
|
263
|
+
return LiveResult(
|
|
264
|
+
session_id=session_id,
|
|
265
|
+
return_code=return_code,
|
|
266
|
+
live_url=live_url,
|
|
267
|
+
output_dir=run_dir,
|
|
268
|
+
event_stream=event_path,
|
|
269
|
+
graph=graph_path,
|
|
270
|
+
viewer=viewer_path,
|
|
271
|
+
)
|
|
272
|
+
finally:
|
|
273
|
+
server.shutdown()
|
|
274
|
+
server.server_close()
|
|
275
|
+
server_thread.join(timeout=5)
|