Skip to content

Commit

Permalink
Fixed the deletion of the lock file, which is used to prevent running…
Browse files Browse the repository at this point in the history
… multiple instances of the application. Previously, it wasn't deleted, causing an error on the next launch.

Enabled logging by default, and now the "enable" parameter works as expected; it was previously ignored.
Improved proper application closure in various scenarios.
Enhanced error handling with informative error messages and the option to open log files.
Added information to the README about the potential "AccessDenied" error when using "High" IO priority.
  • Loading branch information
SystemXFiles committed Sep 26, 2023
1 parent 4fd908d commit 790ff7f
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 46 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,4 @@ cython_debug/
/logging.txt

/pg.lock
/sandbox.py
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ object within an array.
- `"VeryLow"`
- `"Low"`
- `"Normal"`
- `"High"`
- `"High"`: Setting the I/O priority to "High" may result in an AccessDenied error in most cases.
- Example: `"ioPriority": { "py/object": "psutil._pswindows.IOPriority", "value": "Normal" }`

- `affinity` (string, optional): Specifies CPU core affinity. You can define affinity as:
Expand Down
2 changes: 1 addition & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ JSON-объект в массиве.
- `"VeryLow"`
- `"Low"`
- `"Normal"`
- `"High"`
- `"High"`: Установка приоритета ввода-вывода на "High" может вызвать ошибку AccessDenied в большинстве случаев.
- Пример: `"ioPriority": { "py/object": "psutil._pswindows.IOPriority", "value": "Normal" }`

- `affinity` (строка, опционально): Определяет привязку процесса к ядру CPU. Вы можете указать привязку следующим
Expand Down
9 changes: 7 additions & 2 deletions process-governor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
sys.stdout = open(os.devnull, 'w')
sys.stderr = open(os.path.join(os.getenv('TEMP'), 'stderr-{}'.format(os.path.basename(sys.argv[0]))), "w")

import platform
import pyuac
from util import pyuac_fix
from util.lock_instance import create_lock_file
from util.lock_instance import create_lock_file, remove_lock_file

from main_loop import start_app

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():
pyuac_fix.runAsAdmin(wait=False, showCmd=False)
else:
create_lock_file()
try:
start_app()
finally:
os.remove("my_program.lock")
remove_lock_file()
2 changes: 1 addition & 1 deletion src/configuration/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Logs:
and the number of backup log files to keep.
"""

enable: bool = False
enable: bool = True
"""
A boolean flag to enable or disable logging. Default is False (logging is disabled).
"""
Expand Down
75 changes: 48 additions & 27 deletions src/main_loop.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import logging
import os
import sys
import threading
from logging import StreamHandler
from logging.handlers import RotatingFileHandler
from threading import Thread
from time import sleep

import psutil
import pystray
from PIL import Image
from psutil._pswindows import Priority, IOPriority
from pystray import MenuItem
from pystray._win32 import Icon

from configuration.config import Config
from resource.resource import get_tray_icon
from service.config_service import ConfigService
from service.rules_service import RulesService
from util.utils import yesno_error_box


def log_setup(config: Config):
Expand All @@ -24,6 +27,9 @@ def log_setup(config: Config):
Args:
config (Config): The configuration object containing logging settings.
"""
if not config.logging.enable:
return

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
level = config.logging.level_as_int()

Expand Down Expand Up @@ -60,39 +66,59 @@ def priority_setup():
pass


