Skip to content

Commit a58c1b3

Browse files
committed
Optimizations, QoL improvements
1 parent 7a30ca8 commit a58c1b3

14 files changed

Lines changed: 159 additions & 144 deletions

File tree

process-governor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pyuac
55

6-
from constants.app_info import APP_NAME, APP_NAME_WITH_VERSION
6+
from constants.app_info import APP_NAME
77
from constants.log import LOG
88
from main_loop import start_app
99
from util.lock_instance import create_lock_file, remove_lock_file
@@ -19,7 +19,7 @@
1919
"Please run the program as an administrator to ensure proper functionality.")
2020

2121
LOG.error(message)
22-
show_error(f"Error Detected - {APP_NAME_WITH_VERSION}", message)
22+
show_error(message)
2323
sys.exit(1)
2424

2525
create_lock_file()

src/configuration/migration/all_migration.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from configuration.migration.m0_rules_to_split_rules_config import MigrationRules2SplitRulesConfig
55
from configuration.migration.m1_new_fields_in_rule import NewFieldsInRule
66
from configuration.migration.m2_remove_high_io_priority import RemoveHighIoPriority
7-
from constants.app_info import APP_NAME_WITH_VERSION
87
from constants.log import LOG
98
from service.config_service import ConfigService
109
from util.messages import show_error
@@ -55,14 +54,12 @@ def run_all_migration():
5554
except Exception as e:
5655
has_error = True
5756
LOG.exception(f"[{migration_name}] Migration failed.")
58-
show_error(
59-
f"Error Detected - {APP_NAME_WITH_VERSION}",
60-
f"Migration `{migration_name}` failed: \n{str(e)}"
61-
)
57+
show_error(f"Migration `{migration_name}` failed: \n{str(e)}")
6258
break
6359

6460
if not has_error:
6561
ConfigService.save_config_raw(config)
6662

63+
6764
if __name__ == '__main__':
6865
run_all_migration()

src/constants/app_info.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
CURRENT_TAG: Final[str] = f"v{APP_VERSION}"
1313
APP_NAME_WITH_VERSION: Final[str] = f"{APP_NAME} {CURRENT_TAG}"
14+
TITLE_ERROR: Final[str] = f"Error Detected - {APP_NAME_WITH_VERSION}"
1415

1516
if is_portable():
1617
APP_PATH: Final[str] = sys._MEIPASS

src/main_loop.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from configuration.config import Config
1010
from configuration.migration.all_migration import run_all_migration
11-
from constants.app_info import APP_NAME_WITH_VERSION, APP_NAME
11+
from constants.app_info import APP_NAME
1212
from constants.files import LOG_FILE_NAME
1313
from constants.log import LOG
1414
from constants.threads import THREAD_SETTINGS, THREAD_TRAY
@@ -20,6 +20,7 @@
2020
from util.messages import yesno_error_box, show_error
2121
from util.scheduler import TaskScheduler
2222
from util.startup import update_startup
23+
from util.updates import check_updates
2324

2425

2526
def priority_setup():
@@ -96,6 +97,7 @@ def start_app():
9697
run_all_migration()
9798
update_startup()
9899
priority_setup()
100+
check_updates(True)
99101

100102
tray: Icon = init_tray()
101103
main_loop(tray)
@@ -110,26 +112,24 @@ def start_app():
110112

111113

112114
def show_rules_error_message():
113-
title = f"Error Detected - {APP_NAME_WITH_VERSION}"
114115
message = "An error has occurred while loading or applying the rules.\n"
115116

116117
if TaskScheduler.check_task(THREAD_SETTINGS):
117118
message += "Please check the correctness of the rules."
118-
show_error(title, message)
119+
show_error(message)
119120
else:
120121
message += f"Would you like to open the {SETTINGS_TITLE} to review and correct the rules?"
121-
if yesno_error_box(title, message):
122+
if yesno_error_box(message):
122123
open_settings()
123124

124125

125126
def show_abstract_error_message(will_closed: bool):
126-
title = f"Error Detected - {APP_NAME_WITH_VERSION}"
127127
will_closed_text = 'The application will now close.' if will_closed else ''
128128
message = (
129129
f"An error has occurred in the {APP_NAME} application. {will_closed_text}\n"
130130
f"To troubleshoot, please check the log file `{LOG_FILE_NAME}` for details.\n\n"
131131
f"Would you like to open the log file?"
132132
)
133133

134-
if yesno_error_box(title, message):
134+
if yesno_error_box(message):
135135
os.startfile(LOG_FILE_NAME)

src/model/process.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class Process(BaseModel):
8282
exclude=True
8383
)
8484

85+
is_new: bool = Field(exclude=True)
86+
8587
def __hash__(self):
8688
return hash((self.pid, self.bin_path, self.process_name, self.cmd_line))
8789

