Skip to content

Instantly share code, notes, and snippets.

@toofar
Last active October 19, 2020 22:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toofar/33cd9baf420f327472f7eb7d7b2e3f6a to your computer and use it in GitHub Desktop.
Save toofar/33cd9baf420f327472f7eb7d7b2e3f6a to your computer and use it in GitHub Desktop.
Switch to qutebrowser tab for renderrer by PID.
# 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)
@jtyers
Copy link

jtyers commented Aug 15, 2019

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.

@toofar
Copy link
Author

toofar commented Aug 17, 2019

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