def show_tray():
def init_tray() -> Icon:
"""
Display the system tray icon and menu.
Display the system tray icon and menu, allowing the user to gracefully exit the application.
This function creates a system tray icon with a "Quit" menu option to gracefully exit the application.
"""
def quit_window(_icon, _):
_icon.stop()
This function creates a system tray icon with a "Quit" menu option, which allows the user to quit the
Process Governor application gracefully.
menu = (
MenuItem('Quit', quit_window),
Returns:
Icon: The system tray icon object.
"""
menu: tuple[MenuItem] = (
MenuItem('Quit', lambda ico: ico.stop()),
)

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

return icon


def main_loop(config: Config):
def main_loop(config: Config, tray: Icon):
"""
Main application loop for applying rules at regular intervals.
Main application loop for applying rules at regular intervals and managing the system tray icon.
Args:
config (Config): The configuration object containing rule application settings.
tray (Icon): The system tray icon instance to be managed within the loop. It will be stopped gracefully
when the loop exits.
"""
def loop():
while True:
try:
thread = Thread(target=tray.run)
thread.start()

while thread.is_alive():
RulesService.apply_rules(config)
sleep(config.ruleApplyIntervalSeconds)
except KeyboardInterrupt:
pass
except BaseException as e:
logging.exception(e)

thread = threading.Thread(target=loop, name="mainloop")
thread.daemon = True
thread.start()
message = (
f"An error has occurred in the Process Governor application. To troubleshoot, please check the log "
f"file: {config.logging.filename} for details.\n\nWould you like to open the log file?"
)
title = "Process Governor - Error Detected"

if yesno_error_box(title, message):
os.startfile(config.logging.filename)

raise e
finally:
tray.stop()


def start_app():
Expand All @@ -101,14 +127,9 @@ def start_app():
This function loads the configuration, sets up logging and process priorities, and starts the main application loop.
"""
try:
config = ConfigService.load_config()
except BaseException as e:
log_setup(Config())
raise e

config = ConfigService.load_config()
log_setup(config)
priority_setup()

main_loop(config)
show_tray()
tray: Icon = init_tray()
main_loop(config, tray)
2 changes: 0 additions & 2 deletions src/service/processes_info_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from abc import ABC
from fnmatch import fnmatch
from typing import Optional

import psutil
from psutil import NoSuchProcess
Expand Down
6 changes: 3 additions & 3 deletions src/service/rules_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ def apply_rules(cls, config: Config):
Args:
config (Config): The configuration object containing the rules to be applied.
"""
cls.__light_gc_ignored_process_parameters()

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()

Expand Down Expand Up @@ -87,7 +87,7 @@ def __handle_processes(cls, config, processes, services):
f"{', ' + service_info.name + '' if service_info else ''}"
f")")
except NoSuchProcess as _:
pass
logging.warning(f"No such process: {pid}")

@classmethod
def __set_ionice(cls, not_success, process_info, rule):
Expand Down
1 change: 0 additions & 1 deletion src/service/services_info_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from abc import ABC
from fnmatch import fnmatch
from typing import Optional

import psutil
Expand Down
26 changes: 18 additions & 8 deletions src/util/lock_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import psutil

__lock_file = "pg.lock"


def is_process_running(pid):
"""
Expand Down Expand Up @@ -31,16 +33,24 @@ def create_lock_file():
If the lock file does not exist or the process is no longer running, it creates the lock file with the
current process's PID.
"""
lock_file = "pg.lock"

if os.path.isfile(lock_file):
if os.path.isfile(__lock_file):
# Check if the process that created the lock file is still running
with open(lock_file, "r") as file:
pid = int(file.read().strip())

if is_process_running(pid):
sys.exit(1)
with open(__lock_file, "r") as file:
pid_str = file.read().strip()
if pid_str:
if is_process_running(int(pid_str)):
sys.exit(1)

# Create the lock file with the current process's PID
with open(lock_file, "w") as file:
with open(__lock_file, "w") as file:
file.write(str(os.getpid()))


def remove_lock_file():
"""
Remove the lock file used to prevent multiple instances of the application.
This function deletes the lock file created to ensure that only one instance of the application is running.
"""
os.remove(__lock_file)
18 changes: 18 additions & 0 deletions src/util/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from time import time
from typing import Type, TypeVar, Callable, List, Optional

import win32con
from win32api import MessageBoxEx

T = TypeVar('T')


Expand Down Expand Up @@ -45,6 +48,7 @@ def cached(timeout_in_seconds, logged=False) -> Callable[..., T]:
Returns:
Callable: A decorated function with caching capabilities.
"""

def decorator(function: Callable[..., T]) -> Callable[..., T]:
if logged:
logging.info("-- Initializing cache for", function.__name__)
Expand Down Expand Up @@ -128,3 +132,17 @@ def fnmatch_cached(name: str, pattern: str) -> bool:
bool: True if the name matches the pattern, False otherwise.
"""
return pattern and fnmatch(name, pattern)


def yesno_error_box(title: str, message: str) -> bool:
"""
Display a yes/no error message box with a specified title and message.
Args:
title (str): The title of the message box.
message (str): The message to be displayed in the message box.
Returns:
bool: True if the user clicks "Yes," False if the user clicks "No."
"""
return MessageBoxEx(None, message, title, win32con.MB_ICONERROR | win32con.MB_YESNO) == win32con.IDYES

0 comments on commit 790ff7f

Please sign in to comment.