From 0e8054c541206f0ca5fe90a7c941defe60cd5720 Mon Sep 17 00:00:00 2001 From: deacon Date: Mon, 16 Mar 2026 09:47:03 -0400 Subject: [PATCH 1/2] fix(server): use shutil.which to resolve npm for Windows compatibility On Windows, npm is a batch script (npm.cmd) that subprocess cannot find without shell=True. Instead of adding shell=True (security concern per Bandit B602), use shutil.which() to resolve the full path to npm on any platform. --- server.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/server.py b/server.py index ad55e41ef..52d371045 100644 --- a/server.py +++ b/server.py @@ -2,6 +2,7 @@ import asyncio import logging import os +import shutil import signal from rich.console import Console from rich.logging import RichHandler @@ -42,6 +43,22 @@ MAGMA_PATH = "./plugins/magma" +def _resolve_npm(): + """Resolve the full path to the npm executable. + + On Windows, ``npm`` is a batch script (``npm.cmd``) that + ``subprocess`` cannot find without ``shell=True``. Using + ``shutil.which`` resolves the correct path on every platform. + """ + npm = shutil.which("npm") + if npm is None: + raise FileNotFoundError( + "npm is not installed or not on PATH. " + "Install Node.js (https://nodejs.org/) and ensure npm is available." + ) + return npm + + def setup_logger(level=logging.DEBUG): format = "%(message)s" datefmt = "%Y-%m-%d %H:%M:%S" @@ -187,8 +204,9 @@ async def enable_cors(request, response): async def start_vue_dev_server(): + npm = _resolve_npm() proc = await asyncio.create_subprocess_exec( - "npm", "run", "dev", stdout=sys.stdout, stderr=sys.stderr, cwd=MAGMA_PATH + npm, "run", "dev", stdout=sys.stdout, stderr=sys.stderr, cwd=MAGMA_PATH ) logging.info("VueJS development server started (PID %s).", proc.pid) @@ -314,7 +332,8 @@ def _get_size_mb(config_key, default): if args.uiDevHost: if not os.path.exists(f"{MAGMA_PATH}/dist") and (os.path.exists(f"{MAGMA_PATH}") and len(os.listdir(MAGMA_PATH)) > 0): logging.info("Building VueJS front-end.") - subprocess.run(["npm", "run", "build"], cwd=MAGMA_PATH, check=True) + npm = _resolve_npm() + subprocess.run([npm, "run", "build"], cwd=MAGMA_PATH, check=True) logging.info("VueJS front-end build complete.") else: logging.warning( @@ -328,8 +347,9 @@ def _get_size_mb(config_key, default): if args.build: if os.path.exists(f"{MAGMA_PATH}") and len(os.listdir(MAGMA_PATH)) > 0: logging.info("Building VueJS front-end.") - subprocess.run(["npm", "install"], cwd=MAGMA_PATH, check=True) - subprocess.run(["npm", "run", "build"], cwd=MAGMA_PATH, check=True) + npm = _resolve_npm() + subprocess.run([npm, "install"], cwd=MAGMA_PATH, check=True) + subprocess.run([npm, "run", "build"], cwd=MAGMA_PATH, check=True) logging.info("VueJS front-end build complete.") else: logging.warning( From a9fc66383499be775c33f1f3932193834c927a2d Mon Sep 17 00:00:00 2001 From: deacon Date: Mon, 16 Mar 2026 09:53:53 -0400 Subject: [PATCH 2/2] Fix Windows compat: use shell=True for .cmd files resolved by shutil.which On Windows, shutil.which("npm") resolves to npm.cmd, and .cmd files cannot be executed directly by subprocess without shell=True. Add shell=(sys.platform == "win32") to subprocess.run calls and use create_subprocess_shell on Windows for the async dev server call. --- server.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/server.py b/server.py index 52d371045..fb90b9587 100644 --- a/server.py +++ b/server.py @@ -205,9 +205,17 @@ async def enable_cors(request, response): async def start_vue_dev_server(): npm = _resolve_npm() - proc = await asyncio.create_subprocess_exec( - npm, "run", "dev", stdout=sys.stdout, stderr=sys.stderr, cwd=MAGMA_PATH - ) + if sys.platform == "win32": + # On Windows shutil.which resolves to npm.cmd; batch files cannot + # be executed directly by create_subprocess_exec, so use shell. + cmd = subprocess.list2cmdline([npm, "run", "dev"]) + proc = await asyncio.create_subprocess_shell( + cmd, stdout=sys.stdout, stderr=sys.stderr, cwd=MAGMA_PATH, + ) + else: + proc = await asyncio.create_subprocess_exec( + npm, "run", "dev", stdout=sys.stdout, stderr=sys.stderr, cwd=MAGMA_PATH, + ) logging.info("VueJS development server started (PID %s).", proc.pid) @@ -333,7 +341,8 @@ def _get_size_mb(config_key, default): if not os.path.exists(f"{MAGMA_PATH}/dist") and (os.path.exists(f"{MAGMA_PATH}") and len(os.listdir(MAGMA_PATH)) > 0): logging.info("Building VueJS front-end.") npm = _resolve_npm() - subprocess.run([npm, "run", "build"], cwd=MAGMA_PATH, check=True) + subprocess.run([npm, "run", "build"], cwd=MAGMA_PATH, check=True, + shell=(sys.platform == "win32")) logging.info("VueJS front-end build complete.") else: logging.warning( @@ -348,8 +357,10 @@ def _get_size_mb(config_key, default): if os.path.exists(f"{MAGMA_PATH}") and len(os.listdir(MAGMA_PATH)) > 0: logging.info("Building VueJS front-end.") npm = _resolve_npm() - subprocess.run([npm, "install"], cwd=MAGMA_PATH, check=True) - subprocess.run([npm, "run", "build"], cwd=MAGMA_PATH, check=True) + subprocess.run([npm, "install"], cwd=MAGMA_PATH, check=True, + shell=(sys.platform == "win32")) + subprocess.run([npm, "run", "build"], cwd=MAGMA_PATH, check=True, + shell=(sys.platform == "win32")) logging.info("VueJS front-end build complete.") else: logging.warning(