diff --git a/docker-launcher/.gitignore b/docker-launcher/.gitignore new file mode 100644 index 000000000..3b5fccb4e --- /dev/null +++ b/docker-launcher/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*.so +.Python +venv/ +.venv/ + +# PyInstaller +build/ +dist/ +*.spec.bak + +# IDE +.idea/ +.vscode/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Docker +*.log + +# Temp +*.tmp +*.bak + diff --git a/docker-launcher/Dockerfile b/docker-launcher/Dockerfile new file mode 100644 index 000000000..3f83af961 --- /dev/null +++ b/docker-launcher/Dockerfile @@ -0,0 +1,164 @@ +# eSim Docker Container +# Multi-stage build to keep the image smaller (~3.5GB vs ~5GB) +# FOSSEE IIT Bombay + +# ============================================ +# Stage 1: Build dependencies +# ============================================ +FROM ubuntu:22.04 AS builder + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Asia/Kolkata + +# Install build tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential gcc g++ make cmake git \ + python3 python3-pip python3-dev python3-venv \ + autoconf automake libtool pkg-config bison flex gettext wget \ + libgtk-3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Clone eSim +RUN git clone --depth 1 https://github.com/FOSSEE/eSim.git /build/esim + +# Setup Python venv with dependencies +# Note: setuptools<58 is needed for hdlparse which uses deprecated use_2to3 +RUN python3 -m venv /build/venv \ + && /build/venv/bin/pip install --no-cache-dir "setuptools<58.0.0" wheel \ + && /build/venv/bin/pip install --no-cache-dir \ + matplotlib==3.7.5 numpy==1.24.4 scipy==1.10.1 \ + PyQt5==5.15.7 pillow==10.4.0 hdlparse==1.0.4 watchdog==4.0.2 + +# Build GAW3 (analog waveform viewer) from source +# gtkwave is digital only, gaw3 is what eSim actually uses +RUN git clone --depth 1 https://github.com/StefanSchippers/xschem-gaw.git /build/gaw3 \ + && cd /build/gaw3 \ + && aclocal && autoheader \ + && automake --add-missing --foreign 2>/dev/null || true \ + && autoconf \ + && sed -i 's/GETTEXT_MACRO_VERSION = 0.18/GETTEXT_MACRO_VERSION = 0.20/' po/Makefile.in.in \ + && ./configure --prefix=/usr/local \ + && make -j$(nproc) \ + && make DESTDIR=/build/gaw3-install install + + +# ============================================ +# Stage 2: Runtime image +# ============================================ +FROM ubuntu:22.04 + +LABEL maintainer="FOSSEE IIT Bombay" +LABEL description="eSim EDA Tool with GUI support" + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Asia/Kolkata +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +# eSim paths +ENV ESIM_HOME=/usr/local/esim +ENV PYTHONPATH="${ESIM_HOME}/src:/opt/venv/lib/python3.10/site-packages" + +# VNC settings +ENV VNC_PORT=5901 +ENV NOVNC_PORT=6080 +ENV VNC_RESOLUTION=1920x1080 +ENV VNC_DEPTH=24 + +# Desktop and font rendering settings +ENV GTK_THEME=Adwaita +ENV UBUNTU_MENUPROXY=0 +ENV XDG_DATA_DIRS=/usr/share:/usr/local/share:/usr/share/icons +ENV QT_AUTO_SCREEN_SCALE_FACTOR=1 +ENV GDK_SCALE=1 +ENV FREETYPE_PROPERTIES="truetype:interpreter-version=40" + +# Install runtime packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + kicad kicad-libraries ngspice gtkwave xterm \ + python3 python3-wxgtk4.0 \ + libx11-6 libxext6 libxrender1 libxfixes3 libxi6 libxrandr2 \ + libxcursor1 libxinerama1 libgl1 libgl1-mesa-glx libgl1-mesa-dri \ + libxcb1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \ + libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 \ + libxcb-xkb1 libxkbcommon0 libxkbcommon-x11-0 dbus-x11 \ + adwaita-icon-theme-full hicolor-icon-theme gnome-icon-theme \ + oxygen-icon-theme tango-icon-theme humanity-icon-theme \ + ca-certificates libgtk-3-0 libcanberra-gtk-module xdg-utils \ + libglib2.0-0 libfontconfig1 libfreetype6 \ + tigervnc-standalone-server tigervnc-common \ + xfce4 xfce4-terminal novnc websockify \ + xdotool wmctrl openbox tint2 \ + && rm -rf /var/lib/apt/lists/* && apt-get clean + +# Copy built artifacts from builder stage +COPY --from=builder /build/gaw3-install/usr/local /usr/local/ +COPY --from=builder /build/esim ${ESIM_HOME} +COPY --from=builder /build/venv /opt/venv + +# Create user +ARG USERNAME=esim-user +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN groupadd --gid ${USER_GID} ${USERNAME} \ + && useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} \ + && mkdir -p /home/${USERNAME}/workspace \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME} \ + && chown -R ${USERNAME}:${USERNAME} ${ESIM_HOME} + +WORKDIR ${ESIM_HOME}/src/frontEnd + +# Create eSim config +RUN mkdir -p /home/${USERNAME}/.esim \ + && printf '[DEFAULT]\nworkspace=/home/%s/eSim-Workspace\n\n[eSim]\nworkspace=/home/%s/eSim-Workspace\nkicad=/usr/bin\nngspice=/usr/bin\n' \ + ${USERNAME} ${USERNAME} > /home/${USERNAME}/.esim/config.ini \ + && echo '{}' > /home/${USERNAME}/.esim/modelica_map.json \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.esim + +# Setup KiCad symbol libraries +RUN mkdir -p /home/${USERNAME}/.config/kicad/6.0 \ + && KICAD_CONFIG=/home/${USERNAME}/.config/kicad/6.0 \ + && ESIM_SYMLIB=/usr/local/esim/library/kicadLibrary/eSim-symbols \ + && KICAD_SYMLIB=/usr/share/kicad/symbols \ + && echo '(sym_lib_table' > ${KICAD_CONFIG}/sym-lib-table \ + && for lib in eSim_Devices eSim_Sources eSim_Analog eSim_Digital eSim_Hybrid eSim_Power eSim_Subckt eSim_Miscellaneous eSim_Plot eSim_Nghdl eSim_Ngveri eSim_SKY130 eSim_SKY130_Subckts eSim_User; do \ + echo " (lib (name \"$lib\")(type \"KiCad\")(uri \"${ESIM_SYMLIB}/${lib}.kicad_sym\")(options \"\")(descr \"\"))" >> ${KICAD_CONFIG}/sym-lib-table; \ + done \ + && for lib in Device power Simulation_SPICE Connector Analog Transistor_BJT Transistor_FET Diode Amplifier_Operational; do \ + echo " (lib (name \"$lib\")(type \"KiCad\")(uri \"${KICAD_SYMLIB}/${lib}.kicad_sym\")(options \"\")(descr \"\"))" >> ${KICAD_CONFIG}/sym-lib-table; \ + done \ + && echo ')' >> ${KICAD_CONFIG}/sym-lib-table \ + && echo '(fp_lib_table)' > ${KICAD_CONFIG}/fp-lib-table \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config + +# Setup VNC with openbox + tint2 taskbar +RUN mkdir -p /home/${USERNAME}/.vnc \ + && printf '#!/bin/bash\nunset SESSION_MANAGER\nunset DBUS_SESSION_BUS_ADDRESS\nexport XDG_RUNTIME_DIR=/tmp/runtime-esim-user\nmkdir -p $XDG_RUNTIME_DIR && chmod 700 $XDG_RUNTIME_DIR\ntint2 &\nexec openbox-session\n' \ + > /home/${USERNAME}/.vnc/xstartup \ + && chmod +x /home/${USERNAME}/.vnc/xstartup \ + && printf '\x9f\x87\x18\xb4\x8e\x8f\x8a\x57' > /home/${USERNAME}/.vnc/passwd \ + && chmod 600 /home/${USERNAME}/.vnc/passwd \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.vnc + +# XFCE config to fix window focus issues +RUN mkdir -p /home/${USERNAME}/.config/xfce4/xfconf/xfce-perchannel-xml \ + && printf '\n\n \n \n \n \n \n\n' \ + > /home/${USERNAME}/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config + +# Font rendering config for better VNC quality +RUN mkdir -p /home/${USERNAME}/.config/fontconfig \ + && printf '\n\n\n true\n true\n hintslight\n rgb\n\n' \ + > /home/${USERNAME}/.config/fontconfig/fonts.conf \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config/fontconfig + +# Startup script +RUN printf '#!/bin/bash\nset -e\n\n# Ensure workspace exists\nmkdir -p /home/esim-user/eSim-Workspace\ncp -rn /usr/local/esim/Examples/* /home/esim-user/eSim-Workspace/ 2>/dev/null || true\n\nif [ "$1" = "--vnc" ] || [ "$USE_VNC" = "1" ]; then\n echo "Starting eSim in VNC mode"\n vncserver -kill :1 2>/dev/null || true\n export XDG_RUNTIME_DIR=/tmp/runtime-esim-user\n mkdir -p $XDG_RUNTIME_DIR && chmod 700 $XDG_RUNTIME_DIR\n vncserver :1 -geometry ${VNC_RESOLUTION:-1920x1080} -depth ${VNC_DEPTH:-24} -SecurityTypes None\n sleep 3\n websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6080} localhost:5901 &\n echo "VNC ready at http://localhost:${NOVNC_PORT:-6080}/vnc.html"\n export DISPLAY=:1\n sleep 2\n cd /usr/local/esim/src/frontEnd\n python3 Application.py\n tail -f /dev/null\nelse\n echo "Starting eSim in X11 mode"\n cd /usr/local/esim/src/frontEnd\n exec python3 Application.py\nfi\n' \ + > /usr/local/bin/start-esim.sh \ + && chmod +x /usr/local/bin/start-esim.sh + +USER ${USERNAME} + +ENTRYPOINT ["/usr/local/bin/start-esim.sh"] +CMD [] diff --git a/docker-launcher/README.md b/docker-launcher/README.md new file mode 100644 index 000000000..741111278 --- /dev/null +++ b/docker-launcher/README.md @@ -0,0 +1,154 @@ +# eSim Docker + +

