Files
workinf_Blender_Wasm/tools/web/native-desktop-host.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

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()