@@ -39,7 +39,60 @@ def translate_install_cmd(cmd):
3939 return os_cmd (cmd )
4040
4141def req (): return 'user' in session
42- _jobs = {}
42+
43+ # ── Job store: file-based so all gunicorn workers share state ──────────────
44+ # In-memory dict fails with --workers > 1 because each worker has its own
45+ # memory space. A small JSON file per job in /tmp/vortex_jobs/ is shared
46+ # across all workers via the filesystem.
47+ _JOBS_DIR = '/tmp/vortex_jobs'
48+ os .makedirs (_JOBS_DIR , exist_ok = True )
49+ _jobs_lock = threading .Lock ()
50+
51+ def _job_path (job_id ):
52+ return os .path .join (_JOBS_DIR , f'{ job_id } .json' )
53+
54+ def _job_get (job_id ):
55+ try :
56+ with open (_job_path (job_id )) as f :
57+ return json .load (f )
58+ except : return None
59+
60+ def _job_set (job_id , data ):
61+ with _jobs_lock :
62+ try :
63+ with open (_job_path (job_id ), 'w' ) as f :
64+ json .dump (data , f )
65+ except : pass
66+
67+ def _job_append_line (job_id , line ):
68+ with _jobs_lock :
69+ try :
70+ path = _job_path (job_id )
71+ with open (path ) as f : data = json .load (f )
72+ data ['lines' ].append (line )
73+ with open (path , 'w' ) as f : json .dump (data , f )
74+ except : pass
75+
76+ def _job_finish (job_id , success , installed , inst_ver = '' ):
77+ with _jobs_lock :
78+ try :
79+ path = _job_path (job_id )
80+ with open (path ) as f : data = json .load (f )
81+ data .update ({'done' :True ,'success' :success ,'installed' :installed ,
82+ 'installedVer' :inst_ver ,'status' :'done' })
83+ with open (path , 'w' ) as f : json .dump (data , f )
84+ except : pass
85+
86+ def _job_create (job_id , initial_installed = False ):
87+ _job_set (job_id , {'status' :'running' ,'lines' :[],'done' :False ,
88+ 'success' :False ,'installed' :initial_installed ,'installedVer' :'' })
89+
90+ # Legacy compatibility — keep _jobs as a shim for any direct access
91+ class _JobsShim :
92+ def get (self , job_id , default = None ): return _job_get (job_id ) or default
93+ def __setitem__ (self , job_id , val ): _job_set (job_id , val )
94+ def __getitem__ (self , job_id ): return _job_get (job_id )
95+ _jobs = _JobsShim ()
4396
4497def sh (c , t = 10 ):
4598 try :
@@ -903,22 +956,22 @@ def rc_get(key):
903956 if not cmd : return jsonify ({'ok' :False , 'error' :'No install command defined' }), 400
904957
905958 job_id = str (uuid .uuid4 ())[:8 ]
906- _jobs [ job_id ] = { 'status' : 'running' , 'lines' :[], 'done' : False , 'success' : False , 'installed' : False }
959+ _job_create ( job_id , initial_installed = False )
907960
908961 def run_job ():
909- _jobs [ job_id ][ 'lines' ]. append ( f'[VortexPanel] Installing { mod ["name" ]} { ver } ...' )
962+ _job_append_line ( job_id , f'[VortexPanel] Installing { mod ["name" ]} { ver } ...' )
910963 _final_cmd = translate_install_cmd (cmd )
911964 proc = subprocess .Popen (f'DEBIAN_FRONTEND=noninteractive { _final_cmd } 2>&1' ,
912965 shell = True , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True , bufsize = 1 )
913966 for line in proc .stdout :
914- _jobs [ job_id ][ 'lines' ]. append ( line .rstrip ())
967+ _job_append_line ( job_id , line .rstrip ())
915968 proc .wait ()
916969 installed = is_installed (mod ['check' ])
917970 inst_ver = get_version (mod ['id' ]) if installed else ''
918- _jobs [job_id ].update ({'installed' :installed ,'installedVer' :inst_ver ,'done' :True ,'success' :installed ,'status' :'done' })
919- _jobs [job_id ]['lines' ].append (
971+ _job_append_line (job_id ,
920972 f'[VortexPanel] { "✓ Installed successfully! Version: " + inst_ver if installed else "⚠ Installation may have failed — check output above." } '
921973 )
974+ _job_finish (job_id , success = installed , installed = installed , inst_ver = inst_ver )
922975 panel_cache .invalidate ('modules_list' )
923976
924977 threading .Thread (target = run_job , daemon = True ).start ()
@@ -943,29 +996,27 @@ def uninstall_module(mod_id):
943996 if not cmd : return jsonify ({'ok' :False , 'error' :'No uninstall command defined' }), 400
944997
945998 job_id = str (uuid .uuid4 ())[:8 ]
946- _jobs [ job_id ] = { 'status' : 'running' , 'lines' :[], 'done' : False , 'success' : False , 'installed' : True }
999+ _job_create ( job_id , initial_installed = True )
9471000
9481001 def run_job ():
949- _jobs [ job_id ][ 'lines' ]. append ( f'[VortexPanel] Removing { mod ["name" ]} { ver } ...' )
1002+ _job_append_line ( job_id , f'[VortexPanel] Removing { mod ["name" ]} { ver } ...' )
9501003 proc = subprocess .Popen (f'DEBIAN_FRONTEND=noninteractive { cmd } 2>&1' ,
9511004 shell = True , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True , bufsize = 1 )
9521005 for line in proc .stdout :
953- _jobs [ job_id ][ 'lines' ]. append ( line .rstrip ())
1006+ _job_append_line ( job_id , line .rstrip ())
9541007 proc .wait ()
955- # For versioned modules (PHP, Python), check version-specific binary
9561008 if ver and mod_id in ('php' ,'python' ):
9571009 ver_binary = f'php{ ver } ' if mod_id == 'php' else f'python{ ver } '
9581010 still_installed = bool (sh (f'which { ver_binary } 2>/dev/null' ))
9591011 else :
9601012 still_installed = is_installed (mod ['check' ])
9611013 removed = not still_installed
962- # For PHP: overall 'installed' = any PHP still present
9631014 if mod_id == 'php' :
9641015 any_php = is_installed (mod ['check' ])
965- _jobs [ job_id ]. update ({ 'installed' : any_php , 'done' : True , ' success' : removed ,'status' : 'done' } )
1016+ _job_finish ( job_id , success = removed , installed = any_php )
9661017 else :
967- _jobs [ job_id ]. update ({ 'installed' : still_installed , 'done' : True , ' success' : removed ,'status' : 'done' } )
968- _jobs [ job_id ][ 'lines' ]. append (
1018+ _job_finish ( job_id , success = removed , installed = still_installed )
1019+ _job_append_line ( job_id ,
9691020 f'[VortexPanel] { "✓ Removed successfully!" if removed else "⚠ May not be fully removed — check output above." } '
9701021 )
9711022 panel_cache .invalidate ('modules_list' )
@@ -977,15 +1028,19 @@ def run_job():
9771028def job_stream (job_id ):
9781029 def generate ():
9791030 sent = 0
980- while True :
981- job = _jobs . get (job_id )
1031+ for _ in range ( 600 ): # max 3 minutes (600 × 0.3s)
1032+ job = _job_get (job_id )
9821033 if not job :
9831034 yield f'data: { json .dumps ({"error" :"Job not found" })} \n \n ' ; break
984- while sent < len (job ['lines' ]):
985- yield f'data: { json .dumps ({"line" : job ["lines" ][sent ]})} \n \n '
1035+ lines = job .get ('lines' , [])
1036+ while sent < len (lines ):
1037+ yield f'data: { json .dumps ({"line" : lines [sent ]})} \n \n '
9861038 sent += 1
987- if job [ 'done' ] :
1039+ if job . get ( 'done' ) :
9881040 yield f'data: { json .dumps ({"done" :True ,"success" :job ["success" ],"installed" :job ["installed" ],"installedVer" :job .get ("installedVer" ,"" )})} \n \n '
1041+ # Clean up job file after a short delay
1042+ try : os .remove (_job_path (job_id ))
1043+ except : pass
9891044 break
9901045 time .sleep (0.3 )
9911046 return Response (generate (), mimetype = 'text/event-stream' ,
@@ -1846,27 +1901,24 @@ def sh(cmd, t=30):
18461901
18471902 # Run as a streaming job — same system as install/uninstall
18481903 job_id = str (uuid .uuid4 ())[:8 ]
1849- _jobs [ job_id ] = { 'status' : 'running' , 'lines' :[], 'done' : False , 'success' : False , 'installed' : True }
1904+ _job_create ( job_id , initial_installed = True )
18501905
18511906 def run_switch ():
18521907 mod_name = mod ['name' ] if mod else mod_id
1853- _jobs [ job_id ][ 'lines' ]. append ( f'[VortexPanel] Switching { mod_name } to version { ver } ...' )
1908+ _job_append_line ( job_id , f'[VortexPanel] Switching { mod_name } to version { ver } ...' )
18541909 proc = subprocess .Popen (
18551910 f'DEBIAN_FRONTEND=noninteractive { script } 2>&1' ,
18561911 shell = True , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True , bufsize = 1
18571912 )
18581913 for line in proc .stdout :
1859- _jobs [ job_id ][ 'lines' ]. append ( line .rstrip ())
1914+ _job_append_line ( job_id , line .rstrip ())
18601915 proc .wait ()
18611916 success = proc .returncode == 0
18621917 new_ver = sh (ver_check_cmd ) if ver_check_cmd else ver
1863- _jobs [ job_id ][ 'lines' ]. append (
1918+ _job_append_line ( job_id ,
18641919 f'[VortexPanel] { "✓ Switched to " + new_ver + " successfully!" if success else "⚠ Switch may have failed — check output above." } '
18651920 )
1866- _jobs [job_id ].update ({
1867- 'done' : True , 'success' : success , 'status' : 'done' ,
1868- 'installed' : True , 'installedVer' : new_ver ,
1869- })
1921+ _job_finish (job_id , success = success , installed = True , inst_ver = new_ver )
18701922 panel_cache .invalidate ('modules_list' )
18711923
18721924 threading .Thread (target = run_switch , daemon = True ).start ()
0 commit comments