From 52d336ae20c26e559c226e80b73a4dace2fb86ed Mon Sep 17 00:00:00 2001 From: conrad96 Date: Wed, 6 May 2026 12:36:31 +0300 Subject: [PATCH] added clientappctl for app-dev-updates --- scripts/clientappctl | 205 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100755 scripts/clientappctl diff --git a/scripts/clientappctl b/scripts/clientappctl new file mode 100755 index 00000000..ed462881 --- /dev/null +++ b/scripts/clientappctl @@ -0,0 +1,205 @@ +#! /usr/bin/env python3 + +import sys, os, argparse, json, shutil, tempfile, subprocess, traceback + +def run_script(script_path: str) -> int: + """ + Helper to handle subprocess execution + Args: + script_path (str): script path to execute + Returns: + int: 0 if both succeed, 1 if any step fails + """ + if not os.path.exists(script_path): + print(f"Error: Script not found at {script_path}", file=sys.stderr) + return 1 + + try: + subprocess.run(f'{script_path}', check=True, capture_output=True, text=True) + return 0 + except subprocess.CalledProcessError as e: + print(f"Error: {os.path.basename(script_path)} failed (Exit {e.returncode})", file=sys.stderr) + print(f"Details: {e.stderr.strip() or e.stdout.strip()}", file=sys.stderr) + return 1 + except PermissionError: + print(f"Error: {os.path.basename(script_path)} is not executable. Check permissions.", file=sys.stderr) + return 1 + +def handle_start(src_dir, install_path): + """ + Start application + + Args: + src_dir (str): Path to source directory (Working directory) + install_path (str): Path to target installation directory + Returns: + int: 0 -> success + """ + + try: + # disable app + if os.path.exists(os.path.join(install_path, 'disable')): + subprocess.check_output(f'{install_path}/disable') + + # copy all source files to installation path + shutil.copytree(src_dir, install_path, dirs_exist_ok=True) + + # start application + run_script(f'{install_path}/start') + + return 0 + except Exception as ex: + print(f'Error: {ex}') + return 1 + +def handle_stop(install_path: str): + """ + Stop application + Args: + install_path (str): Path to installation directory + Returns: + int: 0 -> success + """ + + if not os.path.exists(os.path.join(install_path, 'stop')): + print(f"Error: stop script not found at {install_path}") + return 1 + + try: + # stop application + run_script(f'{install_path}/stop') + + return 0 + except subprocess.CalledProcessError as e: + print(f"Error: Stop script failed with exit code {e.returncode}") + print(f"Output: {e.stderr or e.stdout}") + return 1 + +def handle_restart(src_path: str, install_path: str): + """ + Restart application + Args: + install_path (str): Path to installation directory + Returns: + int: 0 -> success + """ + if handle_stop(install_path) != 0: + print("Restart aborted: Stop script execution failed.", file=sys.stderr) + return 1 + + if handle_start(src_path, install_path) != 0: + print("Restart aborted: Start script execution failed.", file=sys.stderr) + return 1 + + print('Application restarted successfully.') + return 0 + + +def uninstall_app(args: argparse.Namespace, install_path) -> int: + """ + Uninstall application at specified installation directory. + + Args: + install_path (str): Path to target installation directory. + Returns: + Int + """ + + rval = 0 + try: + + # disable application then uninstall + disable_path = os.path.join(install_path, 'disable') + stop_path = os.path.join(install_path, 'stop') + if os.path.exists(disable_path): + subprocess.check_output(disable_path) + + if os.path.exists(stop_path): + subprocess.check_output(stop_path) + + # reset vols, networks + subprocess.check_output(['docker', 'compose', f'-f {install_path}/docker-compose.yml', 'down', '-v']) + + if os.path.exists(install_path): + shutil.rmtree(install_path) + except Exception as e: + print(f'Exception encountered: {e}') + traceback.print_exc() + rval = 1 + return rval + +# script constants +utils_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) +temp_dir = tempfile.mkdtemp() +build_dir = os.path.join(temp_dir, 'build') +staging_dir = os.path.join(temp_dir, 'staging') +working_dir = working_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +src_path = os.path.join(working_dir, 'src') +CONFIG_JSON = os.path.join(working_dir, 'config.json') + +templates_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'hosted-app-utils', 'templates')) + +template_files = { + f'{templates_dir}/start': f'{working_dir}/src/start', + f'{templates_dir}/stop': f'{working_dir}/src/stop', + f'{templates_dir}/disable': f'{working_dir}/src/disable' +} + +# command line arguments +parser = argparse.ArgumentParser( + prog="clientappctl", + description="Manage client application operations. [ start, stop, restart ]") + +parser.add_argument("-c", "--config", default=CONFIG_JSON, help=f"Path to application configuration (default: {CONFIG_JSON})") + +subparsers = parser.add_subparsers(dest="command", required=True) + +start_parser = subparsers.add_parser("start", help="Start the application") +stop_parser = subparsers.add_parser("stop", help="Stop the application") +restart_parser = subparsers.add_parser("restart", help="Restart the application") + +def main() -> int: + # parse user args + args = parser.parse_args() + + # load configuration + try: + config_path = args.config + if not os.path.exists(config_path): + raise ValueError("Configuration not found.") + + with open(config_path) as f: + config_data = json.load(f) + name_slug = config_data['name'].strip().replace(' ', '_').replace('-', '_').lower() + org_slug = config_data['org'].strip().replace(' ', '_').replace('-', '_').lower() + version = config_data['version'] + + install_path = f'/var/signalytic/clientapps/{org_slug}/{name_slug}' + + # copy temp files into working dir + for src, dest in template_files.items(): + if not os.path.exists(src): + continue + shutil.copy(src, dest) + + if args.command == "start": + print(f"Starting app: {config_data['name']}-{version}") + + if not os.path.exists(install_path): + os.makedirs(install_path) + return handle_start(src_path, install_path) + elif args.command == "stop": + print(f"Stopping app: {config_data['name']}-{version}") + return handle_stop(install_path) + elif args.command == "restart": + print(f"Restarting app: {config_data['name']}-{version}") + return handle_restart(src_path, install_path) + else: + raise ValueError('Invalid operation passed.') + + except Exception as ex: + print(f'Error: {ex}') + return 1 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file