66from subprocess import call ,check_output
77from pathlib import Path
88import json
9+ import logging
910import platform
1011import re
12+ import secrets
13+ import threading
1114from flask_cors import CORS , cross_origin
1215
16+ logger = logging .getLogger (__name__ )
17+
1318# Input validation pattern for safe names (alphanumeric, dash, underscore, slash, dot, space)
1419SAFE_INPUT_PATTERN = re .compile (r'^[a-zA-Z0-9_\-/. ]+$' )
1520# Pattern for filenames - no path separators or .. allowed
@@ -91,12 +96,13 @@ def require_api_key():
9196 """Require a valid API key via X-API-KEY header."""
9297 if not API_KEY :
9398 abort (500 , description = "Server not configured with API key" )
94- provided = request .headers .get ("X-API-KEY" )
95- if provided != API_KEY :
99+ provided = request .headers .get ("X-API-KEY" ) or ""
100+ if not secrets . compare_digest ( provided , API_KEY ) :
96101 abort (403 , description = "Unauthorized" )
97102
98103# Track single Jupyter process to prevent multiple concurrent launches
99104jupyter_process = None
105+ jupyter_lock = threading .Lock ()
100106
101107
102108app = Flask (__name__ )
@@ -554,20 +560,22 @@ def openJupyter():
554560
555561 require_api_key ()
556562
557- if jupyter_process and jupyter_process .poll () is None :
558- return jsonify ({"message" : "Jupyter already running" }), 409
563+ with jupyter_lock :
564+ if jupyter_process and jupyter_process .poll () is None :
565+ return jsonify ({"message" : "Jupyter already running" }), 409
559566
560- try :
561- jupyter_process = subprocess .Popen (
562- ['jupyter' , 'lab' , '--no-browser' ],
563- shell = False ,
564- cwd = concore_path ,
565- stdout = subprocess .DEVNULL ,
566- stderr = subprocess .DEVNULL
567- )
568- return jsonify ({"message" : "Jupyter Lab started" }), 200
569- except Exception :
570- return jsonify ({"error" : "Failed to start Jupyter" }), 500
567+ try :
568+ jupyter_process = subprocess .Popen (
569+ ['jupyter' , 'lab' , '--no-browser' ],
570+ shell = False ,
571+ cwd = concore_path ,
572+ stdout = subprocess .DEVNULL ,
573+ stderr = subprocess .DEVNULL
574+ )
575+ return jsonify ({"message" : "Jupyter Lab started" }), 200
576+ except (FileNotFoundError , PermissionError , OSError ) as e :
577+ logger .error ("Failed to start Jupyter: %s" , e )
578+ return jsonify ({"error" : "Failed to start Jupyter" }), 500
571579
572580
573581@app .route ('/stopJupyter/' , methods = ['POST' ])
@@ -576,13 +584,22 @@ def stopJupyter():
576584
577585 require_api_key ()
578586
579- if not jupyter_process or jupyter_process .poll () is not None :
580- return jsonify ({"message" : "No running Jupyter instance" }), 404
581-
582- jupyter_process .terminate ()
583- jupyter_process = None
587+ with jupyter_lock :
588+ if not jupyter_process or jupyter_process .poll () is not None :
589+ return jsonify ({"message" : "No running Jupyter instance" }), 404
584590
585- return jsonify ({"message" : "Jupyter stopped" }), 200
591+ try :
592+ jupyter_process .terminate ()
593+ # Wait for Jupyter to terminate gracefully
594+ jupyter_process .wait (timeout = 5 )
595+ except subprocess .TimeoutExpired :
596+ # Force kill if it did not exit in time
597+ jupyter_process .kill ()
598+ jupyter_process .wait (timeout = 5 )
599+ finally :
600+ jupyter_process = None
601+
602+ return jsonify ({"message" : "Jupyter stopped" }), 200
586603
587604
588605if __name__ == "__main__" :
0 commit comments