Skip to content

Commit

Permalink
Implemented adding to autorun. Added file modification date check and…
Browse files Browse the repository at this point in the history
…, if there are changes, enforces rules for all processes. Log output has become clearer. Fixed some bugs.
  • Loading branch information
SystemXFiles committed Oct 18, 2023
1 parent c120fde commit 0f38284
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 61 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ priorities, I/O priorities, and core affinity based on user-defined rules in a J
- Fine-tune Windows services and processes based on [user-defined rules](#configuration-format) in the `config.json`
file.
- Continuous monitoring of the configuration file for rule application.
- The ability to add a program to autostart.

## Getting Started

Expand Down
1 change: 1 addition & 0 deletions docs/README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ JSON-конфигурации.
- Тонкая настройка процессов и служб Windows на основе [пользовательских правил](#формат-конфигурации) в
файле `config.json`.
- Непрерывный мониторинг файла конфигурации для применения правил.
- Возможность добавить программу в автозапуск.

## Начало работы

Expand Down
Binary file modified docs/images/tray_menu_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 3 additions & 4 deletions process-governor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@

from main_loop import start_app
from util.lock_instance import create_lock_file, remove_lock_file
from util.messages import message_box, MBIcon
from util.messages import show_info, show_error

if __name__ == "__main__":
if not platform.system() == "Windows":
print("Process Governor is intended to run on Windows only.")
sys.exit(1)

if not pyuac.isUserAdmin():
message_box(
show_error(
"Process Governor",
"This program requires administrator privileges to run.\n"
"Please run the program as an administrator to ensure proper functionality.",
MBIcon.INFORMATION
"Please run the program as an administrator to ensure proper functionality."
)
sys.exit(1)

Expand Down
Binary file modified requirements.txt
Binary file not shown.
18 changes: 16 additions & 2 deletions src/configuration/handler/io_priority.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,29 @@
}


def to_enum(value):
def __to_enum(value):
if isinstance(value, IOPriority):
return value
return __str_to_iopriority_mapping.get(value)


IOPriorityStr = Annotated[
IOPriority,
BeforeValidator(to_enum),
BeforeValidator(__to_enum),
PlainSerializer(lambda value: __iopriority_to_str_mapping.get(value), return_type=str),
WithJsonSchema({'type': 'string'}, mode='serialization'),
]


def iopriority_to_str(value: IOPriority):
"""
Convert a IO priority value to its corresponding string representation.
Args:
value (int): The IO priority value to convert.
Returns:
str: The string representation of the IO priority value, or None if no
mapping is found.
"""
return __iopriority_to_str_mapping.get(value)
18 changes: 16 additions & 2 deletions src/configuration/handler/priority.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,29 @@
}


def to_enum(value):
def __to_enum(value):
if isinstance(value, Priority):
return value
return __str_to_priority_mapping.get(value)


PriorityStr = Annotated[
Priority,
BeforeValidator(to_enum),
BeforeValidator(__to_enum),
PlainSerializer(lambda value: __priority_to_str_mapping.get(value), return_type=str),
WithJsonSchema({'type': 'string'}, mode='serialization'),
]


def priority_to_str(value: Priority):
"""
Convert a priority value to its corresponding string representation.
Args:
value (int): The priority value to convert.
Returns:
str: The string representation of the priority value, or None if no
mapping is found.
"""
return __priority_to_str_mapping.get(value)
39 changes: 24 additions & 15 deletions src/main_loop.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
import traceback
from threading import Thread
from time import sleep
Expand All @@ -8,15 +9,16 @@
import pystray
from PIL import Image
from psutil._pswindows import Priority, IOPriority
from pystray import MenuItem
from pystray import MenuItem, Menu
from pystray._win32 import Icon

from configuration.config import Config
from resource.resource import get_tray_icon
from service.config_service import ConfigService, CONFIG_FILE_NAME
from service.rules_service import RulesService
from util.logs import log_setup, log
from util.messages import message_box, yesno_error_box, MBIcon, show_error
from util.messages import yesno_error_box, show_error
from util.path import get_tray_icon
from util.startup import is_startup, toggle_startup


def priority_setup():
Expand All @@ -40,18 +42,23 @@ def init_tray() -> Icon:
Returns:
Icon: The system tray icon.
"""

image: Image = Image.open(get_tray_icon())
config: Config = ConfigService.load_config()

menu: tuple[MenuItem, ...] = (
MenuItem('Open JSON config', lambda ico: os.startfile(CONFIG_FILE_NAME)),
MenuItem('Open log file', lambda ico: os.startfile(config.logging.filename)),
MenuItem('Quit', lambda ico: ico.stop()),
)
MenuItem('Open JSON config', lambda item: os.startfile(CONFIG_FILE_NAME), default=True),

image: Image = Image.open(get_tray_icon())
icon: Icon = pystray.Icon("tray_icon", image, "Process Governor", menu)
Menu.SEPARATOR,

MenuItem('Run on Startup', lambda item: toggle_startup(), lambda item: is_startup(), visible=getattr(sys, 'frozen', False)),
MenuItem('Open log file', lambda item: os.startfile(config.logging.filename)),

return icon
Menu.SEPARATOR,

MenuItem('Quit', lambda item: item.stop()),
)

return pystray.Icon("tray_icon", image, "Process Governor", menu)


def main_loop(tray: Icon):
Expand All @@ -62,16 +69,18 @@ def main_loop(tray: Icon):
tray (Icon): The system tray icon instance to be managed within the loop. It will be stopped gracefully
when the loop exits.
"""

config: Config = ConfigService.load_config()
thread = Thread(target=tray.run)
thread.start()

log.info('Application started')

config: Optional[Config] = None
is_changed: bool

while thread.is_alive():
RulesService.apply_rules(config)
config = ConfigService.load_config()
config, is_changed = ConfigService.reload_if_changed(config)

RulesService.apply_rules(config, is_changed)
sleep(config.ruleApplyIntervalSeconds)

log.info('The application has stopped')
Expand Down
21 changes: 0 additions & 21 deletions src/resource/resource.py

This file was deleted.

25 changes: 25 additions & 0 deletions src/service/config_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import os.path
from abc import ABC
from os.path import exists
from typing import Optional

from configuration.config import Config
from util.utils import cached
Expand Down Expand Up @@ -47,3 +49,26 @@ def load_config(cls) -> Config:

with open(CONFIG_FILE_NAME, 'r') as file:
return Config(**json.load(file))

__prev_mtime = 0

@classmethod
def reload_if_changed(cls, prev_config: Optional[Config]) -> tuple[Config, bool]:
"""
Reloads the configuration if it has changed since the last reload and returns the updated configuration and a flag indicating whether the configuration has changed.
Parameters:
prev_config (Optional[Config]): The previous configuration object. Can be None if there is no previous configuration.
Returns:
tuple[Config, bool]: A tuple containing the updated configuration object and a boolean flag indicating whether the configuration has changed. If the configuration has changed or there is no previous configuration, the updated configuration is loaded from the file. Otherwise, the previous configuration is returned.
"""
current_mtime = os.path.getmtime(CONFIG_FILE_NAME)
is_changed = current_mtime > cls.__prev_mtime

cls.__prev_mtime = current_mtime

if is_changed or prev_config is None:
return cls.load_config(), True

return prev_config, False
16 changes: 9 additions & 7 deletions src/service/processes_info_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class ProcessesInfoService(ABC):
It is an abstract base class (ABC) to be subclassed by specific implementation classes.
"""

@staticmethod
def get_list() -> dict[int, Process]:
@classmethod
def get_list(cls) -> dict[int, Process]:
"""
Get a dictionary of running processes and their information.
Expand All @@ -22,16 +22,18 @@ def get_list() -> dict[int, Process]:
representing the running processes.
"""
result: dict[int, Process] = {}
cls.__prev_pids = psutil.pids()

for process in psutil.process_iter():
for pid in cls.__prev_pids:
try:
process = psutil.Process(pid)
info = process.as_dict(attrs=['name', 'exe', 'nice', 'ionice', 'cpu_affinity'])
result[process.pid] = Process(
process.pid,
result[pid] = Process(
pid,
info['exe'],
info['name'],
info['nice'],
info['ionice'],
int(info['nice']) if info['nice'] else None,
int(info['ionice']) if info['ionice'] else None,
info['cpu_affinity'],
process
)
Expand Down
34 changes: 24 additions & 10 deletions src/service/rules_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pyuac import isUserAdmin

from configuration.config import Config
from configuration.handler.io_priority import iopriority_to_str
from configuration.handler.priority import priority_to_str
from configuration.rule import Rule
from model.process import Process
from model.service import Service
Expand Down Expand Up @@ -36,20 +38,30 @@ class RulesService(ABC):
__ignored_process_parameters: dict[tuple[int, str], set[_ProcessParameter]] = {}

@classmethod
def apply_rules(cls, config: Config):
def apply_rules(cls, config: Config, force=False):
"""
Apply rules from the provided configuration to processes and services.
Apply the rules defined in the configuration to handle processes and services.
Args:
config (Config): The configuration object containing the rules to be applied.
config (Config): The configuration object containing the rules.
force (bool, optional): If set to True, all processes will be fetched,
regardless of their status. Defaults to False.
Returns:
None
"""
if not config.rules:
return

cls.__light_gc_ignored_process_parameters()

processes: dict[int, Process] = ProcessesInfoService.get_new_processes()
services: dict[int, Service] = ServicesInfoService.get_list()
processes: dict[int, Process]

if force:
processes = ProcessesInfoService.get_list()
else:
processes = ProcessesInfoService.get_new_processes()

cls.__handle_processes(config, processes, services)

Expand Down Expand Up @@ -83,9 +95,9 @@ def __handle_processes(cls, config, processes, services):
cls.__ignore_process_parameter(tuple_pid_name, set(not_success))

log.warning(f"Set failed [{', '.join(map(str, not_success))}] "
f"for {process_info.name} ({process_info.pid}"
f"{', ' + service_info.name + '' if service_info else ''}"
f")")
f"for {process_info.name} ({process_info.pid}"
f"{', ' + service_info.name + '' if service_info else ''}"
f")")
except NoSuchProcess as _:
log.warning(f"No such process: {pid}")

Expand All @@ -98,7 +110,8 @@ def __set_ionice(cls, not_success, process_info, rule: Rule):

try:
process_info.process.ionice(rule.ioPriority)
log.info(f"Set {parameter.value} = {rule.ioPriority} for {process_info.name} ({process_info.pid})")
log.info(
f"Set {parameter.value} {iopriority_to_str(rule.ioPriority)} for {process_info.name} ({process_info.pid})")
except AccessDenied as _:
not_success.append(parameter)

Expand All @@ -111,7 +124,8 @@ def __set_nice(cls, not_success, process_info, rule: Rule):

try:
process_info.process.nice(rule.priority)
log.info(f"Set {parameter.value} = {rule.priority} for {process_info.name} ({process_info.pid})")
log.info(
f"Set {parameter.value} {priority_to_str(rule.priority)} for {process_info.name} ({process_info.pid})")
except AccessDenied as _:
not_success.append(parameter)

Expand All @@ -128,7 +142,7 @@ def __set_affinity(cls, not_success, process_info, rule: Rule):

try:
process_info.process.cpu_affinity(affinity_as_list)
log.info(f"Set {parameter.value} = {rule.affinity} for {process_info.name} ({process_info.pid})")
log.info(f"Set {parameter.value} {rule.affinity} for {process_info.name} ({process_info.pid})")
except AccessDenied as _:
not_success.append(parameter)

Expand Down
12 changes: 12 additions & 0 deletions src/util/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,15 @@ def show_error(title, message):
message (str): The message to display in the error message box.
"""
message_box(title, message, MBIcon.ERROR, MBButton.OK)


def show_info(title, message):
"""
Show an info message box with the given title and message.
Parameters:
title (str): The title of the error message box.
message (str): The message to display in the error message box.
"""
message_box(title, message, MBIcon.INFORMATION, MBButton.OK)

Loading

0 comments on commit 0f38284

Please sign in to comment.