+ eSim Logo + eSim +

+ +

+ Run eSim anywhere using Docker - No installation required! +

+ +

+ Download Launcher • + Quick Start • + Troubleshooting +

+ +--- + +## About + +This project provides a Docker-based solution to run **eSim** (Electronic Circuit Simulation) on any operating system. eSim is developed by FOSSEE, IIT Bombay and integrates KiCad, Ngspice, and Python for circuit design and simulation. + +**What's included:** +- KiCad for schematic design +- Ngspice for SPICE simulation +- GAW3 analog waveform viewer +- All eSim libraries pre-configured + +--- + +## Quick Start + +### Step 1: Get Docker + +Download [Docker Desktop](https://www.docker.com/products/docker-desktop) and make sure it's running. + +### Step 2: Download the Launcher + +Go to [Releases](../../releases/latest) and download: +- Windows: `eSim-Launcher-Windows.exe` +- Linux: `eSim-Launcher-Linux` +- macOS: `eSim-Launcher-macOS` + +### Step 3: Run it + +**Windows:** Double-click the `.exe` file. + +**Linux:** Open terminal and run: +```bash +chmod +x eSim-Launcher-Linux +./eSim-Launcher-Linux +``` + +**macOS:** Open terminal and run: +```bash +chmod +x eSim-Launcher-macOS +./eSim-Launcher-macOS +``` + +--- + +## Display Modes + +The launcher offers two display modes: + +| Mode | Best For | How it Works | +|------|----------|--------------| +| **VNC** | Windows, macOS | Opens eSim in your browser. Works everywhere, no setup needed. | +| **X11** | Linux | Opens eSim in a native window. Best performance on Linux. | + +### Recommendations + +- **Linux** → Use X11 mode (recommended, no lag) +- **Windows** → Use VNC mode (X11 works but KiCad may lag) +- **macOS** → Use VNC mode (X11 requires XQuartz) + +Both modes work on all platforms - the launcher will guide you through any required setup. + +--- + +## Command Line Usage + +```bash +# Interactive menu +python run_esim_docker.py + +# Direct VNC mode +python run_esim_docker.py --vnc + +# Direct X11 mode +python run_esim_docker.py --x11 + +# Update image +python run_esim_docker.py --pull +``` + +--- + +## Workspace + +Your projects are saved to: + +| OS | Location | +|----|----------| +| Windows | `C:\Users\\eSim_Workspace` | +| Linux/macOS | `~/eSim_Workspace` | + +This folder is mounted into the container, so your files persist. + +--- + +## Troubleshooting + +### Docker not running +Open Docker Desktop and wait for it to fully start. + +### Browser shows "localhost not found" +Wait a few seconds and refresh. The container needs time to start. + +### VNC shows blank screen +Refresh the browser page. If still blank, restart the launcher. + +### X11 mode: window doesn't appear (Windows) +Make sure VcXsrv is running. The launcher auto-installs it if needed. + +### X11 mode: window doesn't appear (macOS) +Install XQuartz from xquartz.org, then run `xhost +localhost` in terminal. + +--- + +## Building from Source + +```bash +docker build -t esim:latest . +python run_esim_docker.py --build +``` + +--- + +## Credits + +- **eSim** - FOSSEE Team, IIT Bombay +- **KiCad** - KiCad Developers +- **Ngspice** - Ngspice Team +- **GAW3** - Hervé Quillévéré, Stefan Schippers + +Created as part of the FOSSEE Internship program. + +--- + +## License + +GPL-3.0 diff --git a/docker-launcher/assets/esim_logo.icns b/docker-launcher/assets/esim_logo.icns new file mode 100644 index 000000000..14d19d24c Binary files /dev/null and b/docker-launcher/assets/esim_logo.icns differ diff --git a/docker-launcher/assets/esim_logo.ico b/docker-launcher/assets/esim_logo.ico new file mode 100644 index 000000000..53a345281 Binary files /dev/null and b/docker-launcher/assets/esim_logo.ico differ diff --git a/docker-launcher/assets/esim_logo.png b/docker-launcher/assets/esim_logo.png new file mode 100644 index 000000000..f3d5d7ca6 Binary files /dev/null and b/docker-launcher/assets/esim_logo.png differ diff --git a/docker-launcher/assets/esim_text.png b/docker-launcher/assets/esim_text.png new file mode 100644 index 000000000..fe8fc6e38 Binary files /dev/null and b/docker-launcher/assets/esim_text.png differ diff --git a/docker-launcher/eSim.spec b/docker-launcher/eSim.spec new file mode 100644 index 000000000..55704c968 --- /dev/null +++ b/docker-launcher/eSim.spec @@ -0,0 +1,33 @@ +# PyInstaller spec file for eSim Docker Launcher +# Build: pyinstaller eSim.spec + +block_cipher = None + +a = Analysis( + ['run_esim_docker.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=['socket', 'webbrowser', 'threading', 'tempfile', 'argparse'], + hookspath=[], + runtime_hooks=[], + excludes=['tkinter', 'matplotlib', 'numpy', 'scipy', 'PIL', 'pandas', 'pytest'], + cipher=block_cipher, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='eSim-Launcher', + debug=False, + strip=False, + upx=True, + console=True, # Keep console for menu + icon='esim_logo.png' if __import__('os').path.exists('esim_logo.png') else None, +) diff --git a/docker-launcher/run_esim_docker.py b/docker-launcher/run_esim_docker.py new file mode 100644 index 000000000..9851bb9ad --- /dev/null +++ b/docker-launcher/run_esim_docker.py @@ -0,0 +1,686 @@ +""" +eSim Docker Launcher +FOSSEE IIT Bombay Internship Project + +Simple launcher to run eSim in Docker with VNC or X11 display. +""" + +import os +import sys +import platform +import subprocess +import shutil +import socket +import webbrowser +import time +import tempfile +import urllib.request +from pathlib import Path + +# Docker image (GitHub Container Registry) +DOCKER_IMAGE = "ghcr.io/barun-2005/esim-docker:latest" +LOCAL_IMAGE = "esim:latest" +CONTAINER_NAME = "esim-container" +DOCKERFILE_DIR = Path(__file__).parent.resolve() +WORKSPACE_DIR_NAME = "eSim_Workspace" + +# VcXsrv config for Windows X11 mode +VCXSRV_CONFIG = """ + +""" + + +def run_cmd(cmd, capture=False, check=True, shell=False): + if capture: + return subprocess.run(cmd, capture_output=True, text=True, check=check, shell=shell) + return subprocess.run(cmd, check=check, shell=shell) + + +def cmd_exists(cmd): + return shutil.which(cmd) is not None + + +def clear(): + os.system('cls' if os.name == 'nt' else 'clear') + + +def show_banner(): + clear() + print(""" + ╔══════════════════════════════════════════╗ + ║ eSim Docker Launcher ║ + ║ FOSSEE, IIT Bombay ║ + ╚══════════════════════════════════════════╝ + """) + + +def info(msg): + print(f" [i] {msg}") + +def ok(msg): + print(f" [+] {msg}") + +def warn(msg): + print(f" [!] {msg}") + +def err(msg): + print(f" [-] {msg}", file=sys.stderr) + + +def find_free_port(start=6080, tries=20): + for port in range(start, start + tries): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('', port)) + return port + except OSError: + continue + raise RuntimeError(f"No free port found between {start}-{start+tries}") + + +def wait_for_port(port, timeout=30): + """Wait until noVNC HTTP server is responding.""" + start = time.time() + while time.time() - start < timeout: + try: + req = urllib.request.urlopen(f"http://localhost:{port}/", timeout=2) + req.close() + return True + except: + time.sleep(1) + return False + + +def get_os(): + system = platform.system().lower() + if system == "linux": + try: + with open("/proc/version") as f: + if "microsoft" in f.read().lower(): + return "wsl2" + except: + pass + return "linux" + if system == "windows": + return "windows" + if system == "darwin": + return "macos" + return system + + +def is_wslg(): + return (get_os() == "wsl2" and + os.environ.get("WAYLAND_DISPLAY") and + Path("/mnt/wslg").exists()) + + +# Docker installation helpers + +def open_url(url): + """Open URL in browser.""" + try: + webbrowser.open(url) + return True + except: + return False + + +def install_docker_windows(): + """Check Docker on Windows - try to start it, or install if needed.""" + # First check if Docker Desktop is installed but not running + docker_paths = [ + Path(os.environ.get("PROGRAMFILES", "")) / "Docker" / "Docker" / "Docker Desktop.exe", + Path(os.environ.get("LOCALAPPDATA", "")) / "Docker" / "Docker Desktop.exe", + ] + + docker_exe = None + for p in docker_paths: + if p.exists(): + docker_exe = p + break + + if docker_exe: + info("Docker Desktop is installed but not running.") + resp = input(" Start Docker Desktop now? (y/n): ").strip().lower() + if resp == 'y': + try: + info("Starting Docker Desktop...") + subprocess.Popen([str(docker_exe)], creationflags=subprocess.DETACHED_PROCESS) + info("Docker Desktop is starting. Please wait 30-60 seconds...") + info("Then run this launcher again.") + input("\n Press Enter to exit...") + return True + except Exception as e: + err(f"Failed to start: {e}") + return False + + # Docker not installed - offer to install + info("Docker Desktop is not installed.") + print() + print(" Docker Desktop is required to run eSim.") + print() + resp = input(" Install Docker Desktop now? (y/n): ").strip().lower() + if resp != 'y': + print() + info("You can install Docker manually from:") + info("https://www.docker.com/products/docker-desktop") + return False + + if not cmd_exists("winget"): + info("Opening Docker download page...") + open_url("https://www.docker.com/products/docker-desktop") + info("Please install Docker Desktop and restart this launcher.") + return False + + try: + info("Installing Docker Desktop (this takes a few minutes)...") + subprocess.run(["winget", "install", "-e", "--id", "Docker.DockerDesktop", + "--accept-source-agreements"], check=True) + print() + ok("Docker Desktop installed!") + warn("Please RESTART your computer, then run this launcher again.") + input("\n Press Enter to exit...") + return True + except Exception as e: + err(f"Install failed: {e}") + info("Opening Docker download page...") + open_url("https://www.docker.com/products/docker-desktop") + return False + + +def guide_docker_linux(): + """Guide user to install Docker on Linux.""" + info("Docker is not running or not installed.") + print() + print(" To install Docker on Linux, run these commands:") + print() + print(" curl -fsSL https://get.docker.com | sudo sh") + print(" sudo usermod -aG docker $USER") + print(" # Then log out and log back in") + print() + print(" After installation, start Docker:") + print(" sudo systemctl start docker") + print() + resp = input(" Open Docker installation guide? (y/n): ").strip().lower() + if resp == 'y': + open_url("https://docs.docker.com/engine/install/") + + +def guide_docker_macos(): + """Guide user to install Docker on macOS.""" + info("Docker Desktop is not running or not installed.") + print() + print(" Docker Desktop is required to run eSim.") + print() + resp = input(" Open Docker download page? (y/n): ").strip().lower() + if resp == 'y': + open_url("https://www.docker.com/products/docker-desktop") + print() + info("After installing, open Docker Desktop and wait for it to start.") + + +def install_vcxsrv_windows(): + """Install VcXsrv on Windows.""" + info("VcXsrv is needed for native window mode on Windows.") + resp = input(" Install VcXsrv now? (y/n): ").strip().lower() + if resp != 'y': + return False + + if not cmd_exists("winget"): + info("Opening VcXsrv download page...") + open_url("https://sourceforge.net/projects/vcxsrv/") + return False + + try: + info("Installing VcXsrv...") + subprocess.run(["winget", "install", "-e", "--id", "marha.VcXsrv", + "--accept-source-agreements"], check=True) + ok("VcXsrv installed!") + return True + except: + err("Install failed") + open_url("https://sourceforge.net/projects/vcxsrv/") + return False + + +def start_vcxsrv(): + """Start VcXsrv X server on Windows.""" + paths = [ + Path(os.environ.get("PROGRAMFILES", "")) / "VcXsrv" / "vcxsrv.exe", + Path(os.environ.get("PROGRAMFILES(X86)", "")) / "VcXsrv" / "vcxsrv.exe", + ] + + vcxsrv = None + for p in paths: + if p.exists(): + vcxsrv = p + break + + if not vcxsrv: + if not install_vcxsrv_windows(): + return False + for p in paths: + if p.exists(): + vcxsrv = p + break + if not vcxsrv: + err("VcXsrv not found") + return False + + # Check if already running + try: + result = subprocess.run(["tasklist", "/FI", "IMAGENAME eq vcxsrv.exe"], + capture_output=True, text=True) + if "vcxsrv.exe" in result.stdout.lower(): + ok("VcXsrv already running") + return True + except: + pass + + # Write config and launch + config = Path(tempfile.gettempdir()) / "esim_xserver.xlaunch" + config.write_text(VCXSRV_CONFIG) + + info("Starting VcXsrv...") + try: + xlaunch = vcxsrv.parent / "xlaunch.exe" + if xlaunch.exists(): + subprocess.Popen([str(xlaunch), "-run", str(config)], + creationflags=subprocess.DETACHED_PROCESS) + else: + subprocess.Popen([str(vcxsrv), ":0", "-multiwindow", "-clipboard", "-wgl", "-ac"], + creationflags=subprocess.DETACHED_PROCESS) + time.sleep(2) + ok("VcXsrv started") + return True + except Exception as e: + err(f"Failed: {e}") + return False + + +def get_display_args(os_type): + """Get Docker display environment for X11 mode.""" + if os_type == "linux": + display = os.environ.get("DISPLAY", ":0") + try: + subprocess.run(["xhost", "+local:docker"], capture_output=True) + except: + pass + return display, ["-e", f"DISPLAY={display}", "-v", "/tmp/.X11-unix:/tmp/.X11-unix:rw"] + + if os_type == "wsl2": + if is_wslg(): + display = os.environ.get("DISPLAY", ":0") + return display, ["-e", f"DISPLAY={display}", "-e", "QT_QPA_PLATFORM=xcb", + "-v", "/tmp/.X11-unix:/tmp/.X11-unix:rw"] + try: + with open("/etc/resolv.conf") as f: + for line in f: + if line.startswith("nameserver"): + host_ip = line.split()[1] + break + else: + host_ip = "localhost" + except: + host_ip = "localhost" + return f"{host_ip}:0.0", ["-e", f"DISPLAY={host_ip}:0.0", "-e", "LIBGL_ALWAYS_INDIRECT=1"] + + if os_type == "windows": + return "host.docker.internal:0.0", [ + "-e", "DISPLAY=host.docker.internal:0.0", + "-e", "QT_X11_NO_MITSHM=1", "-e", "NO_AT_BRIDGE=1", "-e", "GTK_A11Y=none" + ] + + if os_type == "macos": + return "host.docker.internal:0", [ + "-e", "DISPLAY=host.docker.internal:0", "-e", "LIBGL_ALWAYS_INDIRECT=1" + ] + + return ":0", ["-e", "DISPLAY=:0"] + + +# Docker operations + +def docker_ok(): + if not cmd_exists("docker"): + return False + try: + run_cmd(["docker", "info"], capture=True) + return True + except: + return False + + +def image_exists(image): + try: + result = run_cmd(["docker", "images", "-q", image], capture=True) + return bool(result.stdout.strip()) + except: + return False + + +def pull_image(image=DOCKER_IMAGE): + info(f"Pulling {image}...") + info("This may take a few minutes on first run...") + print() + try: + subprocess.run(["docker", "pull", image], check=True) + print() + ok("Image downloaded!") + return True + except: + err("Pull failed") + return False + + +def build_image(): + dockerfile = DOCKERFILE_DIR / "Dockerfile" + if not dockerfile.exists(): + err(f"Dockerfile not found: {dockerfile}") + return False + + info("Building from Dockerfile (10-15 min)...") + print() + try: + subprocess.run(["docker", "build", "-t", LOCAL_IMAGE, str(DOCKERFILE_DIR)], check=True) + print() + ok("Build complete!") + return True + except: + err("Build failed") + return False + + +def stop_container(): + run_cmd(["docker", "rm", "-f", CONTAINER_NAME], capture=True, check=False) + + +def get_workspace(): + ws = Path.home() / WORKSPACE_DIR_NAME + ws.mkdir(exist_ok=True) + return ws + + +def get_image(build_local=False): + if build_local: + return LOCAL_IMAGE if build_image() else None + + if image_exists(DOCKER_IMAGE): + return DOCKER_IMAGE + + print() + if pull_image(DOCKER_IMAGE): + return DOCKER_IMAGE + + if image_exists(LOCAL_IMAGE): + warn(f"Using local image: {LOCAL_IMAGE}") + return LOCAL_IMAGE + + warn("Remote unavailable, trying local build...") + return LOCAL_IMAGE if build_image() else None + + +# Launch modes + +def launch_vnc(image, workspace): + """Run eSim in VNC mode (browser).""" + stop_container() + + try: + vnc_port = find_free_port(6080) + server_port = find_free_port(5901) + except RuntimeError as e: + err(str(e)) + return 1 + + cmd = [ + "docker", "run", "--rm", "-it", "--name", CONTAINER_NAME, + "--shm-size=256m", "--ipc=host", + "-v", f"{workspace}:/home/esim-user/eSim-Workspace:rw", + "-p", f"{vnc_port}:6080", "-p", f"{server_port}:5901", + "-e", "USE_VNC=1", image, "--vnc" + ] + + url = f"http://localhost:{vnc_port}/vnc.html" + + print() + print(" " + "=" * 50) + ok("Starting VNC Mode...") + print() + print(f" Browser URL: {url}") + print(f" VNC Server: localhost:{server_port}") + print() + print(" Waiting for container to start...") + print(" " + "=" * 50) + print() + + # Start container in background thread and wait for port + import threading + container_started = threading.Event() + + def run_container(): + subprocess.run(cmd) + container_started.set() + + thread = threading.Thread(target=run_container, daemon=True) + thread.start() + + # Wait for VNC port to be ready, then open browser + info("Waiting for eSim to start...") + if wait_for_port(vnc_port, timeout=45): + time.sleep(5) # Wait for noVNC and eSim to fully initialize + ok("Opening browser...") + webbrowser.open(url) + else: + warn("Container is starting slowly. Please open manually:") + print(f" {url}") + + # Wait for container to finish + thread.join() + return 0 + + +def launch_x11(image, workspace, os_type): + """Run eSim in X11 mode (native window).""" + if os_type == "windows": + if not start_vcxsrv(): + err("X11 server not available. Try VNC mode instead.") + return 1 + + if os_type == "macos": + info("Make sure XQuartz is running (download from xquartz.org)") + info("Run 'xhost +localhost' in terminal if needed") + print() + + display, display_args = get_display_args(os_type) + stop_container() + + cmd = [ + "docker", "run", "--rm", "-it", "--name", CONTAINER_NAME, + "--shm-size=256m", "--ipc=host", + "-v", f"{workspace}:/home/esim-user/eSim-Workspace:rw", + ] + display_args + [image] + + print() + print(" " + "=" * 50) + ok("Starting X11 Mode") + print(f" Display: {display}") + if os_type in ["windows", "macos"]: + print(" Note: KiCad schematic editor may lag slightly") + print(" " + "=" * 50) + print() + + return subprocess.run(cmd).returncode + + +# Menu system + +def show_menu(os_type): + """Display interactive menu based on OS.""" + show_banner() + print(f" OS: {os_type.upper()}") + print() + + if os_type == "linux": + # Linux: X11 first (recommended) + print(" 1. Launch X11 Mode (Recommended)") + print(" 2. Launch VNC Mode (Browser)") + else: + # Windows/Mac: VNC first (recommended) + print(" 1. Launch VNC Mode (Recommended - Browser)") + print(" 2. Launch X11 Mode (Native Window)") + if os_type == "windows": + print(" [KiCad may lag, auto-installs VcXsrv if needed]") + elif os_type == "macos": + print(" [Requires XQuartz, KiCad may lag]") + + print(" 3. Update Image") + print(" 4. Build from Source") + print(" 0. Exit") + print() + return input(" Choice: ").strip() + + +def handle_docker_missing(os_type): + """Handle case when Docker is not available.""" + if os_type == "windows": + install_docker_windows() + elif os_type == "linux": + guide_docker_linux() + elif os_type == "macos": + guide_docker_macos() + else: + err("Docker is required. Please install Docker Desktop.") + input("\n Press Enter to continue...") + + +def run_menu(): + """Interactive menu loop.""" + while True: + os_type = get_os() + choice = show_menu(os_type) + + if choice == "0": + print() + info("Bye!") + return 0 + + if choice not in ["1", "2", "3", "4"]: + err("Invalid choice") + input("\n Press Enter...") + continue + + # Check Docker + print() + if not docker_ok(): + handle_docker_missing(os_type) + continue + + ok("Docker ready") + workspace = get_workspace() + ok(f"Workspace: {workspace}") + + if choice == "3": + print() + pull_image(DOCKER_IMAGE) + input("\n Press Enter...") + continue + + if choice == "4": + print() + build_image() + input("\n Press Enter...") + continue + + image = get_image() + if not image: + err("No image available") + input("\n Press Enter...") + continue + + # Launch based on OS and choice + if os_type == "linux": + # Linux: 1=X11, 2=VNC + if choice == "1": + return launch_x11(image, workspace, os_type) + else: + return launch_vnc(image, workspace) + else: + # Windows/Mac: 1=VNC, 2=X11 + if choice == "1": + return launch_vnc(image, workspace) + else: + return launch_x11(image, workspace, os_type) + + return 0 + + +# CLI mode + +def run_cli(args): + import argparse + + parser = argparse.ArgumentParser(description="eSim Docker Launcher") + parser.add_argument("--vnc", "-v", action="store_true", help="VNC mode (browser)") + parser.add_argument("--x11", "-x", action="store_true", help="X11 mode (native)") + parser.add_argument("--build", "-b", action="store_true", help="Build from Dockerfile") + parser.add_argument("--pull", "-p", action="store_true", help="Force pull image") + parser.add_argument("--shell", "-s", action="store_true", help="Open shell only") + + opts = parser.parse_args(args) + os_type = get_os() + + if not docker_ok(): + handle_docker_missing(os_type) + return 1 + + workspace = get_workspace() + + if opts.pull: + if not pull_image(): + return 1 + + image = get_image(build_local=opts.build) + if not image: + err("No image available") + return 1 + + if opts.shell: + stop_container() + cmd = ["docker", "run", "--rm", "-it", "--name", CONTAINER_NAME, + "-v", f"{workspace}:/home/esim-user/eSim-Workspace:rw", + image, "/bin/bash"] + return subprocess.run(cmd).returncode + + if opts.x11: + return launch_x11(image, workspace, os_type) + + # Default: VNC for Windows/Mac, X11 for Linux + if os_type == "linux" and not opts.vnc: + return launch_x11(image, workspace, os_type) + + return launch_vnc(image, workspace) + + +# Entry point + +def main(): + if len(sys.argv) == 1: + try: + return run_menu() + except KeyboardInterrupt: + print("\n") + info("Cancelled") + return 0 + else: + show_banner() + try: + return run_cli(sys.argv[1:]) + except KeyboardInterrupt: + print("\n") + info("Cancelled") + return 0 + + +if __name__ == "__main__": + sys.exit(main())