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):
|
||||
"""Background: capture window pixmap → JPEG → fan-out."""
|
||||
# Wait for window to appear
|
||||
for attempt in range(20):
|
||||
# Wait for window to appear (up to ~30s for heavy apps like Electron)
|
||||
for attempt in range(30):
|
||||
if not self.alive:
|
||||
return
|
||||
await self._discover_window()
|
||||
if self.window_id:
|
||||
break
|
||||
await asyncio.sleep(0.5 * (1 + attempt * 0.2))
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
if not self.window_id:
|
||||
log.warning("No window found for app %s (%s)", self.app_id, self.command)
|
||||
|
|
@ -269,24 +269,88 @@ class ManagedGuiApp:
|
|||
return None
|
||||
|
||||
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:
|
||||
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:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
XDOTOOL_BIN, "search", "--pid", str(self.process.pid),
|
||||
XDOTOOL_BIN, "search", *args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
env=self._display_env(),
|
||||
env=env,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
|
||||
lines = stdout.decode().strip().splitlines()
|
||||
if lines:
|
||||
self.window_id = int(lines[0])
|
||||
log.debug("Discovered window %d for app %s (pid %d)",
|
||||
self.window_id, self.app_id, self.process.pid)
|
||||
return int(lines[0])
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
# ---- Input forwarding ----
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue