Skip to content

Commit 44a8f89

Browse files
committed
Move desktop notifier to subprocess
1 parent 961044f commit 44a8f89

File tree

7 files changed

+211
-51
lines changed

7 files changed

+211
-51
lines changed

common/structs.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,23 @@ def __init__(self, proc: asyncio.subprocess.Process):
9595
self.daemon = DaemonProcess(proc)
9696

9797
async def get_async(self):
98+
assert self.proc.stdout
9899
while self.proc.returncode is None and not self.proc.stdout.at_eof():
99100
try:
100-
return json.loads(await self.proc.stdout.readline())
101+
line = await self.proc.stdout.readline()
102+
return json.loads(line)
101103
except json.JSONDecodeError:
102104
pass
103105
await asyncio.sleep(0)
104106
raise self.DaemonPipeExit()
105107

108+
def put(self, data: dict | list | str):
109+
assert self.proc.stdin
110+
if self.proc.returncode is None:
111+
self.proc.stdin.write(json.dumps(data).encode() + b"\n")
112+
return
113+
raise self.DaemonPipeExit()
114+
106115
def __enter__(self):
107116
self.daemon.__enter__()
108117
return self
@@ -112,6 +121,9 @@ def __exit__(self, exc_type, exc_val, exc_tb):
112121
if exc_type is self.DaemonPipeExit:
113122
return True
114123

124+
def kill(self):
125+
self.daemon.__exit__()
126+
115127

116128
class Timestamp:
117129
instances = weakref.WeakSet()

main.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def main():
5353
from modules import gui
5454
globals.gui = gui.MainGUI()
5555

56-
from modules import rpc_thread
57-
with rpc_thread.setup():
56+
from modules import notification_proc, rpc_thread
57+
with notification_proc.setup(), rpc_thread.setup():
5858

5959
globals.gui.main_loop()
6060

@@ -80,20 +80,31 @@ def lock_singleton():
8080
pass
8181

8282

83+
def get_subprocess_args(subprocess_type: str):
84+
import json
85+
i = sys.argv.index(subprocess_type)
86+
args = json.loads(sys.argv[i + 1])
87+
kwargs = json.loads(sys.argv[i + 2])
88+
return args, kwargs
89+
90+
8391
if __name__ == "__main__":
8492
if "-c" in sys.argv:
8593
# Mimic python's -c flag to evaluate code
8694
exec(sys.argv[sys.argv.index("-c") + 1])
8795

88-
elif "webview" in sys.argv:
96+
elif "webview-daemon" in sys.argv:
8997
# Run webviews as subprocesses since Qt doesn't like threading
90-
import json
98+
args, kwargs = get_subprocess_args("webview-daemon")
9199
from modules import webview
92-
i = sys.argv.index("webview")
93-
cb = getattr(webview, sys.argv[i + 1])
94-
args = json.loads(sys.argv[i + 2])
95-
kwargs = json.loads(sys.argv[i + 3])
96-
cb(*args, **kwargs)
100+
webview_action = getattr(webview, args.pop(0))
101+
webview_action(*args, **kwargs)
102+
103+
elif "notification-daemon" in sys.argv:
104+
# Run notifications as subprocesses since desktop-notifier doesn't like threading
105+
args, kwargs = get_subprocess_args("notification-daemon")
106+
from modules import notification_proc
107+
notification_proc.daemon(*args, **kwargs)
97108

98109
else:
99110
try:

