Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
113 lines
6.6 KiB
Python
113 lines
6.6 KiB
Python
"""Start an isolated window manager and a loopback-to-Unix extension proxy before Blender."""
|
|
import ctypes as C
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import select
|
|
import socket
|
|
import socketserver
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
import xml.etree.ElementTree as ET
|
|
|
|
workspace=Path(sys.argv[1]);command=sys.argv[2:]
|
|
runtime=Path(__file__).resolve().parents[2]/'build_blender_native'/'desktop-runtime'
|
|
binary=runtime/'usr/bin/openbox'
|
|
if not binary.is_file():raise RuntimeError('NATIVE_WINDOW_MANAGER_MISSING: run /usr/bin/python3 tools/web/prepare-native-desktop.py')
|
|
|
|
class Forward(socketserver.BaseRequestHandler):
|
|
def handle(self):
|
|
upstream=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
|
|
try:
|
|
upstream.settimeout(15);directory=os.open(workspace,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
|
|
try:upstream.connect(f'/proc/self/fd/{directory}/extension-proxy.sock')
|
|
finally:os.close(directory)
|
|
self.request.settimeout(15)
|
|
peers={self.request:upstream,upstream:self.request};deadline=time.monotonic()+240
|
|
while time.monotonic()<deadline:
|
|
ready,_,_=select.select(list(peers),[],[],60)
|
|
if not ready:return
|
|
for source in ready:
|
|
data=source.recv(65536)
|
|
if not data:return
|
|
peers[source].sendall(data)
|
|
except (OSError,TimeoutError):pass
|
|
finally:upstream.close()
|
|
|
|
class Proxy(socketserver.ThreadingTCPServer):
|
|
daemon_threads=True
|
|
allow_reuse_address=True
|
|
slots=threading.BoundedSemaphore(16)
|
|
def process_request(self,request,address):
|
|
if not self.slots.acquire(blocking=False):self.shutdown_request(request);return
|
|
try:super().process_request(request,address)
|
|
except BaseException:self.slots.release();raise
|
|
def process_request_thread(self,request,address):
|
|
try:super().process_request_thread(request,address)
|
|
finally:self.slots.release()
|
|
proxy=Proxy(('127.0.0.1',0),Forward);threading.Thread(target=proxy.serve_forever,daemon=True).start()
|
|
environment=dict(os.environ)
|
|
address='http://127.0.0.1:'+str(proxy.server_address[1])
|
|
for name in ('http_proxy','https_proxy','HTTP_PROXY','HTTPS_PROXY'):environment[name]=address
|
|
environment['NO_PROXY']=environment['no_proxy']='localhost,127.0.0.1,::1'
|
|
environment['BLENDER_WEB_INPUT']='1'
|
|
environment.setdefault('SSL_CERT_FILE','/etc/ssl/certs/ca-certificates.crt')
|
|
environment.setdefault('REQUESTS_CA_BUNDLE',environment['SSL_CERT_FILE'])
|
|
|
|
namespace='http://openbox.org/3.4/rc';ET.register_namespace('',namespace)
|
|
config=ET.Element('{'+namespace+'}openbox_config')
|
|
def element(parent,tag,text=None,**attributes):
|
|
result=ET.SubElement(parent,'{'+namespace+'}'+tag,attributes)
|
|
if text is not None:result.text=text
|
|
return result
|
|
theme=element(config,'theme');element(theme,'name','Clearlooks');element(theme,'titleLayout','NLIMC')
|
|
focus=element(config,'focus');element(focus,'followMouse','no');element(focus,'focusNew','yes')
|
|
keyboard=element(config,'keyboard')
|
|
for key,action in [('A-F4','Close'),('A-Tab','NextWindow'),('A-S-Tab','PreviousWindow')]:element(element(keyboard,'keybind',key=key),'action',name=action)
|
|
mouse=element(config,'mouse');element(mouse,'doubleClickTime','300');contexts={};bindings={}
|
|
actions=[('Titlebar','Left','Drag','Move'),('Titlebar','Left','DoubleClick','ToggleMaximize'),('Titlebar','Left','Press','Focus'),('Titlebar','Left','Press','Raise'),('Close','Left','Click','Close'),('Maximize','Left','Click','ToggleMaximize'),('Iconify','Left','Click','Iconify'),('Frame','W-Left','Drag','Move'),('Frame','W-Right','Drag','Resize'),('Client','Left','Press','Focus'),('Client','Left','Press','Raise')]
|
|
actions.extend((edge,'Left','Drag','Resize') for edge in ('Top','TopLeft','TopRight','Left','Right','Bottom','BottomLeft','BottomRight'))
|
|
for context,button,event,action in actions:
|
|
if context not in contexts:contexts[context]=element(mouse,'context',name=context)
|
|
key=(context,button,event)
|
|
if key not in bindings:bindings[key]=element(contexts[context],'mousebind',button=button,action=event)
|
|
element(bindings[key],'action',name=action)
|
|
applications=element(config,'applications');application=element(applications,'application',type='normal',**{'class':'Blender'})
|
|
element(application,'maximized','yes')
|
|
config_file=workspace/'openbox.xml';ET.ElementTree(config).write(config_file,encoding='utf-8',xml_declaration=True)
|
|
wm_environment=dict(environment,LD_LIBRARY_PATH=str(runtime/'usr/lib/x86_64-linux-gnu'),XDG_DATA_DIRS=str(runtime/'usr/share')+':/usr/local/share:/usr/share')
|
|
wm=subprocess.Popen([str(binary),'--sm-disable','--config-file',str(config_file)],env=wm_environment)
|
|
blender=None
|
|
try:
|
|
# The EWMH owner is the initialization boundary; no Blender event is sent before it exists.
|
|
x=C.CDLL('libX11.so.6');x.XOpenDisplay.argtypes=[C.c_char_p];x.XOpenDisplay.restype=C.c_void_p
|
|
x.XDefaultRootWindow.argtypes=[C.c_void_p];x.XDefaultRootWindow.restype=C.c_ulong
|
|
x.XInternAtom.argtypes=[C.c_void_p,C.c_char_p,C.c_int];x.XInternAtom.restype=C.c_ulong
|
|
x.XGetWindowProperty.argtypes=[C.c_void_p,C.c_ulong,C.c_ulong,C.c_long,C.c_long,C.c_int,C.c_ulong,C.POINTER(C.c_ulong),C.POINTER(C.c_int),C.POINTER(C.c_ulong),C.POINTER(C.c_ulong),C.POINTER(C.c_void_p)]
|
|
x.XFree.argtypes=[C.c_void_p];x.XCloseDisplay.argtypes=[C.c_void_p]
|
|
display=x.XOpenDisplay(None)
|
|
if not display:raise RuntimeError('NATIVE_WM_DISPLAY_MISSING')
|
|
try:
|
|
root=x.XDefaultRootWindow(display);atom=x.XInternAtom(display,b'_NET_SUPPORTING_WM_CHECK',False)
|
|
deadline=time.monotonic()+10
|
|
while True:
|
|
if wm.poll() is not None:raise RuntimeError('NATIVE_WINDOW_MANAGER_EXITED')
|
|
actual=C.c_ulong();form=C.c_int();count=C.c_ulong();remaining=C.c_ulong();data=C.c_void_p()
|
|
x.XGetWindowProperty(display,root,atom,0,1,False,0,C.byref(actual),C.byref(form),C.byref(count),C.byref(remaining),C.byref(data))
|
|
ready=count.value>0
|
|
if data.value:x.XFree(data)
|
|
if ready:break
|
|
if time.monotonic()>deadline:raise RuntimeError('NATIVE_WINDOW_MANAGER_START_TIMEOUT')
|
|
time.sleep(.025)
|
|
finally:x.XCloseDisplay(display)
|
|
(workspace/'desktop-host.json').write_text(json.dumps({'windowManager':'Openbox','network':'REPOSITORY_HTTPS_PROXY'}))
|
|
blender=subprocess.Popen(command,env=environment)
|
|
sys.exit(blender.wait())
|
|
finally:
|
|
if blender and blender.poll() is None:blender.terminate()
|
|
wm.terminate();proxy.shutdown();proxy.server_close()
|
|
try:wm.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:wm.kill();wm.wait()
|