src/service/processes_info_service.py

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,46 @@ class ProcessesInfoService(ABC):
1515
It is an abstract base class (ABC) to be subclassed by specific implementation classes.
1616
"""
1717

18-
__prev_pids: set[int] = []
18+
_cache: dict[int, Process] = {}
1919

2020
@classmethod
21-
def get_processes(
22-
cls,
23-
only_new: bool,
24-
force_return_pids: set[int] = None
25-
) -> dict[int, Process]:
21+
def get_processes(cls) -> dict[int, Process]:
2622
"""
27-
Retrieves a dictionary of running processes and their information.
23+
Returns a dictionary with information about running processes.
2824
29-
Args:
30-
only_new (bool): If True, returns only processes that have started since the last call.
31-
If False, returns all currently running processes.
32-
force_return_pids (set[int], optional): A set of process IDs that should be included in the result
33-
even if they are not new. Defaults to an empty set if not provided.
3425
Returns:
35-
dict[int, Process]: A dictionary where keys are process IDs and values are Process objects,
36-
containing detailed information about each running process.
26+
dict[int, Process]: A dictionary with information about running processes.
3727
"""
38-
services = ServicesInfoService.get_services()
39-
result: dict[int, Process] = {}
40-
current_pids: list[int] = psutil.pids()
41-
force_return_pids = force_return_pids or set()
4228

43-
for pid in current_pids:
44-
if only_new and pid in cls.__prev_pids and pid not in force_return_pids:
45-
continue
29+
cache = cls._cache
30+
services = ServicesInfoService.get_services()
31+
pids = set(psutil.pids())
4632

33+
for pid in pids:
4734
try:
48-
process = psutil.Process(pid)
35+
process_info = psutil.Process(pid)
36+
info = process_info.as_dict(attrs=[
37+
'exe',
38+
'nice', 'ionice', 'cpu_affinity'
39+
])
40+
41+
if pid in cache:
42+
process = cache[pid]
43+
44+
if process.bin_path == info['exe']:
45+
process.priority = none_int(info['nice'])
46+
process.io_priority = none_int(info['ionice'])
47+
process.affinity = info['cpu_affinity']
48+
process.is_new = False
49+
continue
50+
4951
service = services.get(pid)
52+
info = process_info.as_dict(attrs=[
53+
'name', 'exe', 'cmdline',
54+
'nice', 'ionice', 'cpu_affinity'
55+
])
5056

51-
info = process.as_dict(attrs=['name', 'exe', 'nice', 'ionice', 'cpu_affinity', 'cmdline'])
52-
result[pid] = Process.model_construct(
57+
cache[pid] = Process.model_construct(
5358
pid=pid,
5459
process_name=info['name'],
5560
service_name=getattr(service, 'name', None),
@@ -58,16 +63,19 @@ def get_processes(
5863
priority=none_int(info['nice']),
5964
io_priority=none_int(info['ionice']),
6065
affinity=info['cpu_affinity'],
61-
process=process,
62-
service=service
66+
process=process_info,
67+
service=service,
68+
is_new=True
6369
)
6470
except NoSuchProcess:
6571
pass
6672

67-
if only_new:
68-
cls.__prev_pids = set(current_pids)
73+
deleted_pids = cache.keys() - pids
74+
75+
for pid in deleted_pids:
76+
del cache[pid]
6977

70-
return result
78+
return cache.copy()
7179

7280
@staticmethod
7381
def _get_command_line(pid, info):

src/service/rules_service.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class RulesService(ABC):
2929

3030
__ignore_pids: set[int] = {0, os.getpid()}
3131
__ignored_process_parameters: dict[Process, set[ProcessParameter]] = {}
32-
__force_pids: set[int] = set()
3332

3433
@classmethod
3534
def apply_rules(cls, config: Config, only_new: bool):
@@ -47,15 +46,14 @@ def apply_rules(cls, config: Config, only_new: bool):
4746
return
4847

4948
cls.__light_gc_ignored_process_parameters()
50-
cls.__force_pids = cls.__handle_processes(
49+
cls.__handle_processes(
5150
config,
52-
ProcessesInfoService.get_processes(only_new, cls.__force_pids)
51+
ProcessesInfoService.get_processes(),
52+
only_new
5353
)
5454

5555
@classmethod
56-
def __handle_processes(cls, config: Config, processes: dict[int, Process]) -> set[int]:
57-
force_pids: set[int] = set()
58-
56+
def __handle_processes(cls, config: Config, processes: dict[int, Process], only_new: bool):
5957
for pid, process in processes.items():
6058
if pid in cls.__ignore_pids:
6159
continue
@@ -65,16 +63,14 @@ def __handle_processes(cls, config: Config, processes: dict[int, Process]) -> se
6563
if not rule:
6664
continue
6765

68-
if rule.force == BoolStr.YES:
69-
force_pids.add(pid)
66+
if rule.force == BoolStr.NO and only_new and not process.is_new:
67+
continue
7068

7169
if rule.delay > 0:
7270
TaskScheduler.schedule_task(process, cls.__handle_process, rule.delay, process, rule)
7371
else:
7472
cls.__handle_process(process, rule)
7573

76-
return force_pids
77-
7874
@classmethod
7975
def __handle_process(cls, process: Process, rule: ProcessRule | ServiceRule):
8076
parameter_methods: dict[ProcessParameter, tuple[Callable[[Process, ProcessRule | ServiceRule], bool], str]] = {

src/ui/settings.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Optional
44

55
from configuration.migration.all_migration import run_all_migration
6-
from constants.app_info import APP_NAME_WITH_VERSION, APP_NAME
6+
from constants.app_info import APP_NAME_WITH_VERSION, APP_NAME, TITLE_ERROR
77
from constants.files import LOG_FILE_NAME
88
from constants.log import LOG
99
from constants.resources import APP_ICON, UI_SAVE, UI_LOG, UI_CONFIG
@@ -41,7 +41,7 @@ def __init__(self):
4141
def _setup_window(self):
4242
self._center_window()
4343

44-
self.protocol("WM_DELETE_WINDOW", self._on_window_closing)
44+
self.protocol("WM_DELETE_WINDOW", self.close)
4545
self.iconbitmap(APP_ICON)
4646
self.title(APP_NAME_WITH_VERSION)
4747
self.minsize(*RC_WIN_SIZE)
@@ -145,38 +145,45 @@ def _save(self):
145145
self._update_actions_state()
146146
return result
147147

148-
def _on_window_closing(self):
148+
def close(self):
149149
has_error = self._tabs.has_error()
150150

151151
if self._tabs.has_unsaved_changes():
152152
if has_error:
153+
self.to_front()
154+
153155
message = ("There are errors in the rules, and they can't be saved. "
154156
"Do you want to DISCARD them and exit?")
155-
result = messagebox.askyesno(f"{APP_NAME_WITH_VERSION}", message)
157+
result = messagebox.askyesno(TITLE_ERROR, message)
156158

157159
if not result:
158-
return
160+
return False
159161
else:
162+
self.to_front()
163+
160164
message = ("There are unsaved changes. "
161165
"Do you want to save them before exiting?")
162-
result = messagebox.askyesnocancel(f"{APP_NAME_WITH_VERSION}", message)
166+
result = messagebox.askyesnocancel(APP_NAME_WITH_VERSION, message)
163167

164168
if result is None:
165-
return
169+
return False
166170

167171
if result and not self._save():
168-
return
172+
return False
169173
else:
170174
if has_error:
175+
self.to_front()
176+
171177
message = (
172178
f"There are errors in the rules, and they must be corrected before the application can work properly. "
173179
f"Do you still want to close the {SETTINGS_TITLE}?")
174-
result = messagebox.askyesno(f"{APP_NAME_WITH_VERSION}", message)
180+
result = messagebox.askyesno(APP_NAME_WITH_VERSION, message)
175181

176182
if not result:
177-
return
183+
return False
178184

179185
self.destroy()
186+
return True
180187

181188
def _update_actions_state(self, _=None):
182189
tabs = self._tabs
@@ -331,6 +338,12 @@ def _search_focus(self):
331338

332339
tab.actions.search.focus_set()
333340

341+
def to_front(self):
342+
self.deiconify()
343+
self.lift()
344+
self.attributes('-topmost', True)
345+
self.after_idle(self.attributes, '-topmost', False)
346+
334347

335348
class SettingsActions(ttk.Frame):
336349
def __init__(self, *args, **kwargs):
@@ -367,27 +380,50 @@ def _setup_btn(self):
367380
save.pack(**RIGHT_PACK)
368381

369382

383+
__app: Optional[Settings] = None
384+
385+
370386
def open_settings():
387+
global __app
388+
389+
if __app is not None:
390+
__app.after_idle(__app.to_front)
391+
return
392+
371393
def settings():
372394
try:
373-
app = Settings()
374-
app.mainloop()
395+
global __app
396+
397+
try:
398+
__app = Settings()
399+
__app.mainloop()
400+
finally:
401+
__app = None
375402
except:
376403
LOG.exception(f"An unexpected error occurred in the {SETTINGS_TITLE} of {APP_NAME}.")
377404
show_settings_error_message()
378405

379406
TaskScheduler.schedule_task(THREAD_SETTINGS, settings)
380407

381408

409+
def is_opened_settings() -> bool:
410+
global __app
411+
return __app is not None
412+
413+
414+
def get_settings() -> Settings:
415+
global __app
416+
return __app
417+
418+
382419
def show_settings_error_message():
383-
title = f"Error Detected - {APP_NAME_WITH_VERSION}"
384420
message = (
385421
f"An error has occurred in the {SETTINGS_TITLE} of {APP_NAME}.\n"
386422
f"To troubleshoot, please check the log file `{LOG_FILE_NAME}` for details.\n\n"
387423
f"Would you like to open the log file?"
388424
)
389425

390-
if yesno_error_box(title, message):
426+
if yesno_error_box(message):
391427
os.startfile(LOG_FILE_NAME)
392428

393429

0 commit comments

Comments
 (0)