├── .github
└── ISSUE_TEMPLATE
│ ├── bug-report.md
│ └── feature-request.md
├── .gitignore
├── LICENSE
├── build_portable.py
├── docs
├── README.md
├── configuration_file.md
├── icons
│ ├── logo.png
│ └── readme.png
├── images
│ ├── headers
│ │ └── github-banner-readme.png
│ └── screenshots
│ │ ├── other_actions.png
│ │ ├── process_list.png
│ │ ├── process_list_actions.png
│ │ ├── process_list_faded.png
│ │ ├── process_list_menu.png
│ │ ├── process_list_only.png
│ │ ├── process_rules.png
│ │ ├── process_rules_actions.png
│ │ ├── process_rules_add_rule.gif
│ │ ├── process_rules_error.png
│ │ ├── process_rules_menu.png
│ │ ├── process_rules_only.png
│ │ ├── process_rules_unsaved.png
│ │ ├── save_action.png
│ │ ├── service_rules_only.png
│ │ ├── tooltips.png
│ │ └── tray_menu.png
├── rule_behavior_and_tips.md
├── run_and_build.md
└── ui_process_governor.md
├── mypy.ini
├── process-governor.py
├── requirements.txt
├── resources
├── UI
│ ├── add-process-rule.png
│ ├── add-service-rule.png
│ ├── add.png
│ ├── config.png
│ ├── copy.png
│ ├── delete.png
│ ├── down.png
│ ├── error.png
│ ├── file-properties.png
│ ├── go-to-rule.png
│ ├── info-tooltip.png
│ ├── log.png
│ ├── open-folder.png
│ ├── process-list.png
│ ├── process-rules.png
│ ├── process.png
│ ├── redo.png
│ ├── refresh.png
│ ├── save.png
│ ├── select-all.png
│ ├── service-properties.png
│ ├── service-rules.png
│ ├── service.png
│ ├── sort-down.png
│ ├── sort-empty.png
│ ├── sort-up.png
│ ├── undo.png
│ ├── up.png
│ └── warn-tooltip.png
├── app.ico
└── startup.bat
└── src
├── configuration
├── config.py
├── handler
│ └── affinity.py
├── logs.py
├── migration
│ ├── all_migration.py
│ ├── base.py
│ ├── m0_rules_to_split_rules_config.py
│ ├── m1_new_fields_in_rule.py
│ └── m2_remove_high_io_priority_and_logging.py
└── rule.py
├── constants
├── app_info.py
├── files.py
├── log.py
├── resources.py
├── threads.py
├── ui.py
└── updates.py
├── enums
├── bool.py
├── filters.py
├── io_priority.py
├── messages.py
├── priority.py
├── process.py
├── rules.py
└── selector.py
├── main_loop.py
├── model
├── process.py
└── service.py
├── service
├── config_service.py
├── processes_info_service.py
├── rules_service.py
└── services_info_service.py
├── ui
├── settings.py
├── settings_actions.py
├── tray.py
└── widget
│ ├── common
│ ├── button.py
│ ├── combobox.py
│ ├── entry.py
│ ├── label.py
│ └── treeview
│ │ ├── editable.py
│ │ ├── extended.py
│ │ ├── pydantic.py
│ │ ├── scrollable.py
│ │ └── sortable.py
│ └── settings
│ ├── settings_tabs.py
│ ├── tabs
│ ├── base_tab.py
│ ├── processes
│ │ ├── process_list.py
│ │ ├── process_list_actions.py
│ │ ├── process_list_context_menu.py
│ │ └── process_tab.py
│ └── rules
│ │ ├── base_rules_tab.py
│ │ ├── rules_list.py
│ │ ├── rules_list_actions.py
│ │ └── rules_tabs.py
│ └── tooltip.py
└── util
├── cpu.py
├── decorators.py
├── files.py
├── history.py
├── lock_instance.py
├── messages.py
├── scheduler.py
├── startup.py
├── ui.py
├── updates.py
├── utils.py
└── windows_scheduler.py
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Bug report"
3 | about: Create a bug report
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Describe the bug
11 |
12 | A clear and concise description of what the bug is.
13 |
14 | ### Steps to reproduce
15 |
16 | Steps to reproduce the behavior.
17 |
18 | ### Expected behavior
19 |
20 | A clear and concise description of what you expected to happen.
21 |
22 | ### Environment
23 |
24 | - Windows Version: [e.g. 7/8/10/11]
25 | - Other details that you think may affect.
26 |
27 | ### Additional context
28 |
29 | Add any other context about the problem here.
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Feature request"
3 | about: Suggest an idea
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Summary
11 |
12 | Brief explanation of the feature.
13 |
14 | ### Basic example
15 |
16 | Include a basic example or links here.
17 |
18 | ### Motivation
19 |
20 | Why are we doing this? What use cases does it support? What is the expected outcome?
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # IDEs
156 | .idea/
157 | .vscode/
158 | .cursor/
159 |
160 | # Project ignores
161 | /downloads_for_van/
162 | /dist/
163 | /config.json
164 | /config_build/config.json
165 | /logging.txt
166 |
167 | /pg.lock
168 | /sandbox.py
169 | /versionfile.txt
170 |
--------------------------------------------------------------------------------
/build_portable.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os
3 | import shutil
4 |
5 | import PyInstaller.__main__
6 | import pyinstaller_versionfile
7 |
8 | from constants.app_info import APP_NAME, APP_VERSION, APP_AUTHOR, APP_NAME_WITH_VERSION
9 | from constants.files import CONFIG_FILE_NAME
10 | from util.files import explore
11 |
12 | # Setting paths and configuration parameters
13 | VERSION_FILE = "versionfile.txt"
14 | DIST = os.path.join(os.getcwd(), 'dist')
15 | APP_DIST = os.path.join(DIST, APP_NAME)
16 | APP_DIST_WITH_VERSION = os.path.join(DIST, APP_NAME_WITH_VERSION)
17 | CONFIG_FILE_FOR_TESTS = os.path.join(os.getcwd(), CONFIG_FILE_NAME)
18 | CONFIG_FILE_IN_APP_DIST = os.path.join(APP_DIST, CONFIG_FILE_NAME)
19 | SPEC_FILES = r".\*.spec"
20 |
21 | # Deleting existing .spec files to clean up the directory
22 | for filename in glob.glob(SPEC_FILES):
23 | os.remove(filename)
24 |
25 | # Creating a version file for the executable
26 | pyinstaller_versionfile.create_versionfile(
27 | output_file=VERSION_FILE,
28 | version=APP_VERSION,
29 | company_name=APP_AUTHOR,
30 | file_description=APP_NAME,
31 | internal_name=APP_NAME,
32 | legal_copyright=f"© {APP_AUTHOR}",
33 | original_filename=f"{APP_NAME}.exe",
34 | product_name=APP_NAME
35 | )
36 |
37 | # Running PyInstaller to build the application
38 | PyInstaller.__main__.run([
39 | 'process-governor.py', # Source script file
40 | '--clean', # Clean previous builds
41 | '--noconfirm', # No confirmation when deleting dist directory
42 | '--onedir', # Build the app in one directory
43 | '--uac-admin', # Request admin rights on launch
44 | '--hide-console', 'hide-early', # Hide the console on startup
45 | '--add-data', './resources/;./resources', # Add additional resources
46 | '--contents-directory', 'scripts', # Directory for Python and app scripts in the built package
47 | '--icon', 'resources/app.ico', # Application icon
48 | '--debug', 'noarchive', # Disables bundling of application scripts inside the exe
49 | '--name', APP_NAME, # Name of the executable file
50 | '--version-file', VERSION_FILE, # Path to the version file
51 | '--distpath', DIST, # Directory to save the built application
52 | '--collect-all', 'tksvg',
53 | ])
54 |
55 | # Creating an archive of the built application
56 | archive_file_path = shutil.make_archive(APP_DIST_WITH_VERSION, 'zip', APP_DIST)
57 |
58 | # Copying the configuration file for tests into the built application
59 | if os.path.isfile(CONFIG_FILE_FOR_TESTS):
60 | shutil.copyfile(CONFIG_FILE_FOR_TESTS, CONFIG_FILE_IN_APP_DIST)
61 |
62 | explore(archive_file_path)
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [![Contributors][contributors-shield]][contributors-url]
4 | [![Forks][forks-shield]][forks-url]
5 | [![Stargazers][stars-shield]][stars-url]
6 | [![Issues][issues-shield]][issues-url]
7 | [![License][license-shield]][license-url]
8 |
9 |
10 |
28 |
29 |
30 | Table of Contents
31 |
32 | - About The Project
33 | - Getting Started
34 | - Documentation
35 | - Star History
36 | - License
37 |
38 |
39 |
40 | ## About The Project
41 |
42 |
43 |

