Fix window discovery for Electron/forking apps like Nextcloud
xdotool search --pid only matches the exact launched PID, but Electron apps fork into child processes that own the actual window. Now tries: direct PID → descendant PIDs via /proc → window name match. Also increases retry timeout to 30s for heavy apps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9fee17af17
commit
da8f0326f8
1 changed files with 73 additions and 9 deletions
|
|
@ -155,14 +155,14 @@ class ManagedGuiApp:
|
||||||
|
|
||||||
async def _capture_loop(self):
|
async def _capture_loop(self):
|
||||||
"""Background: capture window pixmap → JPEG → fan-out."""
|
"""Background: capture window pixmap → JPEG → fan-out."""
|
||||||
# Wait for window to appear
|
# Wait for window to appear (up to ~30s for heavy apps like Electron)
|
||||||
for attempt in range(20):
|
for attempt in range(30):
|
||||||
if not self.alive:
|
if not self.alive:
|
||||||
return
|
return
|
||||||
await self._discover_window()
|
await self._discover_window()
|
||||||
if self.window_id:
|
if self.window_id:
|
||||||
break
|
break
|
||||||
await asyncio.sleep(0.5 * (1 + attempt * 0.2))
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
if not self.window_id:
|
if not self.window_id:
|
||||||
log.warning("No window found for app %s (%s)", self.app_id, self.command)
|
log.warning("No window found for app %s (%s)", self.app_id, self.command)
|
||||||
|
|
@ -269,24 +269,88 @@ class ManagedGuiApp:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _discover_window(self):
|
async def _discover_window(self):
|
||||||
"""Find X11 window ID for this app's process."""
|
"""Find X11 window ID for this app's process.
|
||||||
|
|
||||||
|
Tries multiple strategies:
|
||||||
|
1. Direct PID match (works for simple apps)
|
||||||
|
2. Child PID match (walks /proc for descendants — Electron apps fork)
|
||||||
|
3. Any visible window on the display (last resort)
|
||||||
|
"""
|
||||||
if not XDOTOOL_BIN or not self.process:
|
if not XDOTOOL_BIN or not self.process:
|
||||||
return
|
return
|
||||||
|
env = self._display_env()
|
||||||
|
|
||||||
|
# Strategy 1: direct PID match
|
||||||
|
wid = await self._xdotool_search("--pid", str(self.process.pid), env=env)
|
||||||
|
if wid:
|
||||||
|
self.window_id = wid
|
||||||
|
log.debug("Window %d found by PID %d for %s", wid, self.process.pid, self.app_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Strategy 2: search child PIDs (Electron/forking apps)
|
||||||
|
try:
|
||||||
|
child_pids = self._get_descendant_pids(self.process.pid)
|
||||||
|
for cpid in child_pids:
|
||||||
|
wid = await self._xdotool_search("--pid", str(cpid), env=env)
|
||||||
|
if wid:
|
||||||
|
self.window_id = wid
|
||||||
|
log.debug("Window %d found by child PID %d for %s", wid, cpid, self.app_id)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Strategy 3: search by window name containing the app command/title
|
||||||
|
for query in (self.title, self.command):
|
||||||
|
if query:
|
||||||
|
wid = await self._xdotool_search("--name", query, env=env)
|
||||||
|
if wid:
|
||||||
|
self.window_id = wid
|
||||||
|
log.debug("Window %d found by name '%s' for %s", wid, query, self.app_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_descendant_pids(parent_pid: int) -> list[int]:
|
||||||
|
"""Walk /proc to find all descendant PIDs of a process."""
|
||||||
|
children = []
|
||||||
|
try:
|
||||||
|
for entry in os.listdir("/proc"):
|
||||||
|
if not entry.isdigit():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with open(f"/proc/{entry}/stat") as f:
|
||||||
|
stat = f.read()
|
||||||
|
# Field 4 is PPID
|
||||||
|
parts = stat.split()
|
||||||
|
ppid = int(parts[3])
|
||||||
|
pid = int(entry)
|
||||||
|
if ppid == parent_pid:
|
||||||
|
children.append(pid)
|
||||||
|
except (OSError, IndexError, ValueError):
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
# Recurse into children
|
||||||
|
grandchildren = []
|
||||||
|
for cpid in children:
|
||||||
|
grandchildren.extend(ManagedGuiApp._get_descendant_pids(cpid))
|
||||||
|
return children + grandchildren
|
||||||
|
|
||||||
|
async def _xdotool_search(self, *args, env=None) -> Optional[int]:
|
||||||
|
"""Run xdotool search and return first window ID, or None."""
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
XDOTOOL_BIN, "search", "--pid", str(self.process.pid),
|
XDOTOOL_BIN, "search", *args,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
env=self._display_env(),
|
env=env,
|
||||||
)
|
)
|
||||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
|
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
|
||||||
lines = stdout.decode().strip().splitlines()
|
lines = stdout.decode().strip().splitlines()
|
||||||
if lines:
|
if lines:
|
||||||
self.window_id = int(lines[0])
|
return int(lines[0])
|
||||||
log.debug("Discovered window %d for app %s (pid %d)",
|
|
||||||
self.window_id, self.app_id, self.process.pid)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
# ---- Input forwarding ----
|
# ---- Input forwarding ----
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue