diff --git a/server.py b/server.py index ad55e41ef..fb90b9587 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,9 +204,18 @@ async def enable_cors(request, response): async def start_vue_dev_server(): - proc = await asyncio.create_subprocess_exec( - "npm", "run", "dev", stdout=sys.stdout, stderr=sys.stderr, cwd=MAGMA_PATH - ) + npm = _resolve_npm() + 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) @@ -314,7 +340,9 @@ 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, + shell=(sys.platform == "win32")) logging.info("VueJS front-end build complete.") else: logging.warning( @@ -328,8 +356,11 @@ 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, + 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(