From 4159a39de53d2997a0a5cf782969ed83d8c090c4 Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Thu, 3 Sep 2026 11:01:21 +0200 Subject: [PATCH 01/11] add convenience script to start/stop the bot --- bot | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100755 bot diff --git a/bot b/bot new file mode 100755 index 00000000..1ac71a23 --- /dev/null +++ b/bot @@ -0,0 +1,84 @@ +#!/bin/bash + +red="$(tput setaf 1)" +blue="$(tput setaf 4)" +green="$(tput setaf 2)" +reset="$(tput sgr0)" + +export PYTHONUNBUFFERED=1 + +JOB_MANAGER="python3 -m eessi_bot_job_manager" +EVENT_HANDLER="python3 -m eessi_bot_event_handler" + +start_bot() { + if pgrep -f "$JOB_MANAGER" >/dev/null; then + echo "$blue>>> job manager is already running$reset" + else + echo "$blue>>> starting job manager...$reset" + $JOB_MANAGER \ + | sed -e '1s/^/\n/' -e "s/\(.*\)/$blue\1$reset/" & + fi + + if pgrep -f "$EVENT_HANDLER" >/dev/null; then + echo "$green>>> event handler is already running$reset" + else + echo "$green>>> starting event handler...$reset" + $EVENT_HANDLER \ + | sed -e '1s/^/\n/' -e "s/\(.*\)/$green\1$reset/" & + fi +} + +stop_bot() { + if pgrep -f "$EVENT_HANDLER" >/dev/null; then + echo "$red>>> stopping event handler...$reset" + pkill -e -f "$EVENT_HANDLER" + else + echo "$red>>> event handler is not running$reset" + fi + + if pgrep -f "$JOB_MANAGER" >/dev/null; then + echo "$red>>> stopping job manager...$reset" + pkill -e -f "$JOB_MANAGER" + else + echo "$red>>> job manager is not running$reset" + fi +} + +status_bot() { + if pgrep -f "$JOB_MANAGER" >/dev/null; then + echo "$blue>>> job manager is running$reset" + else + echo "$red>>> job manager is not running$reset" + fi + + if pgrep -f "$EVENT_HANDLER" >/dev/null; then + echo "$green>>> event handler is running$reset" + else + echo "$red>>> event handler is not running$reset" + fi +} + +case "$1" in + start) + start_bot + ;; + + stop) + stop_bot + ;; + + restart) + stop_bot + sleep 1 + start_bot + ;; + + status) + status_bot + ;; + + *) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 + ;; +esac From 7e2e065928a7a0fdb9cfd5df88feadbfd7920c2f Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Thu, 3 Sep 2026 22:45:33 +0200 Subject: [PATCH 02/11] add bot options and update readme --- README.md | 19 +++++++++++++++++++ bot | 41 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6cfc2628..08e79647 100644 --- a/README.md +++ b/README.md @@ -1409,6 +1409,25 @@ The job manager writes log information to the file `eessi_bot_job_manager.log`. The job manager can run on a different machine than the event handler, as long as both have access to the same shared filesystem. +## Step 7.3: Managing the bot with a single command + +The `bot` script starts, stops, restarts, and checks the status of the 2 bot components: the event handler and the job manager. + +```bash +./bot start [options] +./bot stop +./bot restart [options] +./bot status +``` + +Available `start`/`restart` options are the same as those for `event_handler.sh` and `job_manager.sh`. + +The following example starts the bot with 10 job manager iterations, managing only job ids 1234 and 5678, and listening for events on port 8080: + +```bash +./bot start -i 10 -j 1234,5678 --port 8080 +``` + # Example pull request on software-layer For information on how to make pull requests and let the bot build software, see diff --git a/bot b/bot index 1ac71a23..530af5d7 100755 --- a/bot +++ b/bot @@ -11,11 +11,37 @@ JOB_MANAGER="python3 -m eessi_bot_job_manager" EVENT_HANDLER="python3 -m eessi_bot_event_handler" start_bot() { + local manager_args=() + local handler_args=() + + shift + + while [[ $# -gt 0 ]]; do + case "$1" in + -i|--max-manager-iterations) + manager_args+=("$1" "$2") + shift 2 + ;; + -j|--jobs) + manager_args+=("$1" "$2") + shift 2 + ;; + --port) + handler_args+=("$1" "$2") + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac + done + if pgrep -f "$JOB_MANAGER" >/dev/null; then echo "$blue>>> job manager is already running$reset" else echo "$blue>>> starting job manager...$reset" - $JOB_MANAGER \ + $JOB_MANAGER "${manager_args[@]}" \ | sed -e '1s/^/\n/' -e "s/\(.*\)/$blue\1$reset/" & fi @@ -23,7 +49,7 @@ start_bot() { echo "$green>>> event handler is already running$reset" else echo "$green>>> starting event handler...$reset" - $EVENT_HANDLER \ + $EVENT_HANDLER "${handler_args[@]}" \ | sed -e '1s/^/\n/' -e "s/\(.*\)/$green\1$reset/" & fi } @@ -60,7 +86,7 @@ status_bot() { case "$1" in start) - start_bot + start_bot "$@" ;; stop) @@ -70,7 +96,7 @@ case "$1" in restart) stop_bot sleep 1 - start_bot + start_bot "$@" ;; status) @@ -78,7 +104,12 @@ case "$1" in ;; *) - echo "Usage: $0 {start|stop|restart|status}" + echo "Usage: $0 {start|stop|restart|status} [options]" + echo + echo "Start/restart options:" + echo " -i, --max-manager-iterations N Job manager max iterations" + echo " -j, --jobs N Comma-separated list of job ids" + echo " --port PORT Event handler port" exit 1 ;; esac From f4406f5c8cbc27fff2dfd91cac6bb370e761161e Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Thu, 3 Sep 2026 22:51:02 +0200 Subject: [PATCH 03/11] add license --- bot | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bot b/bot index 530af5d7..fc8cd373 100755 --- a/bot +++ b/bot @@ -1,4 +1,15 @@ #!/bin/bash +# +# This file is part of the EESSI build-and-deploy bot, +# see https://github.com/EESSI/eessi-bot-software-layer +# +# The bot helps with requests to add software installations to the +# EESSI software layer, see https://github.com/EESSI/software-layer +# +# author: Samuel Moors (@smoors) +# +# license: GPLv2 +# red="$(tput setaf 1)" blue="$(tput setaf 4)" From 47debafb0677e4b7ff00f6c805e8eda605eb9e0e Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Fri, 4 Sep 2026 11:08:40 +0200 Subject: [PATCH 04/11] small update readme --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 08e79647..9789e1ff 100644 --- a/README.md +++ b/README.md @@ -1411,7 +1411,9 @@ The job manager can run on a different machine than the event handler, as long a ## Step 7.3: Managing the bot with a single command -The `bot` script starts, stops, restarts, and checks the status of the 2 bot components: the event handler and the job manager. +The `bot` script starts, stops, restarts, and checks the status of the 2 bot +components: the event handler and the job manager. It launches both component +processes in the background. ```bash ./bot start [options] @@ -1420,9 +1422,11 @@ The `bot` script starts, stops, restarts, and checks the status of the 2 bot com ./bot status ``` -Available `start`/`restart` options are the same as those for `event_handler.sh` and `job_manager.sh`. +Available `start`/`restart` options are the same as those for +`event_handler.sh` and `job_manager.sh`. -The following example starts the bot with 10 job manager iterations, managing only job ids 1234 and 5678, and listening for events on port 8080: +The following example starts the bot with 10 job manager iterations, managing +only job ids 1234 and 5678, and listening for events on port 8080: ```bash ./bot start -i 10 -j 1234,5678 --port 8080 From 722235dd769a553b979197e2b5cc0d322c52972e Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Sat, 5 Sep 2026 21:50:35 +0200 Subject: [PATCH 05/11] convert to python and support multiple instances --- bot.py | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++ tools/args.py | 10 +++ 2 files changed, 187 insertions(+) create mode 100755 bot.py diff --git a/bot.py b/bot.py new file mode 100755 index 00000000..1b372cba --- /dev/null +++ b/bot.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# +# This file is part of the EESSI build-and-deploy bot, +# see https://github.com/EESSI/eessi-bot-software-layer +# +# The bot helps with requests to add software installations to the +# EESSI software layer, see https://github.com/EESSI/software-layer +# +# author: Samuel Moors (@smoors) +# +# license: GPLv2 +# + +import argparse +import os +import shlex +import signal +import subprocess +import time +from pathlib import Path + +RED = "\033[31m" +BLUE = "\033[34m" +GREEN = "\033[32m" +RESET = "\033[0m" + +JOB_MANAGER_MODULE = "eessi_bot_job_manager" +EVENT_HANDLER_MODULE = "eessi_bot_event_handler" +DEFAULT_BOT_NAME = "eessi-bot" + + +def parse_args(): + parser = argparse.ArgumentParser( + usage="%(prog)s {start|stop|restart|status} [options]" + ) + parser.add_argument("command", choices=("start", "stop", "restart", "status")) + parser.add_argument( + "--bot-name", + default=DEFAULT_BOT_NAME, + help=f"bot name (default: {DEFAULT_BOT_NAME})", + ) + parser.add_argument( + "--job-manager-opts", + default="", + help="additional job manager options as a string", + ) + parser.add_argument( + "--event-handler-opts", + default="", + help="additional event handler options as a string", + ) + args = parser.parse_args() + + return args + + +def get_cmdline(pid): + try: + data = Path(f"/proc/{pid}/cmdline").read_bytes() + except (FileNotFoundError, PermissionError, ProcessLookupError): + return [] + return [arg.decode(errors="replace") for arg in data.split(b"\0") if arg] + + +def find_processes(module, bot_name): + processes = [] + + for proc in Path("/proc").iterdir(): + if not proc.name.isdigit(): + continue + + pid = int(proc.name) + if pid == os.getpid(): + continue + + cmdline = get_cmdline(pid) + if not cmdline: + continue + + try: + module_index = cmdline.index("-m") + except ValueError: + continue + + if module_index + 1 >= len(cmdline) or cmdline[module_index + 1] != module: + continue + + for i, arg in enumerate(cmdline[:-1]): + if arg == "--bot-name" and cmdline[i + 1] == bot_name: + processes.append(pid) + break + + return processes + + +def start_process(module, bot_name, opts): + cmd = ["python3", "-m", module, "--bot-name", bot_name] + if opts: + cmd.extend(shlex.split(opts)) + + env = os.environ.copy() + env["PYTHONUNBUFFERED"] = "1" + + subprocess.Popen(cmd, env=env, start_new_session=True) + + +def start_bot(bot_name, job_manager_opts, event_handler_opts): + if find_processes(JOB_MANAGER_MODULE, bot_name): + print(f"{BLUE}>>> job manager for bot '{bot_name}' is already running{RESET}") + else: + print(f"{BLUE}>>> starting job manager for bot '{bot_name}'...{RESET}") + start_process(JOB_MANAGER_MODULE, bot_name, job_manager_opts) + + if find_processes(EVENT_HANDLER_MODULE, bot_name): + print(f"{GREEN}>>> event handler for bot '{bot_name}' is already running{RESET}") + else: + print(f"{GREEN}>>> starting event handler for bot '{bot_name}'...{RESET}") + start_process(EVENT_HANDLER_MODULE, bot_name, event_handler_opts) + + +def stop_processes(module, bot_name): + for pid in find_processes(module, bot_name): + try: + cmdline = get_cmdline(pid) + os.kill(pid, signal.SIGTERM) + print(f"killed (pid {pid}): {shlex.join(cmdline)}") + except ProcessLookupError: + pass + + +def stop_bot(bot_name): + if find_processes(EVENT_HANDLER_MODULE, bot_name): + print(f"{RED}>>> stopping event handler for bot '{bot_name}'...{RESET}") + stop_processes(EVENT_HANDLER_MODULE, bot_name) + else: + print(f"{RED}>>> event handler for bot '{bot_name}' is not running{RESET}") + + if find_processes(JOB_MANAGER_MODULE, bot_name): + print(f"{RED}>>> stopping job manager for bot '{bot_name}'...{RESET}") + stop_processes(JOB_MANAGER_MODULE, bot_name) + else: + print(f"{RED}>>> job manager for bot '{bot_name}' is not running{RESET}") + + +def status_bot(bot_name): + if find_processes(JOB_MANAGER_MODULE, bot_name): + print(f"{BLUE}>>> job manager for bot '{bot_name}' is running{RESET}") + else: + print(f"{RED}>>> job manager for bot '{bot_name}' is not running{RESET}") + + if find_processes(EVENT_HANDLER_MODULE, bot_name): + print(f"{GREEN}>>> event handler for bot '{bot_name}' is running{RESET}") + else: + print(f"{RED}>>> event handler for bot '{bot_name}' is not running{RESET}") + + +def main(): + args = parse_args() + + if args.command == "start": + start_bot(args.bot_name, args.job_manager_opts, args.event_handler_opts) + time.sleep(2) + status_bot(args.bot_name) + elif args.command == "stop": + stop_bot(args.bot_name) + elif args.command == "restart": + stop_bot(args.bot_name) + time.sleep(1) + start_bot(args.bot_name, args.job_manager_opts, args.event_handler_opts) + time.sleep(2) + status_bot(args.bot_name) + elif args.command == "status": + status_bot(args.bot_name) + + +if __name__ == "__main__": + main() diff --git a/tools/args.py b/tools/args.py index 27b62ab5..764350df 100644 --- a/tools/args.py +++ b/tools/args.py @@ -85,6 +85,11 @@ def event_handler_parse(args=None): help="listen on a specific port for events (default 3000)", ) + parser.add_argument( + "--bot-name", + help="bot name (not used by the bot itself), useful for keeping track of multiple bots", + ) + return parser.parse_args(args=unknown_args, namespace=parsed_args) @@ -110,4 +115,9 @@ def job_manager_parse(args=None): help="limits the processing to a specific job id or list of comma-separated list of job ids", ) + parser.add_argument( + "--bot-name", + help="bot name (not used by the bot itself), useful for keeping track of multiple bots", + ) + return parser.parse_args(args=unknown_args, namespace=parsed_args) From d393902b01a5a07dcd0c1254b98bbd7bf9122e48 Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Sat, 5 Sep 2026 21:52:43 +0200 Subject: [PATCH 06/11] update --- tools/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/args.py b/tools/args.py index 764350df..4d256f51 100644 --- a/tools/args.py +++ b/tools/args.py @@ -87,7 +87,7 @@ def event_handler_parse(args=None): parser.add_argument( "--bot-name", - help="bot name (not used by the bot itself), useful for keeping track of multiple bots", + help="bot name (not used by the bot itself), useful for keeping track of multiple instances", ) return parser.parse_args(args=unknown_args, namespace=parsed_args) @@ -117,7 +117,7 @@ def job_manager_parse(args=None): parser.add_argument( "--bot-name", - help="bot name (not used by the bot itself), useful for keeping track of multiple bots", + help="bot name (not used by the bot itself), useful for keeping track of multiple instances", ) return parser.parse_args(args=unknown_args, namespace=parsed_args) From 3f9aacc0ed2a0a0734769c89cce20ab6646a9110 Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Sat, 5 Sep 2026 22:03:39 +0200 Subject: [PATCH 07/11] fix tests --- tests/test_tools_args.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_tools_args.py b/tests/test_tools_args.py index a0db511c..ebba364b 100644 --- a/tests/test_tools_args.py +++ b/tests/test_tools_args.py @@ -44,15 +44,16 @@ def test_parse_common_args(test_args, expected_parsed, expected_unknown): # Test event_handler_parse() @pytest.mark.parametrize("test_args,expectation", [ # No args - ([], nullcontext(Namespace(debug=False, build=False, test=False, cron=False, file=None, port=3000))), + ([], nullcontext(Namespace(debug=False, build=False, test=False, cron=False, file=None, port=3000, bot_name=None))), # Short-form args (["-d", "-b", "-t", "-c", "-f", "file.json", "-p", "8000"], nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file.json", port="8000"))), # Long-form args - (["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000"], - nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file2.json", port="9000"))), + (["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000", "--bot-name", "test-bot"], + nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file2.json", port="9000", + bot_name="test-bot"))), # Unknown args - should fail and exit (["-u"], pytest.raises(SystemExit)), @@ -66,15 +67,15 @@ def test_event_handler_parse_known_args(test_args, expectation): # Test job_manager_parse() @pytest.mark.parametrize("test_args,expectation", [ # No args - ([], nullcontext(Namespace(debug=False, max_manager_iterations=-1, jobs=None))), + ([], nullcontext(Namespace(debug=False, max_manager_iterations=-1, jobs=None, bot_name=None))), # Short-form args (["-d", "-i", "0", "-j", "17"], nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17"))), # Long-form args - (["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48"], - nullcontext(Namespace(debug=True, max_manager_iterations="10", jobs="4,18,48"))), + (["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48", "--bot-name", "test-bot"], + nullcontext(Namespace(debug=True, max_manager_iterations="10", jobs="4,18,48", bot_name="test-bot"))), # Unknown args - should fail and exit (["-u"], pytest.raises(SystemExit)), From 217affd270c66c745a4fc78c4706b5a805a08705 Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Sat, 5 Sep 2026 22:11:38 +0200 Subject: [PATCH 08/11] more test fixes --- tests/test_tools_args.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_tools_args.py b/tests/test_tools_args.py index ebba364b..8fbd0727 100644 --- a/tests/test_tools_args.py +++ b/tests/test_tools_args.py @@ -48,7 +48,8 @@ def test_parse_common_args(test_args, expected_parsed, expected_unknown): # Short-form args (["-d", "-b", "-t", "-c", "-f", "file.json", "-p", "8000"], - nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file.json", port="8000"))), + nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file.json", port="8000", + bot_name=None))), # Long-form args (["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000", "--bot-name", "test-bot"], @@ -71,7 +72,7 @@ def test_event_handler_parse_known_args(test_args, expectation): # Short-form args (["-d", "-i", "0", "-j", "17"], - nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17"))), + nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17", bot_name=None))), # Long-form args (["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48", "--bot-name", "test-bot"], From 7e1274073c14fe2eb1da333548b3f50060f570b0 Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Sat, 5 Sep 2026 22:19:05 +0200 Subject: [PATCH 09/11] remove bash script --- bot | 126 ------------------------------------------------------------ 1 file changed, 126 deletions(-) delete mode 100755 bot diff --git a/bot b/bot deleted file mode 100755 index fc8cd373..00000000 --- a/bot +++ /dev/null @@ -1,126 +0,0 @@ -#!/bin/bash -# -# This file is part of the EESSI build-and-deploy bot, -# see https://github.com/EESSI/eessi-bot-software-layer -# -# The bot helps with requests to add software installations to the -# EESSI software layer, see https://github.com/EESSI/software-layer -# -# author: Samuel Moors (@smoors) -# -# license: GPLv2 -# - -red="$(tput setaf 1)" -blue="$(tput setaf 4)" -green="$(tput setaf 2)" -reset="$(tput sgr0)" - -export PYTHONUNBUFFERED=1 - -JOB_MANAGER="python3 -m eessi_bot_job_manager" -EVENT_HANDLER="python3 -m eessi_bot_event_handler" - -start_bot() { - local manager_args=() - local handler_args=() - - shift - - while [[ $# -gt 0 ]]; do - case "$1" in - -i|--max-manager-iterations) - manager_args+=("$1" "$2") - shift 2 - ;; - -j|--jobs) - manager_args+=("$1" "$2") - shift 2 - ;; - --port) - handler_args+=("$1" "$2") - shift 2 - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac - done - - if pgrep -f "$JOB_MANAGER" >/dev/null; then - echo "$blue>>> job manager is already running$reset" - else - echo "$blue>>> starting job manager...$reset" - $JOB_MANAGER "${manager_args[@]}" \ - | sed -e '1s/^/\n/' -e "s/\(.*\)/$blue\1$reset/" & - fi - - if pgrep -f "$EVENT_HANDLER" >/dev/null; then - echo "$green>>> event handler is already running$reset" - else - echo "$green>>> starting event handler...$reset" - $EVENT_HANDLER "${handler_args[@]}" \ - | sed -e '1s/^/\n/' -e "s/\(.*\)/$green\1$reset/" & - fi -} - -stop_bot() { - if pgrep -f "$EVENT_HANDLER" >/dev/null; then - echo "$red>>> stopping event handler...$reset" - pkill -e -f "$EVENT_HANDLER" - else - echo "$red>>> event handler is not running$reset" - fi - - if pgrep -f "$JOB_MANAGER" >/dev/null; then - echo "$red>>> stopping job manager...$reset" - pkill -e -f "$JOB_MANAGER" - else - echo "$red>>> job manager is not running$reset" - fi -} - -status_bot() { - if pgrep -f "$JOB_MANAGER" >/dev/null; then - echo "$blue>>> job manager is running$reset" - else - echo "$red>>> job manager is not running$reset" - fi - - if pgrep -f "$EVENT_HANDLER" >/dev/null; then - echo "$green>>> event handler is running$reset" - else - echo "$red>>> event handler is not running$reset" - fi -} - -case "$1" in - start) - start_bot "$@" - ;; - - stop) - stop_bot - ;; - - restart) - stop_bot - sleep 1 - start_bot "$@" - ;; - - status) - status_bot - ;; - - *) - echo "Usage: $0 {start|stop|restart|status} [options]" - echo - echo "Start/restart options:" - echo " -i, --max-manager-iterations N Job manager max iterations" - echo " -j, --jobs N Comma-separated list of job ids" - echo " --port PORT Event handler port" - exit 1 - ;; -esac From a22b1c046d9cc03d767806eb178d42484ec4ee7b Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Sun, 6 Sep 2026 16:07:54 +0200 Subject: [PATCH 10/11] use --instance and update readme --- README.md | 26 +++++++----- bot.py | 92 ++++++++++++++++++++-------------------- tests/test_tools_args.py | 16 +++---- tools/args.py | 10 +++-- 4 files changed, 75 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 9789e1ff..65bc25f9 100644 --- a/README.md +++ b/README.md @@ -1411,25 +1411,31 @@ The job manager can run on a different machine than the event handler, as long a ## Step 7.3: Managing the bot with a single command -The `bot` script starts, stops, restarts, and checks the status of the 2 bot +The `bot.py` script starts, stops, restarts, and checks the status of the 2 bot components: the event handler and the job manager. It launches both component processes in the background. ```bash -./bot start [options] -./bot stop -./bot restart [options] -./bot status +./bot.py start [options] +./bot.py stop [options] +./bot.py restart [options] +./bot.py status [options] ``` -Available `start`/`restart` options are the same as those for -`event_handler.sh` and `job_manager.sh`. +Available options: -The following example starts the bot with 10 job manager iterations, managing -only job ids 1234 and 5678, and listening for events on port 8080: +|Option|Argument| +|------|--------| +|`-e` / `--event-handler-opts`|String of options that will be passed to the event manager| +|`-i` / `--instance`|Identifies the bot instance to start, stop, restart, or check (default is `eessi-bot`)| +|`-j` / `--job-manager-opts`|String of options that will be passed to the job manager| + +The following example starts bot instance `test-bot` with 10 job manager +iterations, managing only job ids 1234 and 5678, and listening for events on +port 8080: ```bash -./bot start -i 10 -j 1234,5678 --port 8080 +./bot.py start --instance test-bot --job-manager-opts "-i 10 -j 1234,5678" --event-handler-opts "--port 8080" ``` # Example pull request on software-layer diff --git a/bot.py b/bot.py index 1b372cba..5848bc97 100755 --- a/bot.py +++ b/bot.py @@ -26,7 +26,7 @@ JOB_MANAGER_MODULE = "eessi_bot_job_manager" EVENT_HANDLER_MODULE = "eessi_bot_event_handler" -DEFAULT_BOT_NAME = "eessi-bot" +DEFAULT_INSTANCE = "eessi-bot" def parse_args(): @@ -35,23 +35,21 @@ def parse_args(): ) parser.add_argument("command", choices=("start", "stop", "restart", "status")) parser.add_argument( - "--bot-name", - default=DEFAULT_BOT_NAME, - help=f"bot name (default: {DEFAULT_BOT_NAME})", + "-i", "--instance", + default=DEFAULT_INSTANCE, + help=f"bot instance (default: {DEFAULT_INSTANCE})", ) parser.add_argument( - "--job-manager-opts", + "-j", "--job-manager-opts", default="", help="additional job manager options as a string", ) parser.add_argument( - "--event-handler-opts", + "-e", "--event-handler-opts", default="", help="additional event handler options as a string", ) - args = parser.parse_args() - - return args + return parser.parse_args() def get_cmdline(pid): @@ -62,7 +60,7 @@ def get_cmdline(pid): return [arg.decode(errors="replace") for arg in data.split(b"\0") if arg] -def find_processes(module, bot_name): +def find_processes(module, instance): processes = [] for proc in Path("/proc").iterdir(): @@ -86,15 +84,15 @@ def find_processes(module, bot_name): continue for i, arg in enumerate(cmdline[:-1]): - if arg == "--bot-name" and cmdline[i + 1] == bot_name: + if arg == "--instance" and cmdline[i + 1] == instance: processes.append(pid) break return processes -def start_process(module, bot_name, opts): - cmd = ["python3", "-m", module, "--bot-name", bot_name] +def start_process(module, instance, opts): + cmd = ["python3", "-m", module, "--instance", instance] if opts: cmd.extend(shlex.split(opts)) @@ -104,22 +102,22 @@ def start_process(module, bot_name, opts): subprocess.Popen(cmd, env=env, start_new_session=True) -def start_bot(bot_name, job_manager_opts, event_handler_opts): - if find_processes(JOB_MANAGER_MODULE, bot_name): - print(f"{BLUE}>>> job manager for bot '{bot_name}' is already running{RESET}") +def start_bot(instance, job_manager_opts, event_handler_opts): + if find_processes(JOB_MANAGER_MODULE, instance): + print(f"{BLUE}>>> job manager for bot instance '{instance}' is already running{RESET}") else: - print(f"{BLUE}>>> starting job manager for bot '{bot_name}'...{RESET}") - start_process(JOB_MANAGER_MODULE, bot_name, job_manager_opts) + print(f"{BLUE}>>> starting job manager for bot instance '{instance}'...{RESET}") + start_process(JOB_MANAGER_MODULE, instance, job_manager_opts) - if find_processes(EVENT_HANDLER_MODULE, bot_name): - print(f"{GREEN}>>> event handler for bot '{bot_name}' is already running{RESET}") + if find_processes(EVENT_HANDLER_MODULE, instance): + print(f"{GREEN}>>> event handler for bot instance '{instance}' is already running{RESET}") else: - print(f"{GREEN}>>> starting event handler for bot '{bot_name}'...{RESET}") - start_process(EVENT_HANDLER_MODULE, bot_name, event_handler_opts) + print(f"{GREEN}>>> starting event handler for bot instance '{instance}'...{RESET}") + start_process(EVENT_HANDLER_MODULE, instance, event_handler_opts) -def stop_processes(module, bot_name): - for pid in find_processes(module, bot_name): +def stop_processes(module, instance): + for pid in find_processes(module, instance): try: cmdline = get_cmdline(pid) os.kill(pid, signal.SIGTERM) @@ -128,49 +126,49 @@ def stop_processes(module, bot_name): pass -def stop_bot(bot_name): - if find_processes(EVENT_HANDLER_MODULE, bot_name): - print(f"{RED}>>> stopping event handler for bot '{bot_name}'...{RESET}") - stop_processes(EVENT_HANDLER_MODULE, bot_name) +def stop_bot(instance): + if find_processes(EVENT_HANDLER_MODULE, instance): + print(f"{RED}>>> stopping event handler for bot instance '{instance}'...{RESET}") + stop_processes(EVENT_HANDLER_MODULE, instance) else: - print(f"{RED}>>> event handler for bot '{bot_name}' is not running{RESET}") + print(f"{RED}>>> event handler for bot instance '{instance}' is not running{RESET}") - if find_processes(JOB_MANAGER_MODULE, bot_name): - print(f"{RED}>>> stopping job manager for bot '{bot_name}'...{RESET}") - stop_processes(JOB_MANAGER_MODULE, bot_name) + if find_processes(JOB_MANAGER_MODULE, instance): + print(f"{RED}>>> stopping job manager for bot instance '{instance}'...{RESET}") + stop_processes(JOB_MANAGER_MODULE, instance) else: - print(f"{RED}>>> job manager for bot '{bot_name}' is not running{RESET}") + print(f"{RED}>>> job manager for bot instance '{instance}' is not running{RESET}") -def status_bot(bot_name): - if find_processes(JOB_MANAGER_MODULE, bot_name): - print(f"{BLUE}>>> job manager for bot '{bot_name}' is running{RESET}") +def status_bot(instance): + if find_processes(JOB_MANAGER_MODULE, instance): + print(f"{BLUE}>>> job manager for bot instance '{instance}' is running{RESET}") else: - print(f"{RED}>>> job manager for bot '{bot_name}' is not running{RESET}") + print(f"{RED}>>> job manager for bot instance '{instance}' is not running{RESET}") - if find_processes(EVENT_HANDLER_MODULE, bot_name): - print(f"{GREEN}>>> event handler for bot '{bot_name}' is running{RESET}") + if find_processes(EVENT_HANDLER_MODULE, instance): + print(f"{GREEN}>>> event handler for bot instance '{instance}' is running{RESET}") else: - print(f"{RED}>>> event handler for bot '{bot_name}' is not running{RESET}") + print(f"{RED}>>> event handler for bot instance '{instance}' is not running{RESET}") def main(): args = parse_args() if args.command == "start": - start_bot(args.bot_name, args.job_manager_opts, args.event_handler_opts) + start_bot(args.instance, args.job_manager_opts, args.event_handler_opts) time.sleep(2) - status_bot(args.bot_name) + status_bot(args.instance) elif args.command == "stop": - stop_bot(args.bot_name) + stop_bot(args.instance) elif args.command == "restart": - stop_bot(args.bot_name) + stop_bot(args.instance) time.sleep(1) - start_bot(args.bot_name, args.job_manager_opts, args.event_handler_opts) + start_bot(args.instance, args.job_manager_opts, args.event_handler_opts) time.sleep(2) - status_bot(args.bot_name) + status_bot(args.instance) elif args.command == "status": - status_bot(args.bot_name) + status_bot(args.instance) if __name__ == "__main__": diff --git a/tests/test_tools_args.py b/tests/test_tools_args.py index 8fbd0727..39f649ed 100644 --- a/tests/test_tools_args.py +++ b/tests/test_tools_args.py @@ -44,17 +44,17 @@ def test_parse_common_args(test_args, expected_parsed, expected_unknown): # Test event_handler_parse() @pytest.mark.parametrize("test_args,expectation", [ # No args - ([], nullcontext(Namespace(debug=False, build=False, test=False, cron=False, file=None, port=3000, bot_name=None))), + ([], nullcontext(Namespace(debug=False, build=False, test=False, cron=False, file=None, port=3000, instance=None))), # Short-form args (["-d", "-b", "-t", "-c", "-f", "file.json", "-p", "8000"], nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file.json", port="8000", - bot_name=None))), + instance=None))), # Long-form args - (["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000", "--bot-name", "test-bot"], + (["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000", "--instance", "test-bot"], nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file2.json", port="9000", - bot_name="test-bot"))), + instance="test-bot"))), # Unknown args - should fail and exit (["-u"], pytest.raises(SystemExit)), @@ -68,15 +68,15 @@ def test_event_handler_parse_known_args(test_args, expectation): # Test job_manager_parse() @pytest.mark.parametrize("test_args,expectation", [ # No args - ([], nullcontext(Namespace(debug=False, max_manager_iterations=-1, jobs=None, bot_name=None))), + ([], nullcontext(Namespace(debug=False, max_manager_iterations=-1, jobs=None, instance=None))), # Short-form args (["-d", "-i", "0", "-j", "17"], - nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17", bot_name=None))), + nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17", instance=None))), # Long-form args - (["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48", "--bot-name", "test-bot"], - nullcontext(Namespace(debug=True, max_manager_iterations="10", jobs="4,18,48", bot_name="test-bot"))), + (["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48", "--instance", "test-bot"], + nullcontext(Namespace(debug=True, max_manager_iterations="10", jobs="4,18,48", instance="test-bot"))), # Unknown args - should fail and exit (["-u"], pytest.raises(SystemExit)), diff --git a/tools/args.py b/tools/args.py index 4d256f51..f3739f28 100644 --- a/tools/args.py +++ b/tools/args.py @@ -86,8 +86,9 @@ def event_handler_parse(args=None): ) parser.add_argument( - "--bot-name", - help="bot name (not used by the bot itself), useful for keeping track of multiple instances", + "--instance", + help="bot instance (not used by the event handler itself), useful for keeping track of multiple instances on " + "the same machine", ) return parser.parse_args(args=unknown_args, namespace=parsed_args) @@ -116,8 +117,9 @@ def job_manager_parse(args=None): ) parser.add_argument( - "--bot-name", - help="bot name (not used by the bot itself), useful for keeping track of multiple instances", + "--instance", + help="bot instance (not used by the job manager itself), useful for keeping track of multiple instances on " + "the same machine", ) return parser.parse_args(args=unknown_args, namespace=parsed_args) From b7ff15156aa421b65bc77d7d807bf4534d753aa1 Mon Sep 17 00:00:00 2001 From: Samuel Moors Date: Sun, 6 Sep 2026 16:29:13 +0200 Subject: [PATCH 11/11] try to fix tests by fixing test_app.cfg --- tests/test_app.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_app.cfg b/tests/test_app.cfg index e64e7ffb..c5265faa 100644 --- a/tests/test_app.cfg +++ b/tests/test_app.cfg @@ -17,6 +17,7 @@ hosting_platform = github [github] app_name = test-app-github +api_timeout = 10 [gitlab] api_timeout = 123 @@ -50,6 +51,3 @@ running_job = job `{job_id}` is running [bot_control] command_permission = user01 second_user - -[github] -api_timeout = 10