Skip to content

Commit

Permalink
Switched to PyInstaller, UAC request without tricks, changed logging …
Browse files Browse the repository at this point in the history
…settings, improved error handling, easy refactoring and new icon
  • Loading branch information
SystemXFiles committed Oct 15, 2023
1 parent 005f0c3 commit 7092ea4
Show file tree
Hide file tree
Showing 20 changed files with 221 additions and 245 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ cython_debug/
/downloads_for_van/
/dist/
/config.json
/src/data/config.json
/config/config.json
/logging.txt

/pg.lock
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Process Governor

![Logo Process Governor](src/resource/favicon.ico)
![Logo Process Governor](resources/app.ico)

**Process Governor** is a Python utility designed to manage Windows processes and services by adjusting their
priorities, I/O priorities, and core affinity based on user-defined rules in a JSON configuration.
Expand All @@ -21,17 +21,17 @@ To use Process Governor, follow these steps:
1. Clone this repository.
2. Install the required dependencies using `pip`: `pip install -r requirements.txt`
3. Configure your rules in the `config.json` file. You can create the `config.json` file by running the program once.
4. Run the `process-governor.py` script: `python process-governor.py`
4. Run the `process-governor.py` script with **administrative privileges**: `python process-governor.py`

You can close the program by accessing the tray icon:

![Tray menu screenshot](docs/tray_menu_screenshot.png)

## Creating a Portable Build

You can create a portable version of the program using **pyvan**. Follow these steps to build the portable version:
You can create a portable version of the program using **PyInstaller**. Follow these steps to build the portable version:

1. Install pyvan using `pip install pyvan`.
1. Install PyInstaller using `pip install pyinstaller`.
2. Run the `python build_portable.py` script.
3. After the script completes, you will find the portable build in the `dist` folder.

Expand Down
8 changes: 4 additions & 4 deletions README.ru.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Process Governor

![Logo Process Governor](src/resource/favicon.ico)
![Logo Process Governor](resources/app.ico)

**Process Governor** - это утилита на Python, предназначенная для управления процессами и службами в Windows путем
настройки их приоритетов, приоритетов ввода/вывода и привязки к ядрам на основе пользовательских правил, заданных в
Expand All @@ -22,17 +22,17 @@ JSON-конфигурации.
1. Склонируйте этот репозиторий.
2. Установите необходимые зависимости с помощью `pip`: `pip install -r requirements.txt`
3. Настройте правила в файле `config.json`. Вы можете создать файл `config.json`, запустив программу один раз.
4. Запустите скрипт `process-governor.py`: `python process-governor.py`
4. Запустите скрипт `process-governor.py` с правами администратора: `python process-governor.py`

Программу можно закрыть, обратившись к значку в системном трее:

![Tray menu screenshot](docs/tray_menu_screenshot.png)

## Создание портабельной сборки

Вы можете создать портативную версию программы, используя **pyvan**. Чтобы создать портативную версию, выполните следующие шаги:
Вы можете создать портативную версию программы, используя **PyInstaller**. Чтобы создать портативную версию, выполните следующие шаги:

1. Установите pyvan с помощью `pip install pyvan`.
1. Установите PyInstaller с помощью `pip install pyinstaller`.
2. Запустите скрипт `python build_portable.py`.
3. После завершения скрипта, вы найдете портативную версию в папке `dist`.

Expand Down
75 changes: 14 additions & 61 deletions build_portable.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,14 @@
import os
import shutil
from pathlib import Path

import pyvan
from genexe.generate_exe import generate_exe
from pyvan import HEADER_NO_CONSOLE

OPTIONS = {
"main_file_name": "../process-governor.py",
"show_console": False,
"use_existing_requirements": True,
"extra_pip_install_args": [],
"python_version": None,
"use_pipreqs": False,
"install_only_these_modules": [],
"exclude_modules": [],
"include_modules": [],
"path_to_get_pip_and_python_embedded_zip": "downloads_for_van",
"build_dir": "dist",
"pydist_sub_dir": "pydist",
"source_sub_dir": "src",
"icon_file": "src/resource/favicon.ico",
"input_dir": "src"
}

original = pyvan.make_startup_exe


def make_startup_exe(main_file_name, show_console, build_dir, relative_pydist_dir, relative_source_dir, icon_file=None):
""" Make the startup exe file needed to run the script """
print("Making startup exe file")

exe_fname = os.path.join(build_dir, os.path.splitext(os.path.basename(main_file_name))[0] + ".exe")
python_entrypoint = "python.exe"
command_str = f"{{EXE_DIR}}\\{relative_pydist_dir}\\{python_entrypoint} {{EXE_DIR}}\\{relative_source_dir}\\{main_file_name}"

generate_exe(
target=Path(exe_fname),
command=command_str,
icon_file=None if icon_file is None else Path(icon_file),
show_console=show_console
)

main_file_name = os.path.join(build_dir, main_file_name)

if not show_console:
with open(main_file_name, "r", encoding="utf8", errors="surrogateescape") as f:
main_content = f.read()
if HEADER_NO_CONSOLE not in main_content:
with open(main_file_name, "w", encoding="utf8", errors="surrogateescape") as f:
f.write(str(HEADER_NO_CONSOLE + main_content))

