#1501·DeepTutor

[Bug]: [Windows] Detached launcher worker spawns console tools and children without CREATE_NO_WINDOW — empty console windows flash on screen, and closing one kills the whole app

Author: charlesliang-codeCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbug

Do you need to file an issue?

  • I have searched the existing issues and this bug is not already filed.
  • I believe this is a legitimate bug, not just a question or feature request.

Describe the bug

On Windows, when DeepTutor runs through its detached launcher worker (desktop shortcut / detached start), several code paths in deeptutor/runtime/launcher.py spawn console subprocesses without CREATE_NO_WINDOW. Two user-visible consequences:

  1. Empty console windows appear on screen. The detached worker itself has no console (DETACHED_PROCESS), so every console tool it spawns (taskkill, netstat, tasklist, npm) makes Windows allocate a brand-new console window. Non-zero exits from those tools (e.g. taskkill returning 128 for an already-dead PID) additionally leave an empty OpenConsole window behind when Windows Terminal uses its default closeOnExit=graceful.
  2. Closing the frontend's console window takes the web app down. The frontend (node) child gets its own console, hosted in a visible Windows Terminal window titled next-server (v16.x) with an otherwise empty body. Closing that window sends a console-close signal to node: it dies with exit code 3221225786 (0xC000013A, CTRL_C_EXIT), and the launcher — seeing its managed frontend exit — then deliberately stops the backend. Controlled experiment (details under Logs):
Open http://localhost:3782 in your browser.
frontend exited with code 3221225786
Stopping backend (PID 14168)

3221225786 = 0xC000013A (CTRL_C_EXIT). The backend is then torn down as well — the whole app dies from an accidental window close.

Affected spawn sites in deeptutor/runtime/launcher.py (v1.6.8):

# Site What it spawns
1 _spawn() (~line 279) backend (uvicorn) / frontend (node) — only CREATE_NEW_PROCESS_GROUP is set
2 stop path (~line 211) taskkill /PID <pid> /T
3 port probing (~line 331) netstat -ano -p tcp
4 PID probing (~line 357) tasklist /FI "PID eq <pid>" /FO CSV /NH
5 npm invocations (~lines 665, 761) npm <action> / npm run build

The detached worker launch itself (~line 1084, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP) is correct — but being console-less is exactly why every child/tool spawn needs CREATE_NO_WINDOW explicitly.

This complements the earlier Windows launcher fixes (#1147/#1177/#702 — signal handling, process probes, code pages, --detach): those addressed the signal layer; the window allocation layer still reproduces on v1.6.8.

Steps to reproduce

  1. Windows 11, start DeepTutor detached (desktop shortcut or detached worker).
  2. Watch the desktop/taskbar while the launcher runs its port/PID probes and health checks: empty console windows flash, and some linger as empty OpenConsole windows.
  3. Identify the persistent console window belonging to the frontend (a Windows Terminal/OpenConsole host titled next-server (v16.x), created the same second as the node process).
  4. Close it (clicking the X, or equivalently posting WM_CLOSE to its window handle).
  5. Log shows frontend exited with code 3221225786 + Stopping backend (PID ...); ports 3782 (frontend) and 8001 (backend) both go down.

Reproduces on every detached start; the stray windows appear within seconds of launch. Verified in a controlled experiment on v1.6.8 with the CREATE_NO_WINDOW flags removed and everything else stock.

Expected Behavior

  • No empty console windows should be allocated by the detached worker's tool calls or child spawns.
  • Closing a stray console window must never stop the app.

Suggested fix (reference implementation)

Pass CREATE_NO_WINDOW (0x08000000) in addition to the existing flags, at all five sites. Minimal diff shape:

python
# 1) _spawn() — children (backend/frontend)
if os.name == "nt":
    kwargs["creationflags"] = (
        subprocess.CREATE_NEW_PROCESS_GROUP
        | subprocess.CREATE_NO_WINDOW          # keep consoles from being allocated
    )
python
# 2)-5) console tools (taskkill / netstat / tasklist / npm)
subprocess.run(
    cmd,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
    check=False,
    creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
)

CREATE_NO_WINDOW is a no-op for console-attached parents and only suppresses console allocation for children of the console-less detached worker; CREATE_NEW_PROCESS_GROUP is kept for group management and the existing stop path. Applied locally at all five sites: window flashing gone, deeptutor stop still works, Ctrl+C in foreground mode unaffected.

Cross-platform safety (why the conditional matters):

  • subprocess.CREATE_NO_WINDOW only exists on Windows (CPython defines it inside the _mswindows branch, imported from _winapi); on POSIX a bare reference raises AttributeError. Additionally, POSIX Popen._execute_child raises ValueError("creationflags is only supported on Windows platforms") for any non-zero creationflags — but 0 is accepted, so creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 (or equivalently getattr(subprocess, "CREATE_NO_WINDOW", 0)) is a no-op on Linux/macOS and byte-identical in behavior to today.
  • Site 1 needs no extra guard: it sits inside the existing if os.name == "nt": branch; the POSIX branch uses start_new_session=True and stays untouched.
  • Windows-specific behavior note: CREATE_NO_WINDOW gives the child an invisible console (not "no console"); stdio pipes are unaffected, taskkill /T-based stopping is unaffected (verified across multiple stop/restart cycles with the fix applied). In foreground mode Ctrl+C is handled by the launcher (which then stops children via the existing stop path) rather than console-event propagation — net user-visible behavior unchanged.
  • Without the conditional, the tool-invocation sites (netstat/tasklist probing runs harmlessly on Linux today via shutil.which + check=False) would turn a silent no-op into an AttributeError — so the Windows-conditional form is required, not optional.

Related Module

API/Backend

Configuration Used

Default deeptutor start (detached worker), Windows 11, Python 3.11 venv install, Node 22 frontend. Windows Terminal as default terminal host.

Logs and screenshots

Full incident sequence from launcher.log (identical every time):

Open http://localhost:3782 in your browser.
frontend exited with code 3221225786
Stopping backend (PID 14168)

Controlled experiment on v1.6.8 (stock except for the described flags): after a detached start, the frontend console window (Windows Terminal host, title next-server (v16.2.3), same creation second as the node PID) was closed programmatically via PostMessage(hwnd, WM_CLOSE) — the exact message the X button delivers:

--- after WM_CLOSE ---
(node processes: none remain)
listening ports: only 127.0.0.1:8090 (my standalone embedding server) remains

log:
frontend exited with code 3221225786
Stopping backend (PID 34160)

Scope note: the frontend dies from the console-close signal and the launcher then stops the backend itself (managed-process shutdown); the standalone embedding server, which is not a launcher child, survives. So an accidental window close takes down the web app (frontend + backend) but is recoverable by restarting DeepTutor.

Additional Information

  • DeepTutor Version: v1.6.8 (PyPI)
  • Operating System: Windows 11 (10.0.22631), Windows Terminal as default host
  • Python Version: 3.11
  • Node.js Version: 22.x
  • Browser (if applicable): Chrome (irrelevant to trigger)
  • Related Issues: #1147, #1177, #702 (same launcher/Windows family; those fixes shipped in v1.6.3 but the window-allocation layer remains)