@@ -2886,6 +2886,92 @@ class RunArgs:
28862886# ark webapp
28872887# ============================================================
28882888
2889+ _SYSTEMD_SERVICE_NAME = "ark-webapp"
2890+
2891+
2892+ def _service_file_path () -> Path :
2893+ return Path .home () / ".config" / "systemd" / "user" / f"{ _SYSTEMD_SERVICE_NAME } .service"
2894+
2895+
2896+ def _generate_service_unit (host : str , port : int ) -> str :
2897+ """Generate a systemd user service unit file for the ARK webapp."""
2898+ python_bin = sys .executable
2899+ ark_root = get_ark_root ()
2900+ return f"""\
2901+ [Unit]
2902+ Description=ARK Research Portal
2903+ After=network.target
2904+
2905+ [Service]
2906+ Type=simple
2907+ WorkingDirectory={ ark_root }
2908+ ExecStart={ python_bin } -m ark.cli webapp --host { host } --port { port }
2909+ Restart=on-failure
2910+ RestartSec=5
2911+ StandardOutput=journal
2912+ StandardError=journal
2913+
2914+ [Install]
2915+ WantedBy=default.target
2916+ """
2917+
2918+
2919+ def _cmd_webapp_install (host : str , port : int ):
2920+ """Install and start ARK webapp as a systemd user service."""
2921+ import subprocess as _sp
2922+
2923+ svc_path = _service_file_path ()
2924+ svc_path .parent .mkdir (parents = True , exist_ok = True )
2925+
2926+ unit = _generate_service_unit (host , port )
2927+ svc_path .write_text (unit )
2928+ print (f" Service file written to { _c (str (svc_path ), Colors .CYAN )} " )
2929+
2930+ # Reload, enable, and start
2931+ _sp .run (["systemctl" , "--user" , "daemon-reload" ], check = True )
2932+ _sp .run (["systemctl" , "--user" , "enable" , _SYSTEMD_SERVICE_NAME ], check = True )
2933+ _sp .run (["systemctl" , "--user" , "start" , _SYSTEMD_SERVICE_NAME ], check = True )
2934+
2935+ print (f"\n { _c ('ARK Webapp' , Colors .BOLD )} installed and started as systemd user service." )
2936+ print (f" URL: { _c (f'http://{ host } :{ port } ' , Colors .CYAN )} " )
2937+ print ()
2938+ print (f" Manage with:" )
2939+ print (f" { _c (f'systemctl --user status { _SYSTEMD_SERVICE_NAME } ' , Colors .DIM )} " )
2940+ print (f" { _c (f'systemctl --user restart { _SYSTEMD_SERVICE_NAME } ' , Colors .DIM )} " )
2941+ print (f" { _c (f'journalctl --user -u { _SYSTEMD_SERVICE_NAME } -f' , Colors .DIM )} " )
2942+ print ()
2943+
2944+ # Check linger
2945+ try :
2946+ r = _sp .run (["loginctl" , "show-user" , os .environ .get ("USER" , "" )],
2947+ capture_output = True , text = True )
2948+ if "Linger=no" in r .stdout :
2949+ print (f" { _c ('Note:' , Colors .YELLOW )} Linger is not enabled for your user." )
2950+ print (f" The service will stop when you log out. To keep it running:" )
2951+ user = os .environ .get ("USER" , "" )
2952+ print (f" { _c (f'sudo loginctl enable-linger { user } ' , Colors .BOLD )} " )
2953+ print ()
2954+ except FileNotFoundError :
2955+ pass
2956+
2957+
2958+ def _cmd_webapp_uninstall ():
2959+ """Stop and remove the ARK webapp systemd user service."""
2960+ import subprocess as _sp
2961+
2962+ svc_path = _service_file_path ()
2963+ if not svc_path .exists ():
2964+ print (f" { _c ('Not installed:' , Colors .YELLOW )} No systemd service found at { svc_path } " )
2965+ return
2966+
2967+ _sp .run (["systemctl" , "--user" , "stop" , _SYSTEMD_SERVICE_NAME ], capture_output = True )
2968+ _sp .run (["systemctl" , "--user" , "disable" , _SYSTEMD_SERVICE_NAME ], capture_output = True )
2969+ svc_path .unlink (missing_ok = True )
2970+ _sp .run (["systemctl" , "--user" , "daemon-reload" ], check = True )
2971+
2972+ print (f" { _c ('ARK Webapp' , Colors .BOLD )} service stopped and removed." )
2973+
2974+
28892975def cmd_webapp (args ):
28902976 """Start the ARK web app (lab-facing project submission portal)."""
28912977 subcmd = getattr (args , 'webapp_cmd' , None )
@@ -2896,6 +2982,13 @@ def cmd_webapp(args):
28962982 cmd_web (args )
28972983 return
28982984
2985+ if subcmd == 'install' :
2986+ _cmd_webapp_install (args .host , args .port )
2987+ return
2988+ if subcmd == 'uninstall' :
2989+ _cmd_webapp_uninstall ()
2990+ return
2991+
28992992 try :
29002993 import uvicorn
29012994 from ark .webapp import create_app
@@ -2915,6 +3008,8 @@ def cmd_webapp(args):
29153008 print (f" Edit { _c (str (_env_file ()), Colors .CYAN )} and set SMTP_USER / SMTP_PASSWORD.\n " )
29163009
29173010 if args .daemon :
3011+ print (f"\n { _c ('Deprecation:' , Colors .YELLOW )} --daemon uses os.fork() and will be removed in a future release." )
3012+ print (f" Use { _c ('ark webapp install' , Colors .BOLD )} instead for a systemd user service.\n " )
29183013 _root = get_ark_root ()
29193014 _webapp_dir = _root / "ark_webapp"
29203015 _webapp_dir .mkdir (exist_ok = True )
@@ -3401,9 +3496,11 @@ def main():
34013496 webapp_sub = p_webapp .add_subparsers (dest = "webapp_cmd" )
34023497 webapp_sub .add_parser ("disable" , help = "Block new project submissions" )
34033498 webapp_sub .add_parser ("enable" , help = "Allow new project submissions" )
3499+ webapp_sub .add_parser ("install" , help = "Install as systemd user service (auto-start on boot)" )
3500+ webapp_sub .add_parser ("uninstall" , help = "Stop and remove systemd user service" )
34043501 p_webapp .add_argument ("--port" , type = int , default = 8423 , help = "Port (default: 8423)" )
34053502 p_webapp .add_argument ("--host" , default = "0.0.0.0" , help = "Host (default: 0.0.0.0)" )
3406- p_webapp .add_argument ("--daemon" , action = "store_true" , help = "Run in background" )
3503+ p_webapp .add_argument ("--daemon" , action = "store_true" , help = "Run in background (deprecated, use 'install') " )
34073504 p_webapp .set_defaults (func = cmd_webapp )
34083505
34093506 # ark web disable/enable [project]
0 commit comments