Last active
October 19, 2020 22:13
-
-
Save toofar/33cd9baf420f327472f7eb7d7b2e3f6a to your computer and use it in GitHub Desktop.
Switch to qutebrowser tab for renderrer by PID.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# pause all the `QtWebEngineProc`s except one, inject JS into each of the tabs | |
# to see which one returns something. | |
# :activate-pid <pid> | |
import os, signal, functools | |
from PyQt5.QtCore import QTimer | |
from qutebrowser.misc import objects | |
from qutebrowser.utils import objreg | |
from qutebrowser.api import cmdutils, message | |
# Eg /usr/lib/x86_64-linux-gnu/qt5/libexec/QtWebEngineProcess | |
# Will be looked up from /proc/<pid>/cmdline if not set. | |
WEBENGINE_BINARY=None | |
def get_webengines(): | |
global WEBENGINE_BINARY | |
webengines = [] | |
for dirname in os.listdir('/proc'): | |
try: | |
dirname = int(dirname) | |
except ValueError: | |
continue | |
# Prefer matching on binary in case I get around to figuring out how to | |
# change process names. | |
if WEBENGINE_BINARY is not None: | |
try: | |
if os.path.realpath(f"/proc/{dirname}/exe") == WEBENGINE_BINARY: | |
webengines.append(dirname) | |
except OSError: | |
continue | |
else: | |
try: | |
with open(f"/proc/{dirname}/cmdline") as f: | |
cmd = f.read().split() | |
if not cmd or not cmd[0].endswith("QtWebEngineProcess"): | |
continue | |
WEBENGINE_BINARY = cmd[0] | |
webengines.append(dirname) | |
except OSError: | |
continue | |
return webengines | |
def find_tab_in_webengines(index, webengines, delay, callback, results=None): | |
"""Find which tabs map to the PID at webengines[index]. | |
For each PID in `webengines`: | |
* send all the other PIDs SIGSTOP | |
* call `run_js_async` for each of the tabs in this browser instance | |
* see which ones respond (can be multiple depending on process model) | |
* send SIGCONT to the stopped processes | |
Since we are using an async method of pinging renderers we need to | |
do the "see which ones respond" step asyncronously. Putting that | |
method on the queue with no delay seems to be to fast, 5ms works on | |
my maching but slower or more heavily loaded ones might need a | |
higher delay. | |
`callback` receives a dict with PIDs as keys and lists of tabs as | |
values. Don't save the tabs anywhere permanent. | |
`results` is to aggregate findings across multiple runs and is | |
for internal use. | |
""" | |
if not results: | |
results = {} | |
if index == len(webengines): | |
callback(results) | |
return | |
active_tabs = [] | |
for pid in webengines: | |
if pid == webengines[index]: | |
continue | |
os.kill(pid, signal.SIGSTOP) | |
running = True | |
def cb(tab, result): | |
if not running: | |
return | |
active_tabs.append(tab) | |
window_ids = list(objreg.window_registry) | |
for wid in window_ids: | |
win = objreg.get('tabbed-browser', scope='window', | |
window=wid) | |
for tid, tab in enumerate(win.widgets()): | |
tab.run_js_async( | |
str(tid), | |
functools.partial(cb, tab), | |
world=1 | |
) | |
def report(results): | |
nonlocal running | |
running = False | |
for pid in webengines: | |
if pid == webengines[index]: | |
continue | |
os.kill(pid, signal.SIGCONT) | |
if not active_tabs: | |
# presumably the webengine we left running belonged to a | |
# separate instance | |
pass | |
else: | |
results[webengines[index]] = active_tabs | |
QTimer.singleShot( | |
5, | |
functools.partial( | |
find_tab_in_webengines, | |
index+1, | |
webengines, | |
delay, | |
callback, | |
results, | |
) | |
) | |
QTimer.singleShot(delay, functools.partial(report, results)) | |
def activate_tab(found): | |
if len(found) != 1: | |
message.info(f"Got {len(found)} matching tabs.") | |
tab = list(found.values())[0][0] | |
tabbed_browser = objreg.get('tabbed-browser', scope='window', | |
window=tab.win_id) | |
win = tabbed_browser.widget.window() | |
win.activateWindow() | |
win.raise_() | |
tabbed_browser.widget.setCurrentWidget(tab) | |
if 'activate-pid' in objects.commands: | |
del objects.commands['activate-pid'] | |
@cmdutils.register() | |
def activate_pid(pid: int, delay: int = 5): | |
""" | |
Switch to tab for QtWebEngine processes `pid`. | |
Args: | |
pid: The PID of the process for the tab to switch to. | |
delay: The delay to wait for the renderers to respond. | |
""" | |
webengines = get_webengines() | |
if pid not in webengines: | |
message.error(f"No QtWebEngineProc found with pid {pid}") | |
return | |
# Since find_tab_in_webengines goes through the whole list move the one we | |
# are interested in to the end so it only looks at that one. | |
webengines = [x for x in webengines if x != pid] | |
webengines.append(pid) | |
find_tab_in_webengines(len(webengines)-1, webengines, delay, activate_tab) | |
def retitle_processes(found): | |
for pid, tabs in found.items(): | |
# TODO: How to overwrite argv[0] when not in main()? | |
# https://unix.stackexchange.com/a/404180 | |
# "Cannot access memory at address 0x1242" ? | |
# Could also take a PID and switch to that tab | |
for tab in tabs: | |
message.info(f"pid {pid} is tab {tab}") | |
if 'title-to-processes' in objects.commands: | |
del objects.commands['title-to-processes'] | |
@cmdutils.register() | |
def title_to_processes(delay: int = 5): | |
""" | |
Rename QtWebEngine processes for this instance to the tab titles. | |
Args: | |
delay: The delay to wait for the renderers to respond. | |
""" | |
webengines = get_webengines() | |
find_tab_in_webengines(0, webengines, delay, retitle_processes) |
With this extension :activate-pid 1234
should switch to the tab backed by the renderer with PID 1234. I've found that sometimes I have to bump the delay up to 10 to get it to find a match. Actually even that isn't long enough with my current number of tabs (133 lol). With different process models like process-per-site it just goes to one. It probably wouldn't be that hard to make this generate some HTML say what tabs map to what PIDs for that case.
I haven't looked into changing the process title any more. gdb was being mean to me. I just use the not-properly-implemented :title-to-processes
to print the stuff out to the console sometimes because I run with --debug.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm interested in working the other way - given a PID list, find out which tab maps to which PID (so I can see which tabs are hogging my memory/CPU). This code looks like a great starting point... did you get any further with it, in particular the naming of processes? I was wondering about a call that would set something in
/proc/<pid>/...
that can get picked up by scripts etc but that looks harder than I thought.