shutil.copy(main_file_name, build_dir)

print("Done!")


pyvan.make_startup_exe = make_startup_exe

pyvan.build(**OPTIONS)
import PyInstaller.__main__

PyInstaller.__main__.run([
'process-governor.py',
'--noconfirm',
'--onedir',
'--uac-admin',
'--hide-console', 'hide-early',
'--add-data', './resources/*;./resources',
'--contents-directory', 'scripts',
'--icon', 'resources/app.ico',
'--name', 'Process Governor',
'--debug', 'noarchive',
])
File renamed without changes.
File renamed without changes.
Binary file modified docs/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.
31 changes: 17 additions & 14 deletions process-governor.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import sys, os
if sys.executable.endswith('pythonw.exe'):
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 sys

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

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

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:
remove_lock_file()
message_box(
"Process Governor",
"This program requires administrator privileges to run.\n"
"Please run the program as an administrator to ensure proper functionality.",
MBIcon.INFORMATION
)
sys.exit(1)

create_lock_file()
try:
start_app()
finally:
remove_lock_file()
File renamed without changes.
Binary file added resources/app.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion src/configuration/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Logs(BaseModel):
The name of the log file where log messages will be written. Default is 'logging.txt'.
"""

level: Literal['CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET'] = Field(default='WARN')
level: Literal['CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET'] = Field(default='INFO')
"""
The log level for filtering log messages. Default is 'WARN'.
Valid log levels include: 'CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET'.
Expand Down
114 changes: 47 additions & 67 deletions src/main_loop.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import logging
import os
import sys
from logging import StreamHandler
from logging.handlers import RotatingFileHandler
import traceback
from threading import Thread
from time import sleep
from typing import Optional

import psutil
import pystray
Expand All @@ -17,42 +15,8 @@
from resource.resource import get_tray_icon
from service.config_service import ConfigService, CONFIG_FILE_NAME
from service.rules_service import RulesService
from util.utils import yesno_error_box


def log_setup():
"""
Sets up the logging configuration.
Retrieves the logging configuration from the `ConfigService` and sets up the logging handlers and formatters
accordingly. If the logging configuration is disabled, the function does nothing.
"""

config: Config = ConfigService.load_config()

if not config.logging.enable:
return

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

handler = RotatingFileHandler(
config.logging.filename,
maxBytes=config.logging.maxBytes,
backupCount=config.logging.backupCount,
encoding='utf-8',
)
handler.setLevel(level)
handler.setFormatter(formatter)

stream_handler = StreamHandler(sys.stdout)
stream_handler.setLevel(level)
stream_handler.setFormatter(formatter)

logger = logging.getLogger()
logger.setLevel(level)
logger.addHandler(handler)
logger.addHandler(stream_handler)
from util.logs import log_setup, log
from util.messages import message_box, yesno_error_box, MBIcon, show_error


def priority_setup():
Expand Down Expand Up @@ -100,32 +64,17 @@ def main_loop(tray: Icon):
"""

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

try:
thread = Thread(target=tray.run)
thread.start()

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

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"
log.info('Application started')

if yesno_error_box(title, message):
os.startfile(config.logging.filename)
while thread.is_alive():
RulesService.apply_rules(config)
config = ConfigService.load_config()
sleep(config.ruleApplyIntervalSeconds)

raise e
finally:
tray.stop()
log.info('The application has stopped')


def start_app():
Expand All @@ -134,8 +83,39 @@ def start_app():
This function loads the configuration, sets up logging and process priorities, and starts the main application loop.
"""
log_setup()
priority_setup()
tray: Optional[Icon] = None

tray: Icon = init_tray()
main_loop(tray)
try:
log_setup()
priority_setup()

tray: Icon = init_tray()
main_loop(tray)
except KeyboardInterrupt:
pass
except BaseException as e:
log.exception(e)

config: Optional[Config] = None

try:
config = ConfigService.load_config()
except:
pass

title = "Process Governor - Error Detected"

if config:
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?"
)

if yesno_error_box(title, message):
os.startfile(config.logging.filename)
else:
message = f"An error has occurred in the Process Governor application.\n\n" + traceback.format_exc()
show_error(title, message)
finally:
if tray:
tray.stop()
Binary file removed src/resource/favicon.ico
Binary file not shown.
20 changes: 11 additions & 9 deletions src/resource/resource.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import os
import sys


def get_root():
if getattr(sys, 'frozen', False):
application_path = sys._MEIPASS
else:
application_path = os.getcwd()

return application_path


def get_tray_icon() -> str:
"""
Get the path to the tray icon file.
This function checks if the icon file "favicon.ico" exists in the "resource" directory. If it exists, the
full path to the icon file is returned. If not found, it returns the path relative to the "src" directory.
Returns:
str: The path to the tray icon file.
"""
icon_name = "resource/favicon.ico"

if os.path.isfile(icon_name):
return icon_name

return f"src/{icon_name}"
return f"{get_root()}/resources/app.ico"
Loading

0 comments on commit 7092ea4

Please sign in to comment.