RayMelius Claude Opus 4.6 commited on
Commit
0c8d598
Β·
1 Parent(s): 0e97339

Auto-start eunex_me when toggling C++ engine mode

Browse files

When switching to C++ mode, automatically stop Python FIX gateway,
start the eunex_me binary, wait for it to listen on the FIX port,
then connect the bridge. When switching back, stop eunex_me and
restart the Python FIX gateway. Added process lifecycle management
with PID tracking, log files, and 30s startup timeout.

UI shows 'C++ Starting...' during the switch and disables the toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. dashboard/app.py +128 -2
  2. dashboard/templates/index.html +11 -4
dashboard/app.py CHANGED
@@ -23,6 +23,8 @@ import threading
23
  import random
24
  import sys
25
  import os
 
 
26
  import urllib.request
27
  import urllib.error
28
  from collections import deque
@@ -562,6 +564,120 @@ class CppEngineBridge:
562
 
563
  cpp_bridge = CppEngineBridge()
564
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
  # ── SSE broadcasting ────────────────────────────────────────────────
567
 
@@ -598,17 +714,27 @@ def engine_mode_route():
598
  if mode not in ("python", "cpp"):
599
  return jsonify({"error": "Invalid mode"}), 400
600
  if mode == "cpp":
 
 
 
 
 
 
601
  if not cpp_bridge.connect():
602
- return jsonify({"error": "C++ engine not reachable",
 
 
603
  "host": cpp_bridge.host, "port": cpp_bridge.port}), 503
604
  else:
605
  cpp_bridge.disconnect()
 
 
606
  engine_mode = mode
607
  broadcast_event("engine_mode", {"mode": engine_mode})
608
  print(f"[ENGINE] Switched to {mode.upper()} engine")
609
  return jsonify({"mode": engine_mode})
610
  return jsonify({"mode": engine_mode,
611
- "cpp_available": cpp_bridge.is_healthy(),
612
  "cpp_host": cpp_bridge.host,
613
  "cpp_port": cpp_bridge.port})
614
 
 
23
  import random
24
  import sys
25
  import os
26
+ import signal
27
+ import subprocess
28
  import urllib.request
29
  import urllib.error
30
  from collections import deque
 
564
 
565
  cpp_bridge = CppEngineBridge()
566
 
567
+ # ── C++ engine process management ──────────────────────────────────
568
+
569
+ _PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
570
+ _PID_DIR = os.path.join(_PROJECT_ROOT, ".pids")
571
+ _EUNEX_ME_PATHS = [
572
+ os.path.join(_PROJECT_ROOT, "eunex_me"),
573
+ os.path.join(_PROJECT_ROOT, "build", "Release", "eunex_me"),
574
+ os.path.join(_PROJECT_ROOT, "build", "eunex_me"),
575
+ "/app/eunex_me",
576
+ ]
577
+
578
+ _cpp_process = None
579
+ _cpp_process_lock = threading.Lock()
580
+
581
+
582
+ def _find_eunex_me():
583
+ for p in _EUNEX_ME_PATHS:
584
+ if os.path.isfile(p) and os.access(p, os.X_OK):
585
+ return p
586
+ return None
587
+
588
+
589
+ def _kill_pid_file(name):
590
+ pidfile = os.path.join(_PID_DIR, f"{name}.pid")
591
+ if not os.path.isfile(pidfile):
592
+ return
593
+ try:
594
+ pid = int(open(pidfile).read().strip())
595
+ os.kill(pid, signal.SIGTERM)
596
+ for _ in range(20):
597
+ time.sleep(0.25)
598
+ try:
599
+ os.kill(pid, 0)
600
+ except OSError:
601
+ break
602
+ print(f"[ENGINE] Stopped {name} (PID {pid})")
603
+ except (ValueError, OSError):
604
+ pass
605
+ try:
606
+ os.remove(pidfile)
607
+ except OSError:
608
+ pass
609
+
610
+
611
+ def _start_cpp_engine():
612
+ global _cpp_process
613
+ binary = _find_eunex_me()
614
+ if not binary:
615
+ return None, "eunex_me binary not found"
616
+
617
+ _kill_pid_file("fix_gateway")
618
+
619
+ time.sleep(0.5)
620
+
621
+ with _cpp_process_lock:
622
+ if _cpp_process and _cpp_process.poll() is None:
623
+ return _cpp_process, None
624
+ log_path = os.path.join(_PROJECT_ROOT, "logs", "eunex_me.log")
625
+ os.makedirs(os.path.dirname(log_path), exist_ok=True)
626
+ log_f = open(log_path, "w")
627
+ _cpp_process = subprocess.Popen(
628
+ [binary],
629
+ stdout=log_f, stderr=subprocess.STDOUT,
630
+ cwd=_PROJECT_ROOT,
631
+ )
632
+ print(f"[ENGINE] Started eunex_me (PID {_cpp_process.pid}) β€” log: {log_path}")
633
+
634
+ for i in range(30):
635
+ time.sleep(0.5)
636
+ try:
637
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
638
+ s.settimeout(1)
639
+ s.connect(("localhost", FIX_PORT))
640
+ s.close()
641
+ print(f"[ENGINE] eunex_me ready on port {FIX_PORT}")
642
+ return _cpp_process, None
643
+ except OSError:
644
+ pass
645
+
646
+ _stop_cpp_engine()
647
+ return None, "eunex_me started but not listening within 15s"
648
+
649
+
650
+ def _stop_cpp_engine():
651
+ global _cpp_process
652
+ with _cpp_process_lock:
653
+ if _cpp_process and _cpp_process.poll() is None:
654
+ _cpp_process.terminate()
655
+ try:
656
+ _cpp_process.wait(timeout=5)
657
+ except subprocess.TimeoutExpired:
658
+ _cpp_process.kill()
659
+ print(f"[ENGINE] Stopped eunex_me (PID {_cpp_process.pid})")
660
+ _cpp_process = None
661
+
662
+
663
+ def _restart_fix_gateway():
664
+ python = sys.executable
665
+ fix_script = os.path.join(_PROJECT_ROOT, "fix_gateway", "fix_server.py")
666
+ if not os.path.isfile(fix_script):
667
+ return
668
+ log_path = os.path.join(_PROJECT_ROOT, "logs", "fix_gateway.log")
669
+ os.makedirs(os.path.dirname(log_path), exist_ok=True)
670
+ log_f = open(log_path, "w")
671
+ proc = subprocess.Popen(
672
+ [python, fix_script],
673
+ stdout=log_f, stderr=subprocess.STDOUT,
674
+ cwd=_PROJECT_ROOT,
675
+ )
676
+ os.makedirs(_PID_DIR, exist_ok=True)
677
+ with open(os.path.join(_PID_DIR, "fix_gateway.pid"), "w") as f:
678
+ f.write(str(proc.pid))
679
+ print(f"[ENGINE] Restarted fix_gateway (PID {proc.pid})")
680
+
681
 