44 |
45 |
46 | **Process Governor** is a Python utility that automates the management of Windows processes and services by adjusting
47 | their priorities, I/O priorities, and core affinity according to user-defined rules.
48 |
49 | ### Features
50 |
51 | - Adjust process and service priorities for better performance.
52 | - Control I/O priorities to optimize resource utilization.
53 | - Define core affinity for processes.
54 | - Fine-tune Windows services and processes based on user-defined rules.
55 |
56 | ### Screenshots
57 |
58 |
59 | Click to expand
60 |
61 | > 
62 | >
63 | > 
64 | >
65 | > 
66 |
67 |
68 | (back to top)
69 |
70 | ## Getting Started
71 |
72 | To get started with **Process Governor**, follow these steps:
73 |
74 | 1. Download the latest ready-to-use build from the following
75 | link: [Latest Release](https://github.com/SystemXFiles/process-governor/releases/latest).
76 | 2. Run the `Process Governor.exe` executable with **administrative privileges**.
77 | 3. Configure the rules for processes and services.
78 | 4. **Optionally**, enable auto-start for the program to launch automatically with the system.
79 |
80 | You can close the program by accessing the tray icon.
81 |
82 | (back to top)
83 |
84 | ## Documentation
85 |
86 | - [Process Governor UI](ui_process_governor.md)
87 | - [Rule Behavior and Tips](rule_behavior_and_tips.md)
88 | - [Configuration file](configuration_file.md)
89 | - [Running from source and creating a portable build](run_and_build.md)
90 |
91 | (back to top)
92 |
93 | ## Star History
94 |
95 | [](https://star-history.com/#SystemXFiles/process-governor&Date)
96 |
97 | (back to top)
98 |
99 | ## License
100 |
101 | This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](../LICENSE) file for details.
102 |
103 | (back to top)
104 |
105 |
106 |
107 | [contributors-shield]: https://img.shields.io/github/contributors/SystemXFiles/process-governor.svg?style=for-the-badge
108 |
109 | [contributors-url]: https://github.com/SystemXFiles/process-governor/graphs/contributors
110 |
111 | [forks-shield]: https://img.shields.io/github/forks/SystemXFiles/process-governor.svg?style=for-the-badge
112 |
113 | [forks-url]: https://github.com/SystemXFiles/process-governor/network/members
114 |
115 | [stars-shield]: https://img.shields.io/github/stars/SystemXFiles/process-governor.svg?style=for-the-badge
116 |
117 | [stars-url]: https://github.com/SystemXFiles/process-governor/stargazers
118 |
119 | [issues-shield]: https://img.shields.io/github/issues/SystemXFiles/process-governor.svg?style=for-the-badge
120 |
121 | [issues-url]: https://github.com/SystemXFiles/process-governor/issues
122 |
123 | [license-shield]: https://img.shields.io/github/license/SystemXFiles/process-governor.svg?style=for-the-badge
124 |
125 | [license-url]: https://github.com/SystemXFiles/process-governor/blob/master/LICENSE
--------------------------------------------------------------------------------
/docs/configuration_file.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Configuration File
4 |
5 | [ README](README.md#documentation)
6 |
7 | The `config.json` configuration file manages the behavior of the **Process Governor** application. This file allows
8 | users to define rules for regulating process priorities, I/O priorities, and CPU core affinity, as well as manage
9 | services with similar settings.
10 |
11 | The application regularly checks the configuration file for changes and applies the updates accordingly.
12 |
13 | ## Configuration File Example
14 |
15 | Below is an example of the configuration file with several rules defined for processes and services:
16 |
17 |
18 | See an example
19 |
20 | ```json
21 | {
22 | "ruleApplyIntervalSeconds": 1,
23 | "processRules": [
24 | {
25 | "selectorBy": "Name",
26 | "selector": "aida_bench64.dll",
27 | "force": "N"
28 | },
29 | {
30 | "selectorBy": "Name",
31 | "selector": "bg3*.exe",
32 | "affinity": "0-15",
33 | "force": "N",
34 | "delay": "30"
35 | },
36 | {
37 | "selectorBy": "Name",
38 | "selector": "logioptionsplus_*.exe",
39 | "priority": "Idle",
40 | "ioPriority": "Low",
41 | "affinity": "0-15",
42 | "force": "N"
43 | },
44 | {
45 | "selectorBy": "Name",
46 | "selector": "discord.exe",
47 | "priority": "Normal",
48 | "affinity": "0-15",
49 | "force": "Y"
50 | },
51 | {
52 | "selectorBy": "Name",
53 | "selector": "audiodg.exe",
54 | "priority": "Realtime",
55 | "affinity": "16-23",
56 | "force": "N"
57 | },
58 | {
59 | "selectorBy": "Name",
60 | "selector": "*",
61 | "affinity": "0-15",
62 | "force": "N"
63 | }
64 | ],
65 | "serviceRules": [
66 | {
67 | "priority": "Realtime",
68 | "selector": "*audio*",
69 | "force": "N"
70 | }
71 | ],
72 | "version": 3
73 | }
74 | ```
75 |
76 |
77 |
78 | (back to top)
79 |
80 | ## Structure of the `config.json`
81 |
82 | The configuration file contains several sections, each serving a specific purpose.
83 |
84 | ### `ruleApplyIntervalSeconds`
85 |
86 | This parameter defines the interval, in seconds, at which the application applies the rules to processes and services.
87 | The default value is `1`, meaning rules are applied every second.
88 |
89 | ### `processRules`
90 |
91 | This section lists the rules applied to processes. Each rule object specifies how the application should manage a
92 | process based on several key parameters:
93 |
94 | #### Possible parameters:
95 |
96 | - **`selectorBy`** (string): Determines how the `selector` value is interpreted for process matching.
97 | **Valid values:**
98 | - `"Name"`: Match by process name (e.g., `"notepad.exe"`).
99 | - `"Path"`: Match by full executable path (e.g., `"C:/Windows/System32/notepad.exe"`).
100 | - `"Command line"`: Match by command line (e.g., `"App.exe Document.txt"`).
101 |
102 |
103 | - **`selector`** (string): Specifies the name, pattern, or path to the process.
104 | **Supported wildcards:**
105 | - `*`: Matches any number of characters.
106 | - `?`: Matches a single character.
107 | - `**`: Matches any sequence of directories.
108 |
109 | **Examples:**
110 | - `"selector": "name.exe"`
111 | - `"selector": "logioptionsplus_*.exe"`
112 | - `"selector": "C:/Program Files/**/app.exe --file Document.txt"`
113 |
114 |
115 | - **`priority`** (string, optional): Sets the priority level of the process.
116 | **Valid values:**
117 | - `"Idle"`
118 | - `"BelowNormal"`
119 | - `"Normal"`
120 | - `"AboveNormal"`
121 | - `"High"`
122 | - `"Realtime"`
123 |
124 | **Example:** `"priority": "High"`
125 |
126 |
127 | - **`ioPriority`** (string, optional): Sets the I/O priority of the process.
128 | **Valid values:**
129 | - `"VeryLow"`
130 | - `"Low"`
131 | - `"Normal"`
132 |
133 | **Example:** `"ioPriority": "Low"`
134 |
135 |
136 | - **`affinity`** (string, optional): Sets the CPU core affinity for the process.
137 | **Formats:**
138 | - Range (inclusive): `"affinity": "0-3"`
139 | - Specific cores: `"affinity": "0;2;4"`
140 | - Combination: `"affinity": "1;3-5"`
141 |
142 |
143 | - **`force`** (string, optional): Forces the application of the settings.
144 | **Valid values:**
145 | - `"Y"` for continuous enforcement.
146 | - `"N"` for one-time application.
147 |
148 |
149 | - **`delay`** (integer, optional): Delay in seconds before applying the settings.
150 | **Examples:**
151 | - If not specified, the settings are applied immediately.
152 | - Positive values set a delay in seconds before applying the settings.
153 |
154 | (back to top)
155 |
156 | ### `serviceRules`
157 |
158 | This section contains a list of rules applied to services. Unlike `processRules`, the **Service Rule** does not include
159 | the `selectorBy` field because service rules only match by service name using the `selector` field.
160 |
161 | #### Possible parameters:
162 |
163 | - **`selector`** (string): Specifies the name or pattern of the service to match.
164 | **Supported wildcards:**
165 | - `*`: Matches any number of characters.
166 | - `?`: Matches a single character.
167 |
168 | **Examples:**
169 | - `"selector": "ServiceName"`
170 | - `"selector": "*audio*"`
171 |
172 | Other parameters such as `priority`, `ioPriority`, `affinity`, `force`, and `delay` are similar to those
173 | in `processRules`.
174 |
175 | ### `version`
176 |
177 | This field specifies the version of the configuration. It is required for ensuring proper migration and updates when the
178 | program configuration changes over time.
179 |
180 | (back to top)
181 |
182 | ## Validation
183 |
184 | The configuration file undergoes validation to ensure consistency and correctness. If there are any issues, such as
185 | invalid parameter combinations or missing required fields, the application will notify the user and prevent the
186 | configuration from being applied until the errors are resolved.
187 |
188 | (back to top)
--------------------------------------------------------------------------------
/docs/icons/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/icons/logo.png
--------------------------------------------------------------------------------
/docs/icons/readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/icons/readme.png
--------------------------------------------------------------------------------
/docs/images/headers/github-banner-readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/headers/github-banner-readme.png
--------------------------------------------------------------------------------
/docs/images/screenshots/other_actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/other_actions.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_list.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_list_actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_list_actions.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_list_faded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_list_faded.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_list_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_list_menu.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_list_only.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_list_only.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_rules.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_rules.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_rules_actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_rules_actions.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_rules_add_rule.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_rules_add_rule.gif
--------------------------------------------------------------------------------
/docs/images/screenshots/process_rules_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_rules_error.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_rules_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_rules_menu.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_rules_only.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_rules_only.png
--------------------------------------------------------------------------------
/docs/images/screenshots/process_rules_unsaved.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/process_rules_unsaved.png
--------------------------------------------------------------------------------
/docs/images/screenshots/save_action.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/save_action.png
--------------------------------------------------------------------------------
/docs/images/screenshots/service_rules_only.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/service_rules_only.png
--------------------------------------------------------------------------------
/docs/images/screenshots/tooltips.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/tooltips.png
--------------------------------------------------------------------------------
/docs/images/screenshots/tray_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/docs/images/screenshots/tray_menu.png
--------------------------------------------------------------------------------
/docs/rule_behavior_and_tips.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Rule Behavior and Tips
4 |
5 | [ README](README.md#documentation)
6 |
7 | > [!TIP]
8 | > Before proceeding, it is recommended to familiarize yourself with [Process Governor UI](ui_process_governor.md).
9 |
10 | ## Table of Contents
11 |
12 | 1. [Rule Priority](#rule-priority)
13 | 2. [Core Affinity Inheritance in Windows](#core-affinity-inheritance-in-windows)
14 | 3. Common Rule Usage Tips
15 | - [Ignoring a Process](#ignoring-a-process)
16 | - [Rule for All Processes](#rule-for-all-processes)
17 | - [Disabling Hyperthreading](#disabling-hyperthreading)
18 | - [Using Delay to Avoid Side Effects](#using-delay-to-avoid-side-effects)
19 | - [Optimizing for Older or Single-Threaded Games](#optimizing-for-older-or-single-threaded-games)
20 | - [Fixing Audio Crackling Issues](#fixing-audio-crackling-issues)
21 |
22 | ## Rule Priority
23 |
24 | When applying rules, the program first checks **service rules** and then moves to **process rules**. This means that if
25 | a service matches a rule, it will take precedence. If no matching service rule is found, the program then applies the
26 | first matching process rule.
27 |
28 | > [!IMPORTANT]
29 | > Only the first matching rule is applied, so the order of the rules in the configuration is important.
30 |
31 | (back to top)
32 |
33 | ## Core Affinity Inheritance in Windows
34 |
35 | In **Windows**, child processes inherit the core affinity settings from their parent processes. For example, if the
36 | parent process (`explorer.exe`) is restricted to cores 0 and 1, any process launched by it, **for example,** `app.exe`,
37 | will inherit this restriction and be limited to those same cores, unless the application itself or the user changes the
38 | core affinity (for example, via Task Manager or Process Governor).
39 |
40 | This behavior means you should carefully configure rules, especially when using wildcard rules like `*`, which apply to
41 | [all processes](#rule-for-all-processes).
42 |
43 | ### Sources:
44 |
45 | - [Windows Inheritance Documentation](https://learn.microsoft.com/en-us/windows/win32/procthread/inheritance)
46 | - [Why is processor affinity inherited by child processes?](https://devblogs.microsoft.com/oldnewthing/20050817-10/?p=34553)
47 |
48 | (back to top)
49 |
50 | ## Ignoring a Process
51 |
52 | To ignore a process without applying any specific settings:
53 |
54 | 1. Go to the **Process Rules** tab.
55 | 2. Add a new rule.
56 | 3. Set **Process Selector** to the name of the process you want to ignore (e.g., `someprocess.exe`).
57 | 4. Leave all other fields unchanged.
58 |
59 | This will ensure that the process is excluded from any modifications by the governor.
60 |
61 | (back to top)
62 |
63 | ## Rule for All Processes
64 |
65 | To apply a rule to all processes:
66 |
67 | 1. Go to the **Process Rules** tab.
68 | 2. Add a new rule.
69 | 3. Set **Process Selector** to `*` to match all processes (the `*` symbol acts as a wildcard, matching any sequence of
70 | characters in the process name).
71 | 4. Configure the desired settings (e.g., affinity, priority).
72 | 5. Place this rule at the bottom of the list to allow more specific rules to take precedence.
73 |
74 | (back to top)
75 |
76 | ## Disabling Hyperthreading
77 |
78 | To limit a process to physical CPU cores and disable the use of hyperthreaded (logical) cores:
79 |
80 | 1. Go to the **Process Rules** tab.
81 | 2. Add a new rule.
82 | 3. Set **Process Selector** to the target process.
83 | 4. Set **Affinity** to even-numbered cores only (e.g., `0;2;4;6;8;10;12;14`).
84 |
85 | This will prevent the process from using hyperthreaded cores, which can be beneficial for certain workloads.
86 |
87 | (back to top)
88 |
89 | ## Using Delay to Avoid Side Effects
90 |
91 | For some applications, especially games, applying settings like core affinity immediately upon startup can cause issues.
92 | Adding a delay ensures the process has time to initialize before adjustments are applied.
93 |
94 | 1. Go to the **Process Rules** tab.
95 | 2. Add a new rule.
96 | 3. Set **Process Selector** to the game executable (e.g., `bg3.exe`).
97 | 4. Set **Affinity** as needed (e.g., `0-15`).
98 | 5. Set a **Delay** of around 10 seconds to prevent early changes during startup.
99 |
100 | This helps avoid potential problems like sound not working.
101 |
102 | (back to top)
103 |
104 | ## Optimizing for Older or Single-Threaded Games
105 |
106 | Older or poorly optimized games that don’t efficiently use multiple cores can stutter if run with the default core
107 | affinity settings. To improve performance:
108 |
109 | 1. Go to the **Process Rules** tab.
110 | 2. Add a new rule.
111 | 3. Set **Process Selector** to the game process.
112 | 4. Set **Priority** to a higher level (e.g., `AboveNormal` or `High`).
113 | 5. Adjust the **Affinity** to exclude CPU core 0 (e.g., `1-15`).
114 |
115 | This setup can help distribute the load more effectively and reduce stuttering.
116 |
117 | (back to top)
118 |
119 | ## Fixing Audio Crackling Issues
120 |
121 | To address audio crackling or stuttering under high CPU load, it’s recommended to increase the priority of audio-related
122 | processes and services to ensure they have sufficient CPU resources.
123 |
124 | ### Steps for Optimizing Audio Processes:
125 |
126 | 1. Go to the **Process Rules** tab.
127 | 2. Add a new rule for each audio-related process.
128 | 3. Set **Process Selector** to the name of the audio process (e.g., `audiodg.exe`, `voicemeeter8x64.exe`).
129 | 4. Set **Priority** to `Realtime` or `High` depending on the process's importance.
130 |
131 | ### Steps for Optimizing Audio Services:
132 |
133 | 1. Go to the **Service Rules** tab.
134 | 2. Add a new rule for each audio-related service.
135 | 3. Set **Service Selector** to the service name (e.g., `AudioSrv`, `AudioEndpointBuilder`).
136 | 4. Set **Priority** to `Realtime` or `High`.
137 |
138 | This approach prioritizes audio processing over other tasks, preventing interruptions in sound quality during heavy CPU
139 | usage.
140 |
141 | ### Advanced Setup: Load Distribution Across CPU Cores
142 |
143 | For all previously added rules related to audio processes, it is recommended to configure **Affinity** to assign
144 | specific CPU cores dedicated to audio processing tasks. This helps ensure that audio processes have sufficient CPU
145 | resources, minimizing interference from other tasks.
146 |
147 | For example, if you have a **16-thread processor with 8 cores**, you can allocate the last 2 cores (threads 12-15) for
148 | audio tasks, while the first 6 cores (threads 0-11) can be reserved for other applications.
149 |
150 | #### Steps:
151 |
152 | 1. For each previously configured audio process rule:
153 | - Set **Affinity** to the last 2 cores (e.g., threads 12-15) for handling audio processing tasks.
154 |
155 | 2. After configuring the audio processes, add a new rule for all other processes:
156 | - Set **Process Selector** to `*`.
157 | - Set **Affinity** to allocate the remaining CPU cores (e.g., threads 0-11) for non-audio tasks.
158 | - **Important:** This rule must be placed **last** in the rule list, as it serves as a fallback for any processes
159 | that are not explicitly defined in previous rules.
160 |
161 | > [!WARNING]
162 | > Avoid modifying the **Affinity** for audio services like **AudioSrv** or **AudioEndpointBuilder**, as this
163 | > may worsen performance. Adjusting the priority for these services is usually sufficient to resolve audio issues such
164 | > as crackling and stuttering.
165 |
166 | This configuration helps distribute the CPU load, isolating audio processes to specific cores, ensuring smoother and
167 | more stable sound under high system load.
168 |
169 | (back to top)
170 |
--------------------------------------------------------------------------------
/docs/run_and_build.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Running from source and creating a portable build
4 |
5 | [ README](README.md#documentation)
6 |
7 | ## Running from source code
8 |
9 | To run **Process Governor** from source code, follow these steps:
10 |
11 | 1. Clone this repository.
12 | 2. Install the required dependencies using `pip`: `pip install -r requirements.txt`
13 | 3. Run the `process-governor.py` script with **administrative privileges**: `python process-governor.py`
14 | 4. [Configure the rules](docs/ui_rule_configurator.md) for processes and services.
15 |
16 | (back to top)
17 |
18 | ## Creating a portable build
19 |
20 | You can create a portable version of the program using **PyInstaller**. Follow these steps to build the portable
21 | version:
22 |
23 | 1. Install PyInstaller using `pip install pyinstaller`.
24 | 2. Run the `python build_portable.py` script.
25 | 3. After the script completes, you will find the portable build in the `dist` folder.
26 |
27 | Now you have a portable version of the program that you can use without installation.
28 |
29 | (back to top)
30 |
--------------------------------------------------------------------------------
/docs/ui_process_governor.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Process Governor UI
4 |
5 | [ README](README.md#documentation)
6 |
7 | ## Table of Contents
8 |
9 | 1. [System Tray](#system-tray)
10 | 2. [Main Window](#main-window)
11 | 3. [Process List](#process-list)
12 | 4. [Rule Lists](#rule-lists)
13 | - [Process Rules](#process-rules)
14 | - [Service Rules](#service-rules)
15 | - [Managing Rules](#managing-rules)
16 | - [Rules List Context Menu](#rules-list-context-menu)
17 | - [Editing Rules](#editing-rules)
18 | - [Error Handling](#error-handling)
19 |
20 | ## System Tray
21 |
22 | 
23 |
24 | The system tray menu provides quick access to the main program functions:
25 |
26 | - **Process Governor**: Open settings.
27 | - **Open config file**: Open the configuration file for manual settings editing.
28 | - **Open log file**: Open the log file with the program's operation records.
29 | - **Run on Startup**: Enable or disable program startup with the system.
30 | - **Check for Updates**: Check for program updates.
31 | - **Quit**: Exit the program.
32 |
33 | (back to top)
34 |
35 | ## Main Window
36 |
37 | 
38 |
39 | ### Tooltips
40 |
41 | 
42 |
43 | - In the settings interface, tooltips are available, describing the functions of buttons and fields.
44 | - Hover over an interface element to view its tooltip.
45 |
46 | (back to top)
47 |
48 | ### Action Buttons
49 |
50 | 
51 | 
52 |
53 | Main action buttons are available in the settings interface:
54 |
55 | - **Open config file**: Open the configuration file.
56 | - **Open log file**: Open the log file.
57 | - **Save**: Save changes made to the settings.
58 | Hotkey: **Ctrl+S**.
59 | **Details:**
60 | - The button is disabled if there are errors in the settings.
61 | - If no changes have been made, the save button is also disabled.
62 |
63 | (back to top)
64 |
65 | ### Unsaved Changes Status
66 |
67 | 
68 |
69 | Unsaved changes are marked with an asterisk (`*`) on the corresponding rule tab.
70 |
71 | (back to top)
72 |
73 | ## Process List
74 |
75 | 
76 |
77 | The process list provides important information about running processes:
78 |
79 | - **PID**: Unique process identifier.
80 | - **Process Name**: Name of the process.
81 | - **Service Name**: Name of the service, if applicable.
82 | - **Executable Path**: Full path to the executable file.
83 | - **Command Line**: Command with which the process was started.
84 |
85 | (back to top)
86 |
87 | ### Filtering and Search
88 |
89 | 
90 |
91 | - **Search**: Enter text into the search bar to find processes by name or other attributes (e.g., command line).
92 | Hotkey: **Ctrl+F**.
93 |
94 | - **Filter by type**: Use the filter to display processes by type:
95 | - **Show All**: Display all processes.
96 | - **Show Processes**: Display only processes.
97 | - **Show Services**: Display only services.
98 |
99 | - **Refresh**: Refresh the process list.
100 | Hotkey: **F5**.
101 |
102 | (back to top)
103 |
104 | ### Process Context Menu
105 |
106 | 
107 |
108 | - **Add Process Rule**: Add a rule for a process by name, path, or command line.
109 | - **Add Service Rule**: Add a rule for a service by name.
110 | - **Go to Rule**: Go to the existing rule for a process/service.
111 | - **Copy Special**: Copy process attributes (PID, process name, etc.).
112 | - **Open file location**: Open the folder containing the process’s executable file.
113 | - **File Properties**: Open file properties.
114 | - **Service Properties**: Open service properties.
115 |
116 | (back to top)
117 |
118 | ## Rule Lists
119 |
120 | 
121 |
122 | The rule lists are divided into two categories: **Process Rules** and **Service Rules**.
123 |
124 | ### Process Rules
125 |
126 | 
127 |
128 | - **Selector By**: Determines how the **Process Selector** value is interpreted for process matching:
129 | - `Name` — by process name (e.g., `notepad.exe`).
130 | - `Path` — by the full executable path (e.g., `C:/Windows/System32/notepad.exe`).
131 | - `Command line` — by command line arguments (e.g., `App.exe Document.txt` or `D:/Folder/App.exe Document.txt`).
132 |
133 |
134 | - **Process Selector**: Specifies the name, pattern, or path to the process.
135 | **Supported wildcards:**
136 | - `*` — matches any number of characters.
137 | - `?` — matches a single character.
138 | - `**` — matches any sequence of directories.
139 | **Examples:** `name.exe`, `logioptionsplus_*.exe`, `D:/FolderName/App.exe`,
140 | or `C:/Program Files/**/app.exe --file Document.txt`.
141 |
142 |
143 | - **Priority**: Sets the priority level of the process.
144 |
145 | - **I/O Priority**: Sets the I/O priority of the process.
146 |
147 |
148 | - **Affinity**: Sets the CPU core affinity for the process.
149 | **Formats:**
150 | - Range: `0-3`,
151 | - Specific cores: `0;2;4`,
152 | - Combination: `1;3-5`.
153 |
154 |
155 | - **Force**: Forces the application of the settings.
156 | **Possible values:**
157 | - `Y` — for continuous application,
158 | - `N` — for one-time application.
159 |
160 |
161 | - **Delay**: Delay in seconds before applying the settings.
162 | **Possible values:**
163 | - If not specified, the settings are applied immediately.
164 | - Positive values set a delay in seconds before applying the settings.
165 |
166 | (back to top)
167 |
168 | ### Service Rules
169 |
170 | 
171 |
172 | - **Service Selector**: Specifies the name or pattern of the service to match.
173 | **Supported wildcards:**
174 | - `*`: Matches any number of characters.
175 | - `?`: Matches a single character.
176 |
177 | **Examples:**
178 | - `"selector": "ServiceName"`
179 | - `"selector": "*audio*"`
180 |
181 | Other parameters such as **Priority**, **I/O Priority**, **Affinity**, **Force**, and **Delay** are similar to those in
182 | **Process Rules**.
183 | > [!TIP]
184 | > The **Selector By** field is not used in **Service Rules** since services are matched only by name.
185 |
186 | (back to top)
187 |
188 | ### Managing Rules
189 |
190 | 
191 |
192 | - **Add**: Add a new rule.
193 | - **Del**: Delete the selected rules.
194 | - **Up/Down**: Move the selected rule up or down in the rule list.
195 |
196 | (back to top)
197 |
198 | ### Rules List Context Menu
199 |
200 | 
201 |
202 | - **Undo**: Undo the last action.
203 | Hotkey: **Ctrl+Z**.
204 |
205 |
206 | - **Redo**: Redo the undone action.
207 | Hotkeys: **Ctrl+Shift+Z** or **Ctrl+Y**.
208 |
209 |
210 | - **Add**: Add a new rule.
211 | Hotkey: **Ctrl+D**.
212 |
213 |
214 | - **Select all**: Select all rules in the list.
215 | Hotkey: **Ctrl+A**.
216 |
217 |
218 | - **Delete**: Delete the selected rules.
219 | Hotkey: **Del**.
220 |
221 | (back to top)
222 |
223 | ### Editing Rules
224 |
225 | 
226 |
227 | 1. Double-click on the rule cell you want to edit.
228 | 2. Enter new data into the selected cell.
229 | 3. After editing, the changes are automatically saved in the table.
230 |
231 | (back to top)
232 |
233 | ### Error Handling
234 |
235 | 
236 |
237 | - If incorrect data is entered, the corresponding cell is highlighted, and an error icon appears next to it.
238 | - Hover over the error icon to view the error description.
239 |
240 | (back to top)
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | check_untyped_defs = True
3 |
4 | [mypy-pystray.*]
5 | ignore_missing_imports = True
6 |
7 | [mypy-pyuac.*]
8 | ignore_missing_imports = True
9 |
--------------------------------------------------------------------------------
/process-governor.py:
--------------------------------------------------------------------------------
1 | import platform
2 | import sys
3 |
4 | import pyuac
5 |
6 | from constants.app_info import APP_NAME
7 | from constants.log import LOG
8 | from main_loop import start_app
9 | from util.lock_instance import create_lock_file, remove_lock_file
10 | from util.messages import show_error
11 |
12 | if __name__ == "__main__":
13 | if not platform.system() == "Windows":
14 | LOG.error(f"{APP_NAME} is intended to run on Windows only.")
15 | sys.exit(1)
16 |
17 | if not pyuac.isUserAdmin():
18 | message = ("This program requires administrator privileges to run.\n"
19 | "Please run the program as an administrator to ensure proper functionality.")
20 |
21 | LOG.error(message)
22 | show_error(message)
23 | sys.exit(1)
24 |
25 | create_lock_file()
26 | try:
27 | start_app()
28 | finally:
29 | remove_lock_file()
30 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/requirements.txt
--------------------------------------------------------------------------------
/resources/UI/add-process-rule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/add-process-rule.png
--------------------------------------------------------------------------------
/resources/UI/add-service-rule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/add-service-rule.png
--------------------------------------------------------------------------------
/resources/UI/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/add.png
--------------------------------------------------------------------------------
/resources/UI/config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/config.png
--------------------------------------------------------------------------------
/resources/UI/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/copy.png
--------------------------------------------------------------------------------
/resources/UI/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/delete.png
--------------------------------------------------------------------------------
/resources/UI/down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/down.png
--------------------------------------------------------------------------------
/resources/UI/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/error.png
--------------------------------------------------------------------------------
/resources/UI/file-properties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/file-properties.png
--------------------------------------------------------------------------------
/resources/UI/go-to-rule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/go-to-rule.png
--------------------------------------------------------------------------------
/resources/UI/info-tooltip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/info-tooltip.png
--------------------------------------------------------------------------------
/resources/UI/log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/log.png
--------------------------------------------------------------------------------
/resources/UI/open-folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/open-folder.png
--------------------------------------------------------------------------------
/resources/UI/process-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/process-list.png
--------------------------------------------------------------------------------
/resources/UI/process-rules.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/process-rules.png
--------------------------------------------------------------------------------
/resources/UI/process.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/process.png
--------------------------------------------------------------------------------
/resources/UI/redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/redo.png
--------------------------------------------------------------------------------
/resources/UI/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/refresh.png
--------------------------------------------------------------------------------
/resources/UI/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/save.png
--------------------------------------------------------------------------------
/resources/UI/select-all.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/select-all.png
--------------------------------------------------------------------------------
/resources/UI/service-properties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/service-properties.png
--------------------------------------------------------------------------------
/resources/UI/service-rules.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/service-rules.png
--------------------------------------------------------------------------------
/resources/UI/service.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/service.png
--------------------------------------------------------------------------------
/resources/UI/sort-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/sort-down.png
--------------------------------------------------------------------------------
/resources/UI/sort-empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/sort-empty.png
--------------------------------------------------------------------------------
/resources/UI/sort-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/sort-up.png
--------------------------------------------------------------------------------
/resources/UI/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/undo.png
--------------------------------------------------------------------------------
/resources/UI/up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/up.png
--------------------------------------------------------------------------------
/resources/UI/warn-tooltip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/UI/warn-tooltip.png
--------------------------------------------------------------------------------
/resources/app.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SystemXFiles/process-governor/75457621feceecef28255527c60463f7563539e5/resources/app.ico
--------------------------------------------------------------------------------
/resources/startup.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | set "exe_path=%~1"
3 | set "working_directory=%~dp1"
4 | cd /d "%working_directory%"
5 | start "" "%exe_path%"
6 | exit
--------------------------------------------------------------------------------
/src/configuration/config.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from configuration.rule import ProcessRule, ServiceRule
6 |
7 |
8 | class Config(BaseModel):
9 | """
10 | The Config class represents a configuration object for application.
11 |
12 | It defines the structure of the configuration, including rule application interval, and rules.
13 | """
14 |
15 | version: Optional[int] = Field(default=None)
16 | """
17 | The version of the configuration.
18 | This field can be None if the version is not set.
19 | """
20 |
21 | ruleApplyIntervalSeconds: int = Field(default=1)
22 | """
23 | The time interval (in seconds) at which rules are applied to processes and services.
24 | Default is 1 second.
25 | """
26 |
27 | processRules: list[ProcessRule] = Field(default_factory=list)
28 | """
29 | A list of Rule objects that specify how application manages processes based on user-defined rules.
30 | """
31 |
32 | serviceRules: list[ServiceRule] = Field(default_factory=list)
33 | """
34 | A list of Rule objects that specify how application manages services based on user-defined rules.
35 | """
36 |
--------------------------------------------------------------------------------
/src/configuration/handler/affinity.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import PlainSerializer, WithJsonSchema, BeforeValidator
4 | from typing_extensions import Annotated
5 |
6 | from util.cpu import parse_affinity, format_affinity
7 |
8 |
9 | def __to_list(value) -> Optional[list[int]]:
10 | if isinstance(value, list):
11 | return value
12 |
13 | if not value.strip():
14 | return None
15 |
16 | return parse_affinity(value)
17 |
18 |
19 | def __to_str(value) -> Optional[str]:
20 | if not value:
21 | return None
22 |
23 | if isinstance(value, list):
24 | return format_affinity(value)
25 |
26 | return value
27 |
28 |
29 | Affinity = Annotated[
30 | Optional[list[int]],
31 | BeforeValidator(__to_list),
32 | PlainSerializer(__to_str, return_type=str),
33 | WithJsonSchema({'type': 'string'}, mode='serialization'),
34 | ]
35 |
--------------------------------------------------------------------------------
/src/configuration/logs.py:
--------------------------------------------------------------------------------
1 | from logging import getLevelName
2 | from typing import Literal
3 |
4 | from pydantic import BaseModel, Field
5 |
6 |
7 | class Logs(BaseModel):
8 | """
9 | The Logs class represents the logging configuration for application.
10 |
11 | It defines the settings for enabling/disabling logging, specifying the log file name, log level, maximum log file size,
12 | and the number of backup log files to keep.
13 | """
14 |
15 | enable: bool = Field(default=True)
16 | """
17 | A boolean flag to enable or disable logging. Default is True (logging is enabled).
18 | """
19 |
20 | level: Literal['CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET'] = Field(default='DEBUG')
21 | """
22 | The log level for filtering log messages. Default is 'INFO'.
23 | Valid log levels include: 'CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET'.
24 | """
25 |
26 | maxBytes: int = Field(default=1024 * 1024)
27 | """
28 | The maximum size (in bytes) of the log file. When the log file exceeds this size, it will be rotated.
29 | Default is 1 megabyte (1024 * 1024 bytes).
30 | """
31 |
32 | backupCount: int = Field(default=1)
33 | """
34 | The number of backup log files to keep. When log rotation occurs, old log files beyond this count will be deleted.
35 | Default is 1 backup log files.
36 | """
37 |
38 | def level_as_int(self):
39 | """
40 | Get the log level as an integer value.
41 |
42 | This method converts the log level string into its corresponding integer representation.
43 | """
44 | return getLevelName(self.level)
45 |
--------------------------------------------------------------------------------
/src/configuration/migration/all_migration.py:
--------------------------------------------------------------------------------
1 | import copy
2 |
3 | from configuration.migration.base import BaseMigration
4 | from configuration.migration.m0_rules_to_split_rules_config import MigrationRules2SplitRulesConfig
5 | from configuration.migration.m1_new_fields_in_rule import NewFieldsInRule
6 | from configuration.migration.m2_remove_high_io_priority_and_logging import RemoveHighIoPriorityAndLogging
7 | from constants.log import LOG
8 | from service.config_service import ConfigService
9 | from util.messages import show_error
10 |
11 | MIGRATIONS: list[type[BaseMigration]] = [
12 | MigrationRules2SplitRulesConfig,
13 | NewFieldsInRule,
14 | RemoveHighIoPriorityAndLogging
15 | ]
16 | """
17 | List of migration classes to be executed in order.
18 | """
19 |
20 |
21 | def run_all_migration():
22 | """
23 | Runs all necessary migrations on the configuration.
24 | Creates a backup before migration, applies each migration in order,
25 | logs progress, and saves the updated configuration if successful.
26 | Shows an error and stops if any migration fails.
27 | """
28 |
29 | config: dict = ConfigService.load_config_raw()
30 | need_migrate = any(migration.should_migrate(config) for migration in MIGRATIONS)
31 |
32 | if not need_migrate:
33 | return
34 |
35 | LOG.info(f"Creating backup of the current configuration before migration...")
36 | ConfigService.backup_config()
37 |
38 | has_error = False
39 |
40 | for migration in MIGRATIONS:
41 | migration_name = migration.__name__
42 |
43 | try:
44 | if not migration.should_migrate(copy.deepcopy(config)):
45 | continue
46 |
47 | LOG.info(f"[{migration_name}] Starting migration...")
48 |
49 | migrated_config = migration.migrate(copy.deepcopy(config))
50 | migrated_config['version'] = migration.get_target_version()
51 |
52 | LOG.info(f"[{migration_name}] Migration completed to version {migrated_config['version']}.")
53 | config = migrated_config
54 | except Exception as e:
55 | has_error = True
56 | LOG.exception(f"[{migration_name}] Migration failed.")
57 | show_error(f"Migration `{migration_name}` failed: \n{str(e)}")
58 | break
59 |
60 | if not has_error:
61 | ConfigService.save_config_raw(config)
62 |
63 |
64 | if __name__ == '__main__':
65 | run_all_migration()
66 |
--------------------------------------------------------------------------------
/src/configuration/migration/base.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from pydantic.config import JsonDict
4 |
5 |
6 | class BaseMigration(ABC):
7 | @staticmethod
8 | @abstractmethod
9 | def should_migrate(config: JsonDict) -> bool:
10 | """
11 | Checks if migration is necessary.
12 |
13 | :param config: The current configuration dictionary.
14 | :return: True if migration is required, otherwise False.
15 | """
16 | pass
17 |
18 | @staticmethod
19 | @abstractmethod
20 | def migrate(config: JsonDict) -> JsonDict:
21 | """
22 | Performs the migration and returns the updated configuration.
23 |
24 | :param config: The current configuration dictionary.
25 | :return: Updated configuration after migration.
26 | """
27 | pass
28 |
29 | @staticmethod
30 | @abstractmethod
31 | def get_target_version() -> int:
32 | """
33 | Returns the target version for the configuration after migration.
34 |
35 | :return: The target version number.
36 | """
37 | pass
38 |
--------------------------------------------------------------------------------
/src/configuration/migration/m0_rules_to_split_rules_config.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic.config import JsonDict
4 |
5 | from configuration.migration.base import BaseMigration
6 |
7 |
8 | class MigrationRules2SplitRulesConfig(BaseMigration):
9 | @staticmethod
10 | def get_target_version() -> int:
11 | return 1
12 |
13 | @staticmethod
14 | def should_migrate(config: JsonDict) -> bool:
15 | return 'version' not in config or config['version'] is None
16 |
17 | @staticmethod
18 | def migrate(config: JsonDict) -> Optional[JsonDict]:
19 | if 'rules' not in config:
20 | return config
21 |
22 | process_rules = list(filter(lambda o: 'processSelector' in o, config['rules']))
23 | service_rules = list(filter(lambda o: 'serviceSelector' in o, config['rules']))
24 |
25 | for processRule in process_rules:
26 | processRule['selector'] = processRule['processSelector']
27 | del processRule['processSelector']
28 |
29 | for serviceRule in service_rules:
30 | serviceRule['selector'] = serviceRule['serviceSelector']
31 | del serviceRule['serviceSelector']
32 |
33 | del config['rules']
34 | config['processRules'] = process_rules
35 | config['serviceRules'] = service_rules
36 |
37 | return config
38 |
--------------------------------------------------------------------------------
/src/configuration/migration/m1_new_fields_in_rule.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic.config import JsonDict
4 |
5 | from configuration.migration.base import BaseMigration
6 | from enums.bool import BoolStr
7 | from enums.selector import SelectorType
8 |
9 |
10 | class NewFieldsInRule(BaseMigration):
11 | @staticmethod
12 | def get_target_version() -> int:
13 | return 2
14 |
15 | @staticmethod
16 | def should_migrate(config: JsonDict) -> bool:
17 | return config['version'] == 1
18 |
19 | @staticmethod
20 | def migrate(config: JsonDict) -> Optional[JsonDict]:
21 | for rule in config.get('processRules', []):
22 | rule['selectorBy'] = SelectorType.NAME.value
23 | rule['force'] = BoolStr.NO.value
24 |
25 | for rule in config.get('serviceRules', []):
26 | rule['force'] = BoolStr.NO.value
27 |
28 | return config
29 |
--------------------------------------------------------------------------------
/src/configuration/migration/m2_remove_high_io_priority_and_logging.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | from typing import Optional
3 |
4 | from pydantic.config import JsonDict
5 |
6 | from configuration.migration.base import BaseMigration
7 |
8 |
9 | class RemoveHighIoPriorityAndLogging(BaseMigration):
10 | @staticmethod
11 | def get_target_version() -> int:
12 | return 3
13 |
14 | @staticmethod
15 | def should_migrate(config: JsonDict) -> bool:
16 | return config['version'] == 2
17 |
18 | @staticmethod
19 | def migrate(config: JsonDict) -> Optional[JsonDict]:
20 | for rule in itertools.chain(config.get('processRules', []), config.get('serviceRules', [])):
21 | if rule.get('ioPriority') == 'High':
22 | del rule['ioPriority']
23 |
24 | if 'logging' in config:
25 | del config['logging']
26 |
27 | return config
28 |
--------------------------------------------------------------------------------
/src/configuration/rule.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | from configuration.handler.affinity import Affinity
6 | from enums.bool import BoolStr
7 | from enums.io_priority import IOPriorityStr
8 | from enums.priority import PriorityStr
9 | from enums.selector import SelectorType
10 |
11 |
12 | class ProcessRule(BaseModel):
13 | selectorBy: SelectorType = Field(
14 | default=SelectorType.NAME,
15 | title="Selector By",
16 | description="Determines how the __Process Selector__ value is interpreted, influencing how the __process__ is matched.\n"
17 | "**Options:**\n"
18 | "- `Name` - matches by process name (e.g., `notepad.exe`);\n"
19 | "- `Path` - matches by the full executable path (e.g., `C:/Windows/System32/notepad.exe`);\n"
20 | "- `Command line` - matches by command line arguments (e.g., `App.exe Document.txt` or `D:/Folder/App.exe Document.txt`)."
21 | )
22 |
23 | selector: str = Field(
24 | title="Process Selector",
25 | description="Specifies the **name**, **pattern** or **path** of the __process__ to which this rule applies.\n\n"
26 | "**Supports wildcard:** `*` (matches any characters), `?` (matches any single character) and `**` (matches any sequence of directories).\n"
27 | "**Examples:** `name.exe`, `logioptionsplus_*.exe`, `D:/FolderName/App.exe` or `C:/Program Files/**/app.exe --file Document.txt`.",
28 | stretchable_column_ui=True,
29 | justify_ui="left"
30 | )
31 |
32 | priority: Optional[PriorityStr] = Field(
33 | default=None,
34 | title="Priority",
35 | description="Sets the **priority level** for the __process__.\n"
36 | "Higher priority tasks are allocated more CPU time compared to lower priority tasks."
37 | )
38 |
39 | ioPriority: Optional[IOPriorityStr] = Field(
40 | default=None,
41 | title="I/O Priority",
42 | description="Sets the **I/O priority** for the __process__.\n"
43 | "Higher I/O priority means more disk resources and better I/O performance."
44 | )
45 |
46 | affinity: Optional[Affinity] = Field(
47 | default=None,
48 | title="Affinity",
49 | description="Sets the **CPU core affinity** for the __process__, defining which CPU cores are allowed for execution.\n\n"
50 | "**Format:** range `0-3`, specific cores `0;2;4`, combination `1;3-5`.",
51 | justify_ui="left",
52 | width_ui=200
53 | )
54 |
55 | force: BoolStr = Field(
56 | default=BoolStr.NO,
57 | title="Force",
58 | description="**Forces** the settings to be continuously applied to the __process__, even if the application tries to override them.\n\n"
59 | "**Possible values:**\n"
60 | "- `Y` for continuous application;\n"
61 | "- `N` for one-time application.",
62 | )
63 |
64 | delay: Optional[int] = Field(
65 | gt=0,
66 | default=0,
67 | title="Delay",
68 | description="Specifies the **delay** in __seconds__ before the settings are applied to the __process__.\n\n"
69 | "**Possible values:**\n"
70 | "- If not specified, the settings are applied immediately;\n"
71 | "- Positive values set a delay in seconds before applying the settings."
72 | )
73 |
74 |
75 | class ServiceRule(BaseModel):
76 | selector: str = Field(
77 | title="Service Selector",
78 | description="Specifies the **name** of the __service__ to which this rule applies.\n\n"
79 | "**Supports wildcard:** `*` (matches any characters) and `?` (matches any single character)\n"
80 | "**Examples:** `ServiceName` or `Audio*`.",
81 | stretchable_column_ui=True,
82 | justify_ui="left"
83 | )
84 |
85 | priority: Optional[PriorityStr] = Field(
86 | default=None,
87 | title="Priority",
88 | description="Sets the **priority level** for the __service__.\n"
89 | "Higher priority tasks are allocated more CPU time compared to lower priority tasks."
90 | )
91 |
92 | ioPriority: Optional[IOPriorityStr] = Field(
93 | default=None,
94 | title="I/O Priority",
95 | description="Sets the **I/O priority** for the __service__.\n"
96 | "Higher I/O priority means more disk resources and better I/O performance."
97 | )
98 |
99 | affinity: Optional[Affinity] = Field(
100 | default=None,
101 | title="Affinity",
102 | description="Sets the **CPU core affinity** for the __service__, defining which CPU cores are allowed for execution.\n\n"
103 | "**Format:** range `0-3`, specific cores `0;2;4`, combination `1;3-5`.",
104 | justify_ui="left",
105 | width_ui=200
106 | )
107 |
108 | force: BoolStr = Field(
109 | default=BoolStr.NO,
110 | title="Force",
111 | description="**Forces** the settings to be continuously applied to the __service__, even if the application tries to override them.\n\n"
112 | "**Possible values:**\n"
113 | "- `Y` for continuous application;\n"
114 | "- `N` for one-time application.",
115 | )
116 |
117 | delay: Optional[int] = Field(
118 | gt=0,
119 | default=0,
120 | title="Delay",
121 | description="Specifies the **delay** in __seconds__ before the settings are applied to the __service__.\n\n"
122 | "**Possible values:**\n"
123 | "- If not specified, the settings are applied immediately;\n"
124 | "- Positive values set a delay in seconds before applying the settings."
125 | )
126 |
--------------------------------------------------------------------------------
/src/constants/app_info.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from typing import Final
4 |
5 | from util.utils import is_portable
6 |
7 | APP_AUTHOR: Final[str] = "System X - Files"
8 | APP_NAME: Final[str] = "Process Governor"
9 | APP_VERSION: Final[str] = "1.3.1"
10 | APP_PROCESSES = {f"{APP_NAME}.exe", "python.exe"}
11 |
12 | CURRENT_TAG: Final[str] = f"v{APP_VERSION}"
13 | APP_NAME_WITH_VERSION: Final[str] = f"{APP_NAME} {CURRENT_TAG}"
14 | APP_TITLE = f"{APP_NAME_WITH_VERSION} by {APP_AUTHOR}"
15 | TITLE_ERROR: Final[str] = f"Error Detected - {APP_NAME_WITH_VERSION}"
16 |
17 | if is_portable():
18 | APP_PATH: Final[str] = sys._MEIPASS
19 | else:
20 | APP_PATH: Final[str] = os.getcwd()
21 |
22 | STARTUP_TASK_NAME: Final[str] = f"{APP_NAME} Autostart"
23 |
--------------------------------------------------------------------------------
/src/constants/files.py:
--------------------------------------------------------------------------------
1 | from typing import Final
2 |
3 | LOCK_FILE_NAME: Final[str] = "pg.lock"
4 |
5 | CONFIG_FILE_NAME: Final[str] = "config.json"
6 | CONFIG_FILE_ENCODING: Final[str] = "utf-8"
7 | LOG_FILE_NAME: Final[str] = "logging.txt"
8 |
--------------------------------------------------------------------------------
/src/constants/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 | from logging import StreamHandler, Logger, WARNING
4 | from logging.handlers import RotatingFileHandler
5 | from typing import Final
6 |
7 | from configuration.logs import Logs
8 | from constants.files import LOG_FILE_NAME
9 |
10 | logging.addLevelName(WARNING, 'WARN')
11 |
12 |
13 | def __log_setup() -> Logger:
14 | """
15 | Sets up the logging configuration.
16 |
17 | Retrieves the logging configuration from the `ConfigService` and sets up the logging handlers and formatters
18 | accordingly. If the logging configuration is disabled, the function does nothing.
19 | """
20 |
21 | log: Logger = logging.getLogger("proc-gov")
22 | log_cfg: Logs = Logs()
23 |
24 | log.setLevel(log_cfg.level_as_int())
25 |
26 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
27 |
28 | console_handler = StreamHandler(sys.stdout)
29 | console_handler.setFormatter(formatter)
30 | log.addHandler(console_handler)
31 |
32 | if not log_cfg.enable:
33 | return log
34 |
35 | file_handler = RotatingFileHandler(
36 | LOG_FILE_NAME,
37 | maxBytes=log_cfg.maxBytes,
38 | backupCount=log_cfg.backupCount,
39 | encoding='utf-8',
40 | )
41 | file_handler.setFormatter(formatter)
42 | log.addHandler(file_handler)
43 |
44 | return log
45 |
46 |
47 | LOG: Final[logging.Logger] = __log_setup()
48 |
--------------------------------------------------------------------------------
/src/constants/resources.py:
--------------------------------------------------------------------------------
1 | from typing import Final
2 |
3 | from constants.app_info import APP_PATH
4 |
5 | STARTUP_SCRIPT: Final[str] = f"{APP_PATH}/resources/startup.bat"
6 | APP_ICON: Final[str] = f"{APP_PATH}/resources/app.ico"
7 |
8 | UI_PROCESS_RULES: Final[str] = f"{APP_PATH}/resources/UI/process-rules.png"
9 | UI_SERVICE_RULES: Final[str] = f"{APP_PATH}/resources/UI/service-rules.png"
10 | UI_PROCESS_LIST: Final[str] = f"{APP_PATH}/resources/UI/process-list.png"
11 |
12 | UI_ADD: Final[str] = f"{APP_PATH}/resources/UI/add.png"
13 | UI_ADD_PROCESS_RULE: Final[str] = f"{APP_PATH}/resources/UI/add-process-rule.png"
14 | UI_ADD_SERVICE_RULE: Final[str] = f"{APP_PATH}/resources/UI/add-service-rule.png"
15 | UI_DELETE: Final[str] = f"{APP_PATH}/resources/UI/delete.png"
16 | UI_UP: Final[str] = f"{APP_PATH}/resources/UI/up.png"
17 | UI_DOWN: Final[str] = f"{APP_PATH}/resources/UI/down.png"
18 | UI_SAVE: Final[str] = f"{APP_PATH}/resources/UI/save.png"
19 | UI_REFRESH: Final[str] = f"{APP_PATH}/resources/UI/refresh.png"
20 | UI_CONFIG: Final[str] = f"{APP_PATH}/resources/UI/config.png"
21 | UI_LOG: Final[str] = f"{APP_PATH}/resources/UI/log.png"
22 | UI_REDO: Final[str] = f"{APP_PATH}/resources/UI/redo.png"
23 | UI_UNDO: Final[str] = f"{APP_PATH}/resources/UI/undo.png"
24 | UI_SELECT_ALL: Final[str] = f"{APP_PATH}/resources/UI/select-all.png"
25 | UI_COPY: Final[str] = f"{APP_PATH}/resources/UI/copy.png"
26 | UI_SERVICE: Final[str] = f"{APP_PATH}/resources/UI/service.png"
27 | UI_PROCESS: Final[str] = f"{APP_PATH}/resources/UI/process.png"
28 | UI_OPEN_FOLDER: Final[str] = f"{APP_PATH}/resources/UI/open-folder.png"
29 | UI_OPEN_FILE_PROPERTIES: Final[str] = f"{APP_PATH}/resources/UI/file-properties.png"
30 | UI_OPEN_SERVICE_PROPERTIES: Final[str] = f"{APP_PATH}/resources/UI/service-properties.png"
31 | UI_GO_TO_RULE: Final[str] = f"{APP_PATH}/resources/UI/go-to-rule.png"
32 |
33 | UI_INFO_TOOLTIP: Final[str] = f"{APP_PATH}/resources/UI/info-tooltip.png"
34 | UI_WARN_TOOLTIP: Final[str] = f"{APP_PATH}/resources/UI/warn-tooltip.png"
35 |
36 | UI_ERROR: Final[str] = f"{APP_PATH}/resources/UI/error.png"
37 |
38 | UI_SORT_UP: Final[str] = f"{APP_PATH}/resources/UI/sort-up.png"
39 | UI_SORT_DOWN: Final[str] = f"{APP_PATH}/resources/UI/sort-down.png"
40 | UI_SORT_EMPTY: Final[str] = f"{APP_PATH}/resources/UI/sort-empty.png"
41 |
--------------------------------------------------------------------------------
/src/constants/threads.py:
--------------------------------------------------------------------------------
1 | THREAD_SETTINGS = "settings"
2 | THREAD_TRAY = "tray"
3 | THREAD_PROCESS_LIST_DATA = "process_list_data"
4 | THREAD_PROCESS_LIST_ICONS = "process_list_icons"
5 | THREAD_PROCESS_LIST_OPEN_SERVICE_PROPERTIES = "process_list_open_service_properties"
6 |
--------------------------------------------------------------------------------
/src/constants/ui.py:
--------------------------------------------------------------------------------
1 | from tkinter import LEFT, RIGHT
2 | from typing import Final
3 |
4 | UI_PADDING = 10
5 | RC_WIN_SIZE = (1280, 768)
6 | COLUMN_WIDTH_WITH_ICON = 45
7 | TRIM_LENGTH_OF_ITEM_IN_CONTEXT_MENU = 128
8 |
9 | LEFT_PACK = dict(side=LEFT, padx=(0, UI_PADDING))
10 | RIGHT_PACK = dict(side=RIGHT, padx=(UI_PADDING, 0))
11 |
12 | COLUMN_TITLE_PADDING = 30
13 | ERROR_ROW_COLOR = "#ffcdd2"
14 |
15 | SETTINGS_TITLE = "Settings"
16 | OPEN_CONFIG_LABEL = "Open config file"
17 | OPEN_LOG_LABEL = "Open log file"
18 |
19 |
20 | class ActionEvents:
21 | ADD: Final[str] = "<>"
22 | DELETE: Final[str] = "<>"
23 | UP: Final[str] = "<>"
24 | DOWN: Final[str] = "<>"
25 | CANCEL: Final[str] = "<>"
26 | SAVE: Final[str] = "<>"
27 | APPLY_N_CLOSE: Final[str] = "<>"
28 | REFRESH: Final[str] = "<>"
29 | CONFIG: Final[str] = "<>"
30 | LOG: Final[str] = "<>"
31 | FILTER_BY_TYPE: Final[str] = "<>"
32 | SEARCH_CHANGE: Final[str] = "<>"
33 |
34 |
35 | class ExtendedTreeviewEvents:
36 | CHANGE: Final[str] = "<>"
37 | BEFORE_CHANGE: Final[str] = "<>"
38 |
39 |
40 | class EditableTreeviewEvents:
41 | ESCAPE: Final[str] = "<>"
42 | START_EDIT_CELL: Final[str] = "<>"
43 | SAVE_CELL: Final[str] = "<>"
44 |
45 |
46 | class ScrollableTreeviewEvents:
47 | SCROLL: Final[str] = "<>"
48 |
49 |
50 | ERROR_TRYING_UPDATE_TERMINATED_TKINTER_INSTANCE = 'main thread is not in main loop'
51 |
--------------------------------------------------------------------------------
/src/constants/updates.py:
--------------------------------------------------------------------------------
1 | from typing import Final
2 |
3 | API_UPDATE_URL: Final[str] = "https://api.github.com/repos/SystemXFiles/process-governor/releases/latest"
4 | UPDATE_URL: Final[str] = "https://github.com/SystemXFiles/process-governor/releases/latest"
5 |
--------------------------------------------------------------------------------
/src/enums/bool.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 |
3 |
4 | class BoolStr(StrEnum):
5 | NO = "N"
6 | YES = "Y"
7 |
--------------------------------------------------------------------------------
/src/enums/filters.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 |
3 |
4 | class FilterByProcessType(StrEnum):
5 | ALL = 'Show All'
6 | PROCESSES = 'Show Processes'
7 | SERVICES = 'Show Services'
8 |
--------------------------------------------------------------------------------
/src/enums/io_priority.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 | from typing import Final
3 |
4 | from psutil._pswindows import IOPriority
5 |
6 |
7 | class IOPriorityStr(StrEnum):
8 | VERYLOW = 'VeryLow'
9 | LOW = 'Low'
10 | NORMAL = 'Normal'
11 |
12 |
13 | to_iopriority: Final[dict[IOPriorityStr, IOPriority]] = {
14 | IOPriorityStr.VERYLOW: IOPriority.IOPRIO_VERYLOW,
15 | IOPriorityStr.LOW: IOPriority.IOPRIO_LOW,
16 | IOPriorityStr.NORMAL: IOPriority.IOPRIO_NORMAL,
17 | None: None
18 | }
19 |
--------------------------------------------------------------------------------
/src/enums/messages.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 | from win32con import MB_ICONWARNING, MB_ICONINFORMATION, MB_ICONERROR, MB_ICONQUESTION, MB_OK, MB_OKCANCEL, \
4 | MB_ABORTRETRYIGNORE, MB_YESNOCANCEL, MB_YESNO, MB_RETRYCANCEL, IDOK, IDCANCEL, IDABORT, IDRETRY, IDIGNORE, IDYES, \
5 | IDNO, IDCLOSE, IDHELP
6 |
7 |
8 | class MBIcon(IntEnum):
9 | WARNING = MB_ICONWARNING
10 | INFORMATION = MB_ICONINFORMATION
11 | ERROR = MB_ICONERROR
12 | QUESTION = MB_ICONQUESTION
13 |
14 |
15 | class MBButton(IntEnum):
16 | OK = MB_OK
17 | OKCANCEL = MB_OKCANCEL
18 | ABORTRETRYIGNORE = MB_ABORTRETRYIGNORE
19 | YESNOCANCEL = MB_YESNOCANCEL
20 | YESNO = MB_YESNO
21 | RETRYCANCEL = MB_RETRYCANCEL
22 |
23 |
24 | class MBResult(IntEnum):
25 | OK = IDOK
26 | CANCEL = IDCANCEL
27 | ABORT = IDABORT
28 | RETRY = IDRETRY
29 | IGNORE = IDIGNORE
30 | YES = IDYES
31 | NO = IDNO
32 | CLOSE = IDCLOSE
33 | HELP = IDHELP
34 |
--------------------------------------------------------------------------------
/src/enums/priority.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 | from typing import Final
3 |
4 | from psutil._pswindows import Priority
5 |
6 |
7 | class PriorityStr(StrEnum):
8 | IDLE = 'Idle'
9 | BELOW_NORMAL = 'BelowNormal'
10 | NORMAL = 'Normal'
11 | ABOVE_NORMAL = 'AboveNormal'
12 | HIGH = 'High'
13 | REALTIME = 'Realtime'
14 |
15 |
16 | to_priority: Final[dict[PriorityStr, Priority]] = {
17 | PriorityStr.IDLE: Priority.IDLE_PRIORITY_CLASS,
18 | PriorityStr.BELOW_NORMAL: Priority.BELOW_NORMAL_PRIORITY_CLASS,
19 | PriorityStr.NORMAL: Priority.NORMAL_PRIORITY_CLASS,
20 | PriorityStr.ABOVE_NORMAL: Priority.ABOVE_NORMAL_PRIORITY_CLASS,
21 | PriorityStr.HIGH: Priority.HIGH_PRIORITY_CLASS,
22 | PriorityStr.REALTIME: Priority.REALTIME_PRIORITY_CLASS,
23 | None: None
24 | }
25 |
--------------------------------------------------------------------------------
/src/enums/process.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 |
3 |
4 | class ProcessParameter(StrEnum):
5 | AFFINITY = "affinity"
6 | NICE = "priority"
7 | IONICE = "I/O priority"
8 |
--------------------------------------------------------------------------------
/src/enums/rules.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | from pydantic import BaseModel
4 |
5 | from configuration.rule import ProcessRule, ServiceRule
6 |
7 |
8 | class RuleType(Enum):
9 | PROCESS = (ProcessRule, "processRules")
10 | SERVICE = (ServiceRule, "serviceRules")
11 |
12 | def __init__(self, clazz, field_in_config):
13 | self.clazz: type[BaseModel] = clazz
14 | self.field_in_config: str = field_in_config
15 |
--------------------------------------------------------------------------------
/src/enums/selector.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 |
3 |
4 | class SelectorType(StrEnum):
5 | NAME = "Name"
6 | PATH = "Path"
7 | CMDLINE = "CommandLine"
8 |
--------------------------------------------------------------------------------
/src/main_loop.py:
--------------------------------------------------------------------------------
1 | import os
2 | from time import sleep
3 | from typing import Optional
4 |
5 | import psutil
6 | from psutil._pswindows import Priority, IOPriority
7 | from pystray._win32 import Icon
8 |
9 | from configuration.config import Config
10 | from configuration.migration.all_migration import run_all_migration
11 | from constants.app_info import APP_NAME
12 | from constants.files import LOG_FILE_NAME
13 | from constants.log import LOG
14 | from constants.threads import THREAD_SETTINGS, THREAD_TRAY
15 | from constants.ui import SETTINGS_TITLE
16 | from service.config_service import ConfigService
17 | from service.rules_service import RulesService
18 | from ui.settings import open_settings
19 | from ui.tray import init_tray
20 | from util.messages import yesno_error_box, show_error
21 | from util.scheduler import TaskScheduler
22 | from util.startup import update_startup
23 | from util.updates import check_updates
24 |
25 |
26 | def priority_setup():
27 | """
28 | Set process priority and I/O priority.
29 |
30 | This function sets the process priority to BELOW_NORMAL_PRIORITY_CLASS and the I/O priority to IOPRIO_LOW.
31 | """
32 | try:
33 | process = psutil.Process()
34 | process.nice(Priority.BELOW_NORMAL_PRIORITY_CLASS)
35 | process.ionice(IOPriority.IOPRIO_LOW)
36 | except psutil.Error:
37 | pass
38 |
39 |
40 | def main_loop(tray: Icon):
41 | """
42 | Main application loop for applying rules at regular intervals, updating the configuration, and managing the system tray icon.
43 |
44 | Args:
45 | tray (Icon): The system tray icon instance to be managed within the loop. It will be stopped gracefully
46 | when the loop exits.
47 | """
48 | TaskScheduler.schedule_task(THREAD_TRAY, tray.run)
49 |
50 | LOG.info('Application started')
51 |
52 | config: Optional[Config] = None
53 | is_changed: bool
54 | last_error_message = None
55 |
56 | while TaskScheduler.check_task(THREAD_TRAY):
57 | try:
58 | config, is_changed = ConfigService.reload_if_changed(config)
59 |
60 | if is_changed:
61 | LOG.info("Configuration file has been modified. Reloading all rules to apply changes.")
62 |
63 | RulesService.apply_rules(config, not is_changed)
64 | last_error_message = None
65 | except KeyboardInterrupt as e:
66 | raise e
67 | except BaseException as e:
68 | if not config:
69 | config = Config()
70 |
71 | current_error_message = str(e)
72 |
73 | if current_error_message != last_error_message:
74 | LOG.exception("Error in the loop of loading and applying rules.")
75 |
76 | last_error_message = current_error_message
77 |
78 | if ConfigService.rules_has_error():
79 | show_rules_error_message()
80 | else:
81 | show_abstract_error_message(False)
82 |
83 | sleep(config.ruleApplyIntervalSeconds)
84 |
85 | LOG.info('The application has stopped')
86 |
87 |
88 | def start_app():
89 | """
90 | Start application.
91 |
92 | This function loads the configuration, sets up logging and process priorities, and starts the main application loop.
93 | """
94 | tray: Optional[Icon] = None
95 |
96 | try:
97 | run_all_migration()
98 | update_startup()
99 | priority_setup()
100 | check_updates(True)
101 |
102 | tray: Icon = init_tray()
103 | main_loop(tray)
104 | except KeyboardInterrupt:
105 | pass
106 | except:
107 | LOG.exception("A critical error occurred, causing the application to stop.")
108 | show_abstract_error_message(True)
109 | finally:
110 | if tray:
111 | tray.stop()
112 |
113 |
114 | def show_rules_error_message():
115 | message = "An error has occurred while loading or applying the rules.\n"
116 |
117 | if TaskScheduler.check_task(THREAD_SETTINGS):
118 | message += "Please check the correctness of the rules."
119 | show_error(message)
120 | else:
121 | message += f"Would you like to open the {SETTINGS_TITLE} to review and correct the rules?"
122 | if yesno_error_box(message):
123 | open_settings()
124 |
125 |
126 | def show_abstract_error_message(will_closed: bool):
127 | will_closed_text = 'The application will now close.' if will_closed else ''
128 | message = (
129 | f"An error has occurred in the {APP_NAME} application. {will_closed_text}\n"
130 | f"To troubleshoot, please check the log file `{LOG_FILE_NAME}` for details.\n\n"
131 | f"Would you like to open the log file?"
132 | )
133 |
134 | if yesno_error_box(message):
135 | os.startfile(LOG_FILE_NAME)
136 |
--------------------------------------------------------------------------------
/src/model/process.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import psutil
4 | from psutil._pswindows import Priority, IOPriority
5 | from pydantic import BaseModel, Field, ConfigDict
6 |
7 | from model.service import Service
8 |
9 |
10 | class Process(BaseModel):
11 | model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True)
12 |
13 | """
14 | The Process class represents information about a running process.
15 |
16 | It includes attributes such as process ID (pid), executable name (exe), process name (name), priority (nice), I/O priority
17 | (ionice), CPU core affinity, and the associated psutil.Process object.
18 | """
19 |
20 | pid: int = Field(
21 | title="PID",
22 | description="The unique identifier of the __process__ (**Process ID**).",
23 | default_sort_column_ui=True,
24 | width_ui=75
25 | )
26 |
27 | process_name: Optional[str] = Field(
28 | title="Process Name",
29 | description="The **name** of the __process__.",
30 | justify_ui="left",
31 | width_ui=200
32 | )
33 |
34 | service_name: Optional[str] = Field(
35 | title="Service Name",
36 | description="The **name** of the __service__ associated with this __process__.\n"
37 | "This field may be absent if the process is not a service.",
38 | justify_ui="left",
39 | width_ui=250
40 | )
41 |
42 | bin_path: Optional[str] = Field(
43 | title="Executable Path",
44 | description="The **full path** to the executable binary of the __process__.",
45 | stretchable_column_ui=True,
46 | justify_ui="left"
47 | )
48 |
49 | cmd_line: Optional[str] = Field(
50 | title="Command Line",
51 | description="The **command line** used to start the __process__, including all arguments.",
52 | stretchable_column_ui=True,
53 | justify_ui="left"
54 | )
55 |
56 | priority: Optional[Priority] = Field(
57 | title="Priority",
58 | description="The **priority level** of the __process__.",
59 | exclude=True
60 | )
61 |
62 | io_priority: Optional[IOPriority] = Field(
63 | title="I/O Priority",
64 | description="The **I/O priority** of the __process__.",
65 | exclude=True
66 | )
67 |
68 | affinity: Optional[list[int]] = Field(
69 | title="CPU Core Affinity",
70 | description="A list of integers representing the CPU cores to which the __process__ is bound (**CPU core affinity**).",
71 | exclude=True
72 | )
73 |
74 | process: psutil.Process = Field(
75 | description="The psutil.Process object associated with the __process__, providing access to additional control.",
76 | exclude=True
77 | )
78 |
79 | service: Optional[Service] = Field(
80 | description="Contains information about the service if the current __process__ is associated with one.\n"
81 | "If the __process__ is not related to a service, this will be None.",
82 | exclude=True
83 | )
84 |
85 | is_new: bool = Field(exclude=True)
86 |
87 | def __hash__(self):
88 | return hash((self.pid, self.bin_path, self.process_name, self.cmd_line))
89 |
90 | def __eq__(self, other):
91 | if isinstance(other, Process):
92 | return ((self.pid, self.bin_path, self.process_name, self.cmd_line) ==
93 | (other.pid, other.bin_path, other.process_name, other.cmd_line))
94 | return False
95 |
--------------------------------------------------------------------------------
/src/model/service.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass
5 | class Service:
6 | """
7 | The Service class represents information about a Windows service.
8 |
9 | It includes attributes such as service process ID (pid), name, display name and current status.
10 | """
11 |
12 | pid: int
13 | """
14 | The process ID (pid) associated with the Windows service.
15 | """
16 |
17 | name: str
18 | """
19 | The name of the Windows service.
20 | """
21 |
22 | display_name: str
23 | """
24 | The display name of the Windows service.
25 | """
26 |
27 | status: str
28 | """
29 | The current status of the Windows service.
30 | """
31 |
--------------------------------------------------------------------------------
/src/service/config_service.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os.path
3 | from abc import ABC
4 | from datetime import datetime
5 | from os.path import exists
6 | from typing import Optional, Any
7 |
8 | from pydantic.config import JsonDict
9 |
10 | from configuration.config import Config
11 | from constants.files import CONFIG_FILE_NAME, CONFIG_FILE_ENCODING
12 | from enums.rules import RuleType
13 | from util.decorators import cached
14 |
15 |
16 | class ConfigService(ABC):
17 | """
18 | ConfigService is responsible for managing the application's configuration data.
19 |
20 | This class provides methods for saving, loading, and accessing the configuration.
21 | """
22 |
23 | @classmethod
24 | def save_config(cls, config: Config):
25 | """
26 | Save the provided configuration object to a JSON file.
27 |
28 | If the configuration is not initialized, it creates a new one.
29 |
30 | Args:
31 | config (Config): The configuration object to be saved.
32 | """
33 | if config is None:
34 | raise ValueError("config is None")
35 |
36 | with open(CONFIG_FILE_NAME, 'w', encoding=CONFIG_FILE_ENCODING) as file:
37 | json_data = config.model_dump_json(indent=4, exclude_none=True, warnings=False)
38 | file.write(json_data)
39 |
40 | @classmethod
41 | @cached(1)
42 | def load_config(cls, validate=True) -> Config:
43 | """
44 | Load the configuration from a JSON file or create a new one if the file doesn't exist.
45 |
46 | Args:
47 | validate (bool): Whether to validate the configuration upon loading. Defaults to True.
48 |
49 | Returns:
50 | Config: The loaded or newly created configuration object.
51 | """
52 | if not exists(CONFIG_FILE_NAME):
53 | cls.save_config(config := Config())
54 | return config
55 |
56 | with open(CONFIG_FILE_NAME, 'r', encoding=CONFIG_FILE_ENCODING) as file:
57 | if validate:
58 | return Config(**json.load(file))
59 |
60 | return Config.model_construct(**json.load(file))
61 |
62 | __prev_mtime = 0
63 |
64 | @classmethod
65 | def reload_if_changed(cls, prev_config: Optional[Config]) -> tuple[Config, bool]:
66 | """
67 | 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.
68 |
69 | Args:
70 | prev_config (Optional[Config]): The previous configuration object. Can be None if there is no previous configuration.
71 |
72 | Returns:
73 | 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.
74 | """
75 | current_mtime = os.path.getmtime(CONFIG_FILE_NAME)
76 | is_changed = current_mtime > cls.__prev_mtime
77 |
78 | cls.__prev_mtime = current_mtime
79 |
80 | if is_changed or prev_config is None:
81 | return cls.load_config(), True
82 |
83 | return prev_config, False
84 |
85 | @classmethod
86 | def rules_has_error(cls) -> bool:
87 | """
88 | Checks if there are any errors in the rules defined in the configuration.
89 |
90 | Returns:
91 | bool: True if there are errors in the rules, otherwise False.
92 | """
93 | try:
94 | for rule_type in RuleType:
95 | rules: list[Any] = cls.load_config_raw().get(rule_type.field_in_config, [])
96 |
97 | try:
98 | for rule in rules:
99 | rule_type.clazz(**rule)
100 | except:
101 | return True
102 | except:
103 | pass # Yes, this is indeed a pass.
104 |
105 | return False
106 |
107 | @classmethod
108 | def load_config_raw(cls) -> JsonDict:
109 | """
110 | Loads the raw configuration as a dictionary from the configuration file.
111 |
112 | Returns:
113 | dict: The raw configuration data.
114 | """
115 | if not exists(CONFIG_FILE_NAME):
116 | cls.save_config(Config())
117 |
118 | with open(CONFIG_FILE_NAME, 'r', encoding=CONFIG_FILE_ENCODING) as file:
119 | return json.load(file)
120 |
121 | @classmethod
122 | def save_config_raw(cls, config: JsonDict):
123 | """
124 | Saves the raw configuration dictionary to the configuration file.
125 |
126 | Args:
127 | config (dict): The configuration data to be saved.
128 | """
129 | if config is None:
130 | raise ValueError("config is None")
131 |
132 | with open(CONFIG_FILE_NAME, 'w', encoding=CONFIG_FILE_ENCODING) as file:
133 | json.dump(config, file, indent=4)
134 |
135 | @classmethod
136 | def backup_config(cls):
137 | """
138 | Creates a backup of the current configuration file in the same directory where the original configuration file is located.
139 |
140 | If the configuration file does not exist, no backup is created.
141 |
142 | Raises:
143 | IOError: If the backup process fails.
144 | """
145 | if not exists(CONFIG_FILE_NAME):
146 | return
147 |
148 | base_name, ext = os.path.splitext(CONFIG_FILE_NAME)
149 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
150 | backup_filename = f"{base_name}_backup_{timestamp}.{ext}"
151 |
152 | try:
153 | with open(CONFIG_FILE_NAME, 'r', encoding=CONFIG_FILE_ENCODING) as src_file:
154 | with open(backup_filename, 'w', encoding=CONFIG_FILE_ENCODING) as dst_file:
155 | dst_file.write(src_file.read())
156 | except IOError as e:
157 | raise IOError(f"Failed to create backup: {e}")
158 |
159 |
160 | if __name__ == '__main__':
161 | print(ConfigService.rules_has_error())
162 |
--------------------------------------------------------------------------------
/src/service/processes_info_service.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | from abc import ABC
3 | from typing import Optional
4 |
5 | import psutil
6 | from psutil import NoSuchProcess
7 |
8 | from model.process import Process
9 | from service.services_info_service import ServicesInfoService
10 | from util.utils import none_int
11 |
12 |
13 | class ProcessesInfoService(ABC):
14 | """
15 | The ProcessesInfoService class provides methods for retrieving information about running processes.
16 | """
17 |
18 | _cache: dict[int, Process] = {}
19 |
20 | @classmethod
21 | def get_processes(cls) -> dict[int, Process]:
22 | """
23 | Returns a dictionary with information about running processes.
24 |
25 | Returns:
26 | dict[int, Process]: A dictionary with information about running processes.
27 | """
28 |
29 | cache = cls._cache
30 | services: Optional[dict] = None
31 | pids = set(psutil.pids())
32 |
33 | for pid in pids:
34 | try:
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 |
51 | if services is None:
52 | services = ServicesInfoService.get_running_services()
53 |
54 | service = services.get(pid)
55 | info = process_info.as_dict(attrs=[
56 | 'name', 'exe', 'cmdline',
57 | 'nice', 'ionice', 'cpu_affinity'
58 | ])
59 |
60 | cache[pid] = Process.model_construct(
61 | pid=pid,
62 | process_name=info['name'],
63 | service_name=getattr(service, 'name', None),
64 | bin_path=info['exe'],
65 | cmd_line=cls._get_command_line(pid, info),
66 | priority=none_int(info['nice']),
67 | io_priority=none_int(info['ionice']),
68 | affinity=info['cpu_affinity'],
69 | process=process_info,
70 | service=service,
71 | is_new=True
72 | )
73 | except NoSuchProcess:
74 | pass
75 |
76 | deleted_pids = cache.keys() - pids
77 |
78 | for pid in deleted_pids:
79 | del cache[pid]
80 |
81 | return cache.copy()
82 |
83 | @staticmethod
84 | def _get_command_line(pid, info):
85 | if pid == 0:
86 | return ''
87 |
88 | cmdline = info['cmdline'] or ['']
89 |
90 | if not cmdline[0]:
91 | cmdline[0] = info['exe'] or info['name']
92 |
93 | if not cmdline[0]:
94 | return ''
95 |
96 | return subprocess.list2cmdline(cmdline)
97 |
--------------------------------------------------------------------------------
/src/service/rules_service.py:
--------------------------------------------------------------------------------
1 | import os
2 | from abc import ABC
3 | from typing import Optional, Callable
4 |
5 | import psutil
6 | from psutil import AccessDenied, NoSuchProcess
7 |
8 | from configuration.config import Config
9 | from configuration.rule import ProcessRule, ServiceRule
10 | from constants.log import LOG
11 | from enums.bool import BoolStr
12 | from enums.io_priority import to_iopriority
13 | from enums.priority import to_priority
14 | from enums.process import ProcessParameter
15 | from enums.selector import SelectorType
16 | from model.process import Process
17 | from service.processes_info_service import ProcessesInfoService
18 | from util.cpu import format_affinity
19 | from util.decorators import cached
20 | from util.scheduler import TaskScheduler
21 | from util.utils import path_match
22 |
23 |
24 | class RulesService(ABC):
25 | """
26 | The RulesService class provides methods for applying rules to processes and services.
27 | """
28 |
29 | __ignore_pids: set[int] = {0, os.getpid()}
30 | __ignored_process_parameters: dict[Process, set[ProcessParameter]] = {}
31 |
32 | @classmethod
33 | def apply_rules(cls, config: Config, only_new: bool):
34 | """
35 | Apply the rules defined in the configuration to handle processes and services.
36 |
37 | Args:
38 | config (Config): The configuration object containing the rules.
39 | only_new (bool, optional): If set to True, all processes will be fetched, regardless of their status.
40 |
41 | Returns:
42 | None
43 | """
44 | if not (config.serviceRules or config.processRules):
45 | return
46 |
47 | cls.__light_gc_ignored_process_parameters()
48 | cls.__handle_processes(config, ProcessesInfoService.get_processes(), only_new)
49 |
50 | @classmethod
51 | def __handle_processes(
52 | cls, config: Config, processes: dict[int, Process], only_new: bool
53 | ):
54 | for pid, process in processes.items():
55 | if pid in cls.__ignore_pids:
56 | continue
57 |
58 | rule: Optional[ProcessRule | ServiceRule] = cls.__first_rule_by_process(
59 | config, process
60 | )
61 |
62 | if not rule:
63 | continue
64 |
65 | if rule.force == BoolStr.NO and only_new and not process.is_new:
66 | continue
67 |
68 | if rule.delay > 0:
69 | TaskScheduler.schedule_task(
70 | process, cls.__handle_process, process, rule, delay=rule.delay
71 | )
72 | else:
73 | cls.__handle_process(process, rule)
74 |
75 | @classmethod
76 | def __handle_process(cls, process: Process, rule: ProcessRule | ServiceRule):
77 | parameter_methods: dict[
78 | ProcessParameter,
79 | tuple[Callable[[Process, ProcessRule | ServiceRule], bool], str],
80 | ] = {
81 | ProcessParameter.AFFINITY: (
82 | cls.__set_affinity,
83 | format_affinity(rule.affinity),
84 | ),
85 | ProcessParameter.NICE: (cls.__set_nice, rule.priority),
86 | ProcessParameter.IONICE: (cls.__set_ionice, rule.ioPriority),
87 | }
88 |
89 | try:
90 | ignored_parameters = cls.__ignored_process_parameters.setdefault(
91 | process, set()
92 | )
93 |
94 | for param, (method, logger_value) in parameter_methods.items():
95 | if param in ignored_parameters:
96 | continue
97 |
98 | service_name = f", {process.service_name}" if process.service else ""
99 | logger_string = f"{param.value} `{logger_value}` for {process.process_name} ({process.pid}{service_name})"
100 |
101 | try:
102 | if method(process, rule):
103 | LOG.info(f"Set {logger_string}.")
104 | except AccessDenied:
105 | ignored_parameters.add(param)
106 | LOG.warning(f"Failed to set {logger_string}.")
107 |
108 | except NoSuchProcess:
109 | pass
110 |
111 | @classmethod
112 | def __set_ionice(cls, process: Process, rule: ProcessRule | ServiceRule):
113 | io_priority = to_iopriority[rule.ioPriority]
114 |
115 | if io_priority and process.io_priority != io_priority:
116 | process.process.ionice(io_priority)
117 | return True
118 |
119 | @classmethod
120 | def __set_nice(cls, process: Process, rule: ProcessRule | ServiceRule):
121 | priority = to_priority[rule.priority]
122 |
123 | if priority and process.priority != priority:
124 | process.process.nice(priority)
125 | return True
126 |
127 | @classmethod
128 | def __set_affinity(cls, process: Process, rule: ProcessRule | ServiceRule):
129 | if rule.affinity and process.affinity != rule.affinity:
130 | process.process.cpu_affinity(rule.affinity)
131 | return True
132 |
133 | @classmethod
134 | def __first_rule_by_process(
135 | cls, config: Config, process: Process
136 | ) -> Optional[ProcessRule | ServiceRule]:
137 | if process.service:
138 | for rule in config.serviceRules:
139 | if path_match(rule.selector, process.service_name):
140 | return rule
141 |
142 | for rule in config.processRules:
143 | value = cls._get_value_for_matching(process, rule)
144 |
145 | if path_match(rule.selector, value):
146 | return rule
147 |
148 | return None
149 |
150 | @classmethod
151 | def find_rules_ids_by_process(
152 | cls,
153 | process: Process,
154 | process_rules: dict[str, ProcessRule],
155 | service_rules: dict[str, ServiceRule],
156 | ) -> list[tuple[str, ProcessRule | ServiceRule]]:
157 | result = []
158 |
159 | if process.service:
160 | for row_id, rule in service_rules.items():
161 | if path_match(rule.selector, process.service_name):
162 | result.append((row_id, rule))
163 |
164 | for row_id, rule in process_rules.items():
165 | value = cls._get_value_for_matching(process, rule)
166 |
167 | if path_match(rule.selector, value):
168 | result.append((row_id, rule))
169 |
170 | return result
171 |
172 | @classmethod
173 | def _get_value_for_matching(cls, process, rule):
174 | if rule.selectorBy == SelectorType.NAME:
175 | return process.process_name
176 | elif rule.selectorBy == SelectorType.PATH:
177 | return process.bin_path
178 | elif rule.selectorBy == SelectorType.CMDLINE:
179 | return process.cmd_line
180 |
181 | message = f"Unknown selector type: {rule.selectorBy}"
182 | LOG.error(message)
183 | raise ValueError(message)
184 |
185 | @classmethod
186 | @cached(5) # Workaround to ensure the procedure runs only once every 5 seconds
187 | def __light_gc_ignored_process_parameters(cls) -> None:
188 | pids = psutil.pids()
189 | # Create a copy of the items to iterate over, preventing modification issues
190 | current_items = list(cls.__ignored_process_parameters.items())
191 | cls.__ignored_process_parameters = {
192 | key: value for key, value in current_items if key.pid in pids
193 | }
194 |
--------------------------------------------------------------------------------
/src/service/services_info_service.py:
--------------------------------------------------------------------------------
1 | from abc import ABC
2 |
3 | import psutil
4 | from psutil import STATUS_STOPPED, NoSuchProcess, ZombieProcess, AccessDenied
5 | from psutil._pswindows import WindowsService
6 |
7 | from model.service import Service
8 | from util.decorators import suppress_exception
9 |
10 | # Fix bug of psutil
11 | WindowsService.description = suppress_exception(
12 | WindowsService.description,
13 | (FileNotFoundError, ZombieProcess, AccessDenied, OSError),
14 | lambda: ""
15 | )
16 | WindowsService._query_config = suppress_exception(
17 | WindowsService._query_config,
18 | (FileNotFoundError, ZombieProcess, AccessDenied, OSError),
19 | lambda: dict(display_name="", binpath="", username="", start_type="")
20 | )
21 |
22 |
23 | class ServicesInfoService(ABC):
24 | """
25 | The ServicesInfoService class provides methods for retrieving information about Windows services.
26 | """
27 |
28 | @staticmethod
29 | def get_running_services() -> dict[int, Service]:
30 | result: dict[int, Service] = {}
31 |
32 | for service in psutil.win_service_iter():
33 | try:
34 | # noinspection PyUnresolvedReferences
35 | info = service._query_status()
36 | status = info['status']
37 | pid = info['pid']
38 |
39 | if pid == STATUS_STOPPED:
40 | continue
41 |
42 | result[pid] = Service(
43 | pid,
44 | service.name(),
45 | service.display_name(),
46 | status
47 | )
48 | except NoSuchProcess:
49 | pass
50 |
51 | return result
52 |
53 | @staticmethod
54 | def get_services() -> list[Service]:
55 | result: list[Service] = []
56 |
57 | for service in psutil.win_service_iter():
58 | try:
59 | # noinspection PyUnresolvedReferences
60 | info = service._query_status()
61 |
62 | result.append(Service(
63 | info['pid'],
64 | service.name(),
65 | info['status']
66 | ))
67 | except NoSuchProcess:
68 | pass
69 |
70 | return result
71 |
--------------------------------------------------------------------------------
/src/ui/settings_actions.py:
--------------------------------------------------------------------------------
1 | from tkinter import ttk, DISABLED
2 |
3 | from constants.resources import UI_CONFIG, UI_LOG, UI_SAVE
4 | from constants.ui import OPEN_CONFIG_LABEL, ActionEvents, OPEN_LOG_LABEL, LEFT_PACK, RIGHT_PACK
5 | from ui.widget.common.button import ExtendedButton
6 | from util.ui import load_img
7 |
8 |
9 | class SettingsActions(ttk.Frame):
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | self._setup_btn()
13 |
14 | def _setup_btn(self):
15 | self.open_config = open_config = ExtendedButton(
16 | self,
17 | text=f"{OPEN_CONFIG_LABEL}",
18 | event=ActionEvents.CONFIG,
19 | image=load_img(file=UI_CONFIG),
20 | description="**Opens** the __config file__."
21 | )
22 |
23 | self.open_log = open_log = ExtendedButton(
24 | self,
25 | text=f"{OPEN_LOG_LABEL}",
26 | event=ActionEvents.LOG,
27 | image=load_img(file=UI_LOG),
28 | description="**Opens** the __log file__."
29 | )
30 |
31 | self.save = save = ExtendedButton(
32 | self,
33 | text="Save",
34 | state=DISABLED,
35 | event=ActionEvents.SAVE,
36 | image=load_img(file=UI_SAVE),
37 | description="**Saves** the __settings__. \n**Hotkey:** __Ctrl+S__."
38 | )
39 |
40 | open_config.pack(**LEFT_PACK)
41 | open_log.pack(**LEFT_PACK)
42 | save.pack(**RIGHT_PACK)
43 |
--------------------------------------------------------------------------------
/src/ui/tray.py:
--------------------------------------------------------------------------------
1 | import pystray
2 | from PIL import Image
3 | from pystray import MenuItem, Menu
4 | from pystray._win32 import Icon
5 |
6 | from constants.app_info import APP_NAME_WITH_VERSION, APP_TITLE
7 | from constants.resources import APP_ICON
8 | from constants.ui import OPEN_LOG_LABEL, OPEN_CONFIG_LABEL
9 | from ui.settings import open_settings, is_opened_settings, get_settings
10 | from util.files import open_log_file, open_config_file
11 | from util.startup import toggle_startup, is_in_startup
12 | from util.updates import check_updates
13 | from util.utils import is_portable
14 |
15 |
16 | def close_app(item):
17 | if not is_opened_settings():
18 | return item.stop()
19 |
20 | settings = get_settings()
21 |
22 | def close():
23 | if settings.close():
24 | item.stop()
25 |
26 | settings.after_idle(close)
27 |
28 |
29 | def init_tray() -> Icon:
30 | """
31 | Initializes and returns a system tray icon.
32 |
33 | Returns:
34 | Icon: The system tray icon.
35 | """
36 | image: Image = Image.open(APP_ICON)
37 |
38 | menu: tuple[MenuItem, ...] = (
39 | MenuItem(APP_NAME_WITH_VERSION, lambda item: open_settings(), default=True),
40 | Menu.SEPARATOR,
41 |
42 | MenuItem(OPEN_CONFIG_LABEL, lambda _: open_config_file()),
43 | MenuItem(OPEN_LOG_LABEL, lambda _: open_log_file()),
44 | Menu.SEPARATOR,
45 |
46 | MenuItem(
47 | 'Run on Startup',
48 | lambda item: toggle_startup(),
49 | lambda item: is_in_startup(),
50 | visible=is_portable()
51 | ),
52 | MenuItem(
53 | 'Check for Updates',
54 | lambda item: check_updates()
55 | ),
56 | Menu.SEPARATOR,
57 |
58 | MenuItem('Quit', close_app),
59 | )
60 |
61 | return pystray.Icon("tray_icon", image, APP_TITLE, menu)
62 |
--------------------------------------------------------------------------------
/src/ui/widget/common/button.py:
--------------------------------------------------------------------------------
1 | from tkinter import ttk, LEFT, PhotoImage
2 | from typing import Optional
3 |
4 | from constants.ui import UI_PADDING
5 | from util.ui import get_button_font, single_font_width
6 |
7 |
8 | class ExtendedButton(ttk.Button):
9 |
10 | def __init__(
11 | self,
12 | master,
13 | *args,
14 | event: str,
15 | text: Optional[str] = None,
16 | width: Optional[int] = None,
17 | image=None,
18 | description=None,
19 | compound=None,
20 | **kwargs
21 | ):
22 | self._image = image
23 | self._font = get_button_font()
24 | self.description = description
25 |
26 | if compound is None:
27 | compound = LEFT if text else None
28 |
29 | super().__init__(
30 | master,
31 | *args,
32 | text=f" {text}" if image and text else text,
33 | command=lambda: master.event_generate(event),
34 | image=image,
35 | compound=compound,
36 | width=self._calc_width(text, image, width),
37 | **kwargs
38 | )
39 |
40 | def _calc_width(self, text: Optional[str], image: Optional[PhotoImage], width: Optional[int]) -> Optional[int]:
41 | if width:
42 | return width
43 |
44 | width_image = getattr(image, 'width', lambda: 0)()
45 |
46 | if not text:
47 | return None
48 |
49 | return int((self._font.measure(text) + width_image + UI_PADDING) / single_font_width()) + 1
50 |
--------------------------------------------------------------------------------
/src/ui/widget/common/combobox.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from tkinter import ttk, Tk
3 | from typing import Optional
4 |
5 | from util.ui import get_default_font, single_font_width
6 | from util.utils import extract_type, get_values_from_enum
7 |
8 |
9 | class EnumCombobox(ttk.Combobox):
10 | def __init__(
11 | self,
12 | master,
13 | annotation: type[Enum] | Optional[type[Enum]],
14 | description=None,
15 | auto_width=True,
16 | *args,
17 | **kwargs
18 | ):
19 | self.description = description
20 | self._font = get_default_font() if auto_width else None
21 | self._enum_type = extract_type(annotation)
22 | self._values = get_values_from_enum(annotation)
23 |
24 | super().__init__(
25 | master,
26 | values=self._values,
27 | width=self._calculate_max_width() if auto_width else None,
28 | *args,
29 | **kwargs
30 | )
31 |
32 | def get_enum_value(self) -> Optional[Enum]:
33 | selected_value = self.get()
34 |
35 | if selected_value:
36 | return self._enum_type(selected_value)
37 |
38 | return None
39 |
40 | def _calculate_max_width(self):
41 | return max(map(self._font.measure, self._values)) // single_font_width() + 1
42 |
43 |
44 | if __name__ == "__main__":
45 | # Пример использования
46 |
47 | class MyEnum(str, Enum):
48 | OPTION1 = "Option 1"
49 | OPTION2 = "Option 2"
50 | OPTION3 = "Option 3"
51 |
52 |
53 | root = Tk()
54 |
55 | combobox = EnumCombobox(root, MyEnum, state="readonly")
56 | combobox.pack(padx=10, pady=10)
57 |
58 |
59 | def on_select(event):
60 | enum_value = combobox.get_enum_value()
61 | print(f"Selected Enum: {enum_value}")
62 |
63 |
64 | combobox.bind("<>", on_select, '+')
65 |
66 | root.mainloop()
67 |
--------------------------------------------------------------------------------
/src/ui/widget/common/entry.py:
--------------------------------------------------------------------------------
1 | from tkinter import END, Entry
2 |
3 | from util.history import HistoryManager
4 |
5 |
6 | class ExtendedEntry(Entry):
7 | def __init__(self, master=None, placeholder=None, color='grey', description=None, *args, **kwargs):
8 | super().__init__(
9 | master,
10 | *args,
11 | **kwargs
12 | )
13 |
14 | self._commit_history = True
15 | self.description = description
16 | self._setup_placeholder(color, placeholder)
17 | self._setup_history()
18 |
19 | def _setup_placeholder(self, color, placeholder):
20 | self._placeholder = placeholder
21 |
22 | if placeholder is not None:
23 | self._placeholder_color = color
24 | self._default_fg_color = self['fg']
25 |
26 | self.bind("", self._focus_in, '+')
27 | self.bind("", self._focus_out, '+')
28 |
29 | self._focus_out()
30 |
31 | def _setup_history(self):
32 | self.history = history = HistoryManager(self.get, self.set)
33 |
34 | # Doesn't work globally
35 | # self.bind("<>", lambda _: history.redo() or print('redo'), '+')
36 | # self.bind("<>", lambda _: history.undo() or print('undo'), '+')
37 |
38 | self.config(validate="key", validatecommand=(self.register(self._text_changed)))
39 |
40 | def _text_changed(self):
41 | if self._commit_history:
42 | self.history.commit()
43 | return True
44 |
45 | def _set_placeholder(self):
46 | self.set(self._placeholder)
47 | self['fg'] = self._placeholder_color
48 |
49 | def _focus_in(self, *args):
50 | if super().get() == self._placeholder:
51 | self.delete(0, END)
52 | self['fg'] = self._default_fg_color
53 |
54 | def _focus_out(self, *args):
55 | if not self.get():
56 | self._set_placeholder()
57 |
58 | def get(self):
59 | current_text = super().get()
60 |
61 | if self._placeholder is not None and current_text == self._placeholder:
62 | return ""
63 | else:
64 | return current_text
65 |
66 | def set(self, value):
67 | try:
68 | self._commit_history = False
69 | self.delete(0, END)
70 | self.insert(0, value)
71 | finally:
72 | self._commit_history = True
73 |
--------------------------------------------------------------------------------
/src/ui/widget/common/label.py:
--------------------------------------------------------------------------------
1 | import re
2 | from tkinter import font, END, Text, ttk, Label
3 |
4 | from util.ui import get_parent_with_bg, get_label_font
5 |
6 |
7 | class WrappingLabel(Label):
8 | def __init__(self, master=None, **kwargs):
9 | Label.__init__(self, master, **kwargs)
10 | self.bind('', lambda e: self.config(wraplength=self.winfo_width()), '+')
11 |
12 |
13 | class RichLabel(Text):
14 | def __init__(self, *args, text="", textvariable=None, **kwargs):
15 | kwargs.setdefault("borderwidth", 0)
16 | kwargs.setdefault("relief", "flat")
17 | kwargs.setdefault("highlightthickness", 0)
18 | kwargs.setdefault("cursor", "arrow")
19 | kwargs["takefocus"] = False
20 |
21 | super().__init__(*args, **kwargs)
22 | self._textvariable = textvariable
23 |
24 | if "bg" not in kwargs:
25 | def _set_bg(event):
26 | parent = get_parent_with_bg(event.widget)
27 | if parent:
28 | self.configure(bg=parent.cget("bg"))
29 |
30 | self.bind("