modules/api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
globals,
5252
icons,
5353
msgbox,
54+
notification_proc,
5455
utils,
5556
webview,
5657
)
@@ -997,7 +998,7 @@ def open_callback():
997998
MsgBox.info, buttons
998999
)
9991000
if globals.gui.hidden or not globals.gui.focused:
1000-
globals.gui.tray.notify(
1001+
notification_proc.notify(
10011002
title="Notifications",
10021003
msg=msg,
10031004
buttons=[
@@ -1235,7 +1236,7 @@ def update_callback_done(_):
12351236
bottom=True
12361237
)
12371238
if globals.gui.hidden or not globals.gui.focused:
1238-
globals.gui.tray.notify(
1239+
notification_proc.notify(
12391240
title="F95Checker update",
12401241
msg="F95Checker has received an update!",
12411242
)

modules/gui.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import pathlib
1010
import pickle
1111
import platform
12-
import shutil
1312
import sys
1413
import threading
1514
import time
@@ -22,7 +21,6 @@
2221
QtWidgets,
2322
)
2423
import aiohttp
25-
import desktop_notifier
2624
import glfw
2725
import imgui
2826
import OpenGL
@@ -5104,10 +5102,6 @@ def __init__(self, main_gui: MainGUI):
51045102
self.main_gui = main_gui
51055103
self.idle_icon = QtGui.QIcon(str(globals.self_path / 'resources/icons/logo.png'))
51065104
self.paused_icon = QtGui.QIcon(str(globals.self_path / 'resources/icons/paused.png'))
5107-
self._notify = desktop_notifier.DesktopNotifier(
5108-
app_name="F95Checker",
5109-
app_icon=desktop_notifier.Icon(globals.self_path / "resources/icons/icon.png"),
5110-
)
51115105
super().__init__(self.idle_icon)
51125106

51135107
self.watermark = QtGui.QAction(f"F95Checker {globals.version_name}")
@@ -5213,29 +5207,3 @@ def update_status(self, *_):
52135207
def activated_filter(self, reason: QtWidgets.QSystemTrayIcon.ActivationReason):
52145208
if reason in self.show_gui_events:
52155209
self.main_gui.show()
5216-
5217-
def notify(
5218-
self,
5219-
title: str,
5220-
msg: str,
5221-
urgency=desktop_notifier.Urgency.Normal,
5222-
icon: desktop_notifier.Icon = None,
5223-
buttons: list[desktop_notifier.Button] = [],
5224-
attachment: desktop_notifier.Attachment = None,
5225-
timeout=5,
5226-
):
5227-
async_thread.run(self._notify.send(
5228-
title=title,
5229-
message=msg,
5230-
urgency=urgency,
5231-
icon=icon,
5232-
buttons=buttons + [
5233-
desktop_notifier.Button(
5234-
title="View",
5235-
on_pressed=self.main_gui.show,
5236-
),
5237-
],
5238-
on_clicked=self.main_gui.show,
5239-
attachment=attachment,
5240-
timeout=timeout,
5241-
))

modules/notification_proc.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import asyncio
2+
import contextlib
3+
import functools
4+
import json
5+
import sys
6+
import typing
7+
8+
import desktop_notifier
9+
10+
pipe: "structs.DaemonPipe" = None
11+
server: asyncio.Future = None
12+
callbacks: dict[int, typing.Callable] = {}
13+
14+
15+
@contextlib.contextmanager
16+
def setup():
17+
start()
18+
try:
19+
yield
20+
finally:
21+
stop()
22+
23+
24+
def start():
25+
global pipe, server
26+
27+
import shlex
28+
import subprocess
29+
from common.structs import DaemonPipe
30+
from external import async_thread
31+
from modules import globals
32+
33+
args = []
34+
kwargs = dict(
35+
icon_uri=(globals.self_path / "resources/icons/icon.png").as_uri(),
36+
)
37+
38+
proc = async_thread.wait(asyncio.create_subprocess_exec(
39+
*shlex.split(globals.start_cmd),
40+
"notification-daemon",
41+
json.dumps(args),
42+
json.dumps(kwargs),
43+
stdin=subprocess.PIPE,
44+
stdout=subprocess.PIPE,
45+
))
46+
pipe = DaemonPipe(proc)
47+
48+
server = async_thread.run(_server())
49+
50+
51+
def stop():
52+
global pipe, server
53+
server.cancel()
54+
server = None
55+
pipe.kill()
56+
pipe = None
57+
58+
59+
async def _server():
60+
from modules import globals
61+
62+
while True:
63+
data = await pipe.get_async()
64+
65+
try:
66+
event, args, kwargs = data
67+
if event == "callback":
68+
callback = callbacks.pop(args[0], globals.gui.show)
69+
callback()
70+
else:
71+
pass
72+
except Exception:
73+
pass
74+
75+
76+
def notify(
77+
title: str,
78+
msg: str,
79+
urgency=desktop_notifier.Urgency.Normal,
80+
icon: desktop_notifier.Icon = None,
81+
buttons: list[desktop_notifier.Button] = [],
82+
attachment: desktop_notifier.Attachment = None,
83+
timeout=5,
84+
):
85+
button_callbacks = {"View": 0}
86+
for button in buttons:
87+
button_callbacks[button.title] = hash(button.on_pressed)
88+
callbacks[hash(button.on_pressed)] = button.on_pressed
89+
kwargs = dict(
90+
title=title,
91+
msg=msg,
92+
urgency=urgency.value,
93+
icon=icon.as_uri() if icon else None,
94+
button_callbacks=button_callbacks,
95+
on_clicked_callback=0,
96+
attachment=attachment.as_uri() if attachment else None,
97+
timeout=timeout,
98+
)
99+
pipe.put(("notify", [], kwargs))
100+
101+
102+
def _callback(callback: int):
103+
print(json.dumps(("callback", [callback], {})), flush=True)
104+
105+
106+
async def _notify(
107+
notifier: desktop_notifier.DesktopNotifier,
108+
title: str,
109+
msg: str,
110+
urgency: str,
111+
icon: str | None,
112+
button_callbacks: dict[str, int],
113+
on_clicked_callback: int,
114+
attachment: str | None,
115+
timeout: int,
116+
):
117+
await notifier.send(
118+
title=title,
119+
message=msg,
120+
urgency=desktop_notifier.Urgency(urgency),
121+
icon=desktop_notifier.Icon(uri=icon) if icon else None,
122+
buttons=[
123+
desktop_notifier.Button(
124+
title=button,
125+
on_pressed=functools.partial(_callback, callback),
126+
)
127+
for button, callback in button_callbacks.items()
128+
],
129+
on_clicked=functools.partial(_callback, on_clicked_callback),
130+
attachment=desktop_notifier.Attachment(uri=attachment) if attachment else None,
131+
timeout=timeout,
132+
)
133+
134+
135+
async def _daemon(icon_uri: str):
136+
loop = asyncio.get_event_loop()
137+
138+
stdin_full = asyncio.Event()
139+
loop.add_reader(sys.stdin.fileno(), stdin_full.set)
140+
141+
notifier = desktop_notifier.DesktopNotifier(
142+
app_name="F95Checker",
143+
app_icon=desktop_notifier.Icon(uri=icon_uri),
144+
)
145+
146+
while True:
147+
await stdin_full.wait()
148+
stdin_full.clear()
149+
line = await loop.run_in_executor(None, sys.stdin.readline)
150+
151+
try:
152+
data = json.loads(line)
153+
154+
event, args, kwargs = data
155+
if event == "notify":
156+
await _notify(notifier, *args, **kwargs)
157+
else:
158+
pass
159+
except Exception:
160+
pass
161+
162+
163+
def daemon(*args, **kwargs):
164+
asyncio.run(_daemon(*args, **kwargs))

modules/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
globals,
2525
icons,
2626
msgbox,
27+
notification_proc,
2728
)
2829

2930

@@ -119,7 +120,7 @@ def done_callback(future: asyncio.Future):
119120
else:
120121
image = None
121122
count = len(globals.updated_games)
122-
globals.gui.tray.notify(
123+
notification_proc.notify(
123124
title="Updates",
124125
msg=f"{count} item{'' if count == 1 else 's'} in your library {'has' if count == 1 else 'have'} received updates!",
125126
attachment=image,
@@ -313,7 +314,7 @@ def push_popup(*args, bottom=False, **kwargs):
313314
"Server downtime",
314315
):
315316
return
316-
globals.gui.tray.notify(
317+
notification_proc.notify(
317318
title="Oops",
318319
msg="Something went wrong! Click to view the error.",
319320
icon=desktop_notifier.Icon(globals.self_path / "resources/icons/error.png")

modules/webview.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import asyncio
21
import base64
32
import json
43
import os
54
import pathlib
65
import re
7-
import shlex
86
import sys
97

108
from PyQt6 import (
@@ -23,8 +21,10 @@
2321

2422

2523
async def start(action: str, *args, centered=True, use_f95_cookies=True, pipe=False, **kwargs):
26-
import subprocess
24+
import asyncio
2725
import imgui
26+
import shlex
27+
import subprocess
2828
from common.structs import (
2929
DaemonPipe,
3030
DaemonProcess,
@@ -44,11 +44,14 @@ async def start(action: str, *args, centered=True, use_f95_cookies=True, pipe=Fa
4444
int(globals.gui.screen_pos[1] + (imgui.io.display_size.y / 2) - size[1] / 2),
4545
)
4646

47+
args = [action, *args]
48+
kwargs = create_kwargs() | kwargs
49+
4750
proc = await asyncio.create_subprocess_exec(
4851
*shlex.split(globals.start_cmd),
49-
"webview", action,
52+
"webview-daemon",
5053
json.dumps(args),
51-
json.dumps(create_kwargs() | kwargs),
54+
json.dumps(kwargs),
5255
stdout=(subprocess.PIPE if pipe else None),
5356
)
5457

0 commit comments

Comments
 (0)