682
  # ── SSE broadcasting ────────────────────────────────────────────────
683
 
 
714
  if mode not in ("python", "cpp"):
715
  return jsonify({"error": "Invalid mode"}), 400
716
  if mode == "cpp":
717
+ if not _find_eunex_me():
718
+ return jsonify({"error": "eunex_me binary not found",
719
+ "hint": "Build it: cd build && cmake .. && cmake --build ."}), 400
720
+ proc, err = _start_cpp_engine()
721
+ if err:
722
+ return jsonify({"error": err}), 503
723
  if not cpp_bridge.connect():
724
+ _stop_cpp_engine()
725
+ _restart_fix_gateway()
726
+ return jsonify({"error": "C++ engine started but FIX handshake failed",
727
  "host": cpp_bridge.host, "port": cpp_bridge.port}), 503
728
  else:
729
  cpp_bridge.disconnect()
730
+ _stop_cpp_engine()
731
+ _restart_fix_gateway()
732
  engine_mode = mode
733
  broadcast_event("engine_mode", {"mode": engine_mode})
734
  print(f"[ENGINE] Switched to {mode.upper()} engine")
735
  return jsonify({"mode": engine_mode})
736
  return jsonify({"mode": engine_mode,
737
+ "cpp_available": _find_eunex_me() is not None,
738
  "cpp_host": cpp_bridge.host,
739
  "cpp_port": cpp_bridge.port})
740
 
dashboard/templates/index.html CHANGED
@@ -966,9 +966,12 @@ function fmt(v) { return v > 0 ? parseFloat(v).toFixed(2) : '-'; }
966
  async function toggleEngine(useCpp) {
967
  const mode = useCpp ? 'cpp' : 'python';
968
  const sw = document.getElementById('engineSwitch');
 
 
 
969
  try {
970
  const ctrl = new AbortController();
971
- setTimeout(() => ctrl.abort(), 10000);
972
  const resp = await fetch('/engine/mode', {
973
  method: 'POST',
974
  headers: {'Content-Type': 'application/json'},
@@ -979,17 +982,21 @@ async function toggleEngine(useCpp) {
979
  if (!text.startsWith('{')) throw new Error('Server not ready');
980
  const data = JSON.parse(text);
981
  if (!resp.ok) {
982
- alert('C++ engine not available:\n' + (data.error || 'Connection failed') +
983
- '\n\nStart eunex_me first:\n ./build/Release/eunex_me');
984
  sw.checked = false;
985
  updateEngineUI('python');
986
  return;
987
  }
988
  updateEngineUI(data.mode);
989
  } catch(e) {
990
- alert('Failed to switch engine: ' + e.message);
 
991
  sw.checked = false;
992
  updateEngineUI('python');
 
 
 
993
  }
994
  }
995
 
 
966
  async function toggleEngine(useCpp) {
967
  const mode = useCpp ? 'cpp' : 'python';
968
  const sw = document.getElementById('engineSwitch');
969
+ sw.disabled = true;
970
+ const labelR = document.getElementById('engineLabelR');
971
+ if (useCpp) labelR.textContent = 'C++ Starting…';
972
  try {
973
  const ctrl = new AbortController();
974
+ setTimeout(() => ctrl.abort(), 30000);
975
  const resp = await fetch('/engine/mode', {
976
  method: 'POST',
977
  headers: {'Content-Type': 'application/json'},
 
982
  if (!text.startsWith('{')) throw new Error('Server not ready');
983
  const data = JSON.parse(text);
984
  if (!resp.ok) {
985
+ alert('Engine switch failed:\n' + (data.error || 'Connection failed') +
986
+ (data.hint ? '\n\n' + data.hint : ''));
987
  sw.checked = false;
988
  updateEngineUI('python');
989
  return;
990
  }
991
  updateEngineUI(data.mode);
992
  } catch(e) {
993
+ const msg = e.name === 'AbortError' ? 'Timeout β€” engine took too long to start' : e.message;
994
+ alert('Failed to switch engine: ' + msg);
995
  sw.checked = false;
996
  updateEngineUI('python');
997
+ } finally {
998
+ sw.disabled = false;
999
+ labelR.textContent = 'C++';
1000
  }
1001
  }
1002