From 8ad824c1105ca0198d0ec2912c47a621fddfc230 Mon Sep 17 00:00:00 2001 From: Sathya Pramodh <94102031+sathya-pramodh@users.noreply.github.com> Date: Tue, 14 Jun 2022 13:47:46 +0530 Subject: [PATCH] Added a logger class to create and manage log files internally. Added an auto-suspend functionality where the macro automatically suspends unused instances. Added a keybind to suspend and unsuspend instances manually as well. Changes to .gitignore and README.md. --- .github/ISSUE_TEMPLATE/bug_report.md | 3 + .gitignore | 1 + README.md | 10 +++ config.py | 12 +++ helper_scripts/logging.py | 72 ++++++++++++++++++ macro_handlers/instance_handlers.py | 28 +++++-- macro_handlers/reset_handlers.py | 12 ++- macro_handlers/suspend_handlers.py | 69 +++++++++++++++++ multi_instance.py | 109 +++++++++++++++++++++------ 9 files changed, 284 insertions(+), 32 deletions(-) create mode 100644 helper_scripts/logging.py create mode 100644 macro_handlers/suspend_handlers.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 483070d..cfa17e7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,3 +28,6 @@ If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. + +**Log Files** +Attach the necessary log files for the debugging process. diff --git a/.gitignore b/.gitignore index 749ccda..02b613b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.py[cod] *$py.class +log/ diff --git a/README.md b/README.md index bfe5f60..e70496c 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,18 @@ sudo python3 multi_instance.py ``` ## Some important instructions - This macro also assumes that you are using Atum and FastReset mods for Minecraft 1.16.1. It is hardcoded assuming so. +- Log files are located in the 'log' directory in the project script's main folder. So issues must be submitted with the relevant log files attached to them. +- The 'testing' branch is meant only for testing purposes and releases from that branch are created with the '-testing' tag at the end of them. + +# Contribution +- Code contributions can be made to the testing branch. Pull requests must be made with proper comments and documentation. +- The code must follow all PEP8 conventions and must be in python3.x ONLY. # Default Keybinds - `Ctrl+R` - Reset all instances (RSG) - `Shift+R` - Reset the current instance (RSG) +- `Ctrl+S` - Suspend all instances other than the active one. +- `Alt+S` - Unsuspend all instances. - `Ctrl+1` - Switch to Instance 1 - `Ctrl+2` - Switch to Instance 2 - `Ctrl+3` - Switch to Instance 3 @@ -105,3 +113,5 @@ sudo python3 multi_instance.py - SWITCH_INSTANCES - The list of keybinds (in order of instance number) to switch to that respective instance. - RESET_ALL_INSTANCES - The list of keybinds to reset all instances. - RESET_CURRENT_INSTANCE - The list of keybinds to reset the current instance. +- SUSPEND_ALL_INSTANCES - The list of keybinds for suspending instances other than the active instance. +- UNSUSPEND_ALL_INSTANCES - The list of keybinds for un-suspending all instances. diff --git a/config.py b/config.py index d071975..5076813 100644 --- a/config.py +++ b/config.py @@ -35,3 +35,15 @@ # Note: Do not use 'alt' instead of 'shift' here because the macro contains 'Tab' as a part of it and 'alt+tab' is already a keybind in most desktop environments. # The default is ["shift+r"] RESET_CURRENT_INSTANCE = ["shift+r"] + +# The list of keybinds for suspending instances other than the active instance. +# Each element of the list must adhere to the allowed values of the keyboard module, else the script will exit out with an error message. +# The allowed values of the keyboard module can be found at: https://github.com/boppreh/keyboard +# The default is ["ctrl+s"] +SUSPEND_ALL_INSTANCES = ["ctrl+s"] + +# The list of keybinds for un-suspending all instances. +# Each element of the list must adhere to the allowed values of the keyboard module, else the script will exit out with an error message. +# The allowed values of the keyboard module can be found at: https://github.com/boppreh/keyboard +# The default is ["alt+s"] +UNSUSPEND_ALL_INSTANCES = ["alt+s"] diff --git a/helper_scripts/logging.py b/helper_scripts/logging.py new file mode 100644 index 0000000..f93938d --- /dev/null +++ b/helper_scripts/logging.py @@ -0,0 +1,72 @@ +""" +This is the module that implements a logger as a class. + +Custom designed for the project(MultiInstanceLinux). +""" +# Author: sathya-pramodh +# Github: https://github.com/sathya-pramodh + +# Software Licensed under the MIT License. + +# License terms: + +# MIT License + +# Copyright (c) 2022 sathya-pramodh + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Imports +from datetime import datetime + + +class Logging: + """ + Custom implementation of a logging class. + """ + + def __init__(self, output_path): + """ + Initializer method for the class. + + output_path + The full path to the folder to which you want to output the logs to. + """ + self._path = output_path + self._session_log_file = ( + output_path + datetime.now().strftime("%Y-%m-%d") + ".log" + ) + + def log(self, message): + """ + Method to log a message to a log file. + + message + A string data type containing the message to be logged. + + Returns None + """ + with open(self._session_log_file, "a") as file: + write_string = ( + "\n" + + "[MultiInstanceLinux {}]".format(datetime.now().strftime("%H:%M:%S")) + + ": " + + message + ) + file.write(write_string) diff --git a/macro_handlers/instance_handlers.py b/macro_handlers/instance_handlers.py index 32fa961..c9c8d5b 100644 --- a/macro_handlers/instance_handlers.py +++ b/macro_handlers/instance_handlers.py @@ -38,18 +38,23 @@ import subprocess -def instance_switch_macro(instance_keybinds, keybind, hex_codes): +def instance_switch_macro(instance_keybinds, keybind, hex_codes, pids): """ Handles the switch instances macro. instance_keybinds - A list of keybinds as strings. + A list of keybinds as strings. + keybind - A string denoting the keybind pressed. + A string denoting the keybind pressed. + hex_codes - A list of the hex codes of the open Minecraft instances. + A list of the hex codes of the open Minecraft instances. + + pids + A dictionary of process IDs of the open Minecraft instances. - Returns None + Returns the hex code of the instance that was switched to for logging purposes. """ instance_number = instance_keybinds.index(keybind) @@ -58,9 +63,16 @@ def instance_switch_macro(instance_keybinds, keybind, hex_codes): ["xdotool", "getactivewindow"] ).decode("UTF-8") current_hex_code = hex(int(current_hex_code_in_base_ten)) - os.system("wmctrl -i -a " + current_hex_code) - time.sleep(0.25) - os.system("xdotool key --window " + current_hex_code + " Escape") + if len(current_hex_code.split("x")[1]) == 7: + current_hex_code = ( + current_hex_code.split("x")[0] + "x0" + current_hex_code.split("x")[1] + ) + for hex_code in hex_codes: + if hex_code != current_hex_code and hex_code != target_hex_code: + os.system("kill -STOP " + pids[hex_code]) + os.system("kill -CONT " + pids[target_hex_code]) os.system("wmctrl -i -a " + target_hex_code) time.sleep(0.25) os.system("xdotool key --window " + target_hex_code + " Escape") + + return target_hex_code diff --git a/macro_handlers/reset_handlers.py b/macro_handlers/reset_handlers.py index 7fe1d09..74dbeaf 100644 --- a/macro_handlers/reset_handlers.py +++ b/macro_handlers/reset_handlers.py @@ -35,17 +35,22 @@ import os -def reset_all_macro(hex_codes): +def reset_all_macro(hex_codes, pids): """ Handles the resetting macro on all instances. It is hardcoded for Minecraft 1.16.1. hex_codes - A list of the hex codes of the open Minecraft Instances. + A list of the hex codes of the open Minecraft Instances. + + pids + A list of PIDs of all the open instances. Returns None """ macro = " Tab+" * 8 + "Enter" + for pid in pids.values(): + os.system("kill -CONT " + pid) current_hex_code_in_base_ten = subprocess.check_output( ["xdotool", "getwindowfocus"] ).decode("UTF-8") @@ -66,7 +71,7 @@ def reset_current_macro(): Handles the reset instance macro for the current window in focus. It is hardcoded for Minecraft 1.16.1. - Returns None + Returns the hex code of the instance that was reset for logging purposes """ macro = " Tab+" * 8 + "Enter" hex_code_in_base_ten = subprocess.check_output( @@ -77,3 +82,4 @@ def reset_current_macro(): time.sleep(0.25) os.system("xdotool key --window " + hex_code + " Escape") os.system("xdotool key --window " + hex_code + macro) + return hex_code diff --git a/macro_handlers/suspend_handlers.py b/macro_handlers/suspend_handlers.py new file mode 100644 index 0000000..fa3af9f --- /dev/null +++ b/macro_handlers/suspend_handlers.py @@ -0,0 +1,69 @@ +""" +This is a helper script that handles the suspend keybinds. +Called from handle_instance_keybinds() in multi_instance.py +""" +# Author: sathya-pramodh +# Github: https://github.com/sathya-pramodh +# Software licensed under the MIT license. +# License Terms: + +# MIT License + +# Copyright (c) 2022 sathya-pramodh + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import os +import subprocess + + +def suspend_all_macro(pids): + """ + The function that handles suspending all instances other than the current active instance. + + pids + A list of the PIDs of all the open instances. + + Returns the hex code of the current active instance for logging purposes. + """ + current_hex_code_in_base_ten = subprocess.check_output( + ["xdotool", "getwindowfocus"] + ).decode("UTF-8") + current_hex_code = hex(int(current_hex_code_in_base_ten)) + if len(current_hex_code.split("x")[1]) == 7: + current_hex_code = ( + current_hex_code.split("x")[0] + "x0" + current_hex_code.split("x")[1] + ) + for hex_code, pid in pids.items(): + if hex_code != current_hex_code: + os.system("kill -STOP " + pid) + + return current_hex_code + + +def unsuspend_all_macro(pids): + """ + The function that handles unsuspending all instances other than the current active instance. + + pids + A list of PIDs of all the open instances. + + Returns the hex code of the current active instance for logging purposes. + """ + for hex_code, pid in pids.items(): + os.system("kill -CONT " + pid) diff --git a/multi_instance.py b/multi_instance.py index 62ab858..80a9f89 100644 --- a/multi_instance.py +++ b/multi_instance.py @@ -35,18 +35,22 @@ # SOFTWARE. # Imports +import os +import time import keyboard import macro_handlers.instance_handlers as switch_macro import macro_handlers.reset_handlers as reset_macro +import macro_handlers.suspend_handlers as suspend_macro import config import subprocess +from helper_scripts.logging import Logging def get_hex_codes(): """ - Gets the hex codes of the open Minecraft instances by executing the command "wmctrl -l". + Gets the hex codes of the open Minecraft instances. - Returns the list of hex codes. + Returns the list of hex codes """ processes = subprocess.check_output(["wmctrl", "-l"]).decode("UTF-8").split("\n") hex_codes = [] @@ -57,35 +61,72 @@ def get_hex_codes(): return hex_codes -def handle_instance_keybinds(hex_codes): +def get_process_ids(hex_codes): + """ + Gets the process IDs of the open Minecraft instances. + + Returns the list of the process IDs + """ + pids = {} + for hex_code in hex_codes: + pid = ( + subprocess.check_output(["xdotool", "getwindowpid", hex_code]) + .decode("UTF-8") + .strip() + ) + pids[hex_code] = pid + + return pids + + +def handle_instance_keybinds(hex_codes, pids): """ Handles the instance keybinds defined in config.py. hex_codes - A list of the corresponding hex codes of the open instances. + A list of the corresponding hex codes of the open instances. + + pids + A dictionary of the pids of the open instances. Returns None """ instance_keybinds = config.SWITCH_INSTANCES reset_all_keybinds = config.RESET_ALL_INSTANCES reset_one_keybinds = config.RESET_CURRENT_INSTANCE + suspend_all_keybinds = config.SUSPEND_ALL_INSTANCES + unsuspend_all_keybinds = config.UNSUSPEND_ALL_INSTANCES while True: for instance_keybind in instance_keybinds: if keyboard.is_pressed(instance_keybind): - switch_macro.instance_switch_macro( - instance_keybinds, instance_keybind, hex_codes + hex_code = switch_macro.instance_switch_macro( + instance_keybinds, instance_keybind, hex_codes, pids ) - print("Debug Info: Switched to respective instance.") + logger.log("Switched to instance with hex code: {}".format(hex_code)) for reset_all_keybind in reset_all_keybinds: if keyboard.is_pressed(reset_all_keybind): - reset_macro.reset_all_macro(hex_codes) - print("Debug Info: All instances reset.") + reset_macro.reset_all_macro(hex_codes, pids) + logger.log("All instances were reset.") for reset_one_keybind in reset_one_keybinds: if keyboard.is_pressed(reset_one_keybind): - reset_macro.reset_current_macro() - print("Debug Info: Instance was reset.") + hex_code = reset_macro.reset_current_macro() + logger.log("Instance with hex code: {} was reset.".format(hex_code)) + + for suspend_all_keybind in suspend_all_keybinds: + if keyboard.is_pressed(suspend_all_keybind): + hex_code = suspend_macro.suspend_all_macro(pids) + logger.log( + "All instances other than the instance with hex code: {} were suspended.".format( + hex_code + ) + ) + + for unsuspend_all_keybind in unsuspend_all_keybinds: + if keyboard.is_pressed(unsuspend_all_keybind): + suspend_macro.unsuspend_all_macro(pids) + logger.log("All instances were unsuspended.") def main(): @@ -96,31 +137,57 @@ def main(): """ try: if config.NUM_INSTANCES > 9 or config.NUM_INSTANCES < 2: - print( - "The number of instances should be greater than 1 and lesser than 9. Please make necessary changes to the config file." + logger.log( + "The number of instances should be greater than 1 and lesser than 9. Detected number of instances: {}".format( + config.NUM_INSTANCES + ) ) return -1 hex_codes = get_hex_codes() - print("Debug Info: Hex codes of the windows obtained.") + logger.log("Hex codes of the windows obtained. Hex Codes: {}".format(hex_codes)) + pids = get_process_ids(hex_codes) + logger.log("PIDs of the windows obtained. PIDs: {}".format(pids)) if len(hex_codes) != config.NUM_INSTANCES: - print( - "Some instances are not open. Please check if your instances are open." + logger.log( + "Some instances are not open. Number of instances detected: {}".format( + len(hex_codes) + ) ) return -1 - handle_instance_keybinds(hex_codes) + handle_instance_keybinds(hex_codes, pids) except KeyboardInterrupt: return 0 # Checking if the script has been imported from an external script. if __name__ == "__main__": + script_start_time = time.time() + WORKING_DIRECTORY = os.getcwd() + if not os.path.isdir("{}/log/".format(WORKING_DIRECTORY)): + os.mkdir("{}/log/".format(WORKING_DIRECTORY)) + logger = Logging("{}/log/".format(WORKING_DIRECTORY)) return_code = main() - # Printing proper debug info based on the return code. if return_code == 0: - print("Debug Info: Script exitted successfully.") - elif return_code == -1: - print("Debug Info: Error occured in script. Exitting...") + script_end_time = time.time() + time_taken_by_script = script_end_time - script_start_time + logger.log( + "Script exitted successfully. Exitted with code 0. Time taken for execution: {} seconds".format( + time_taken_by_script + ) + ) + + if return_code == -1: + script_end_time = time.time() + time_taken_by_script = script_end_time - script_start_time + logger.log( + "Script exitted with an error. Exitted with code -1. Time take for execution: {} seconds".format( + time_taken_by_script + ) + ) + print( + "Some error occurred in the script. Please check the log files for more information. Exit code(-1)." + )