Auto-start eunex_me when toggling C++ engine mode
Browse filesWhen 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>
- dashboard/app.py +128 -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 |
-
|
|
|
|
|
|
|
| 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":
|
| 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(),
|
| 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('
|
| 983 |
-
'\n\
|
| 984 |
sw.checked = false;
|
| 985 |
updateEngineUI('python');
|
| 986 |
return;
|
| 987 |
}
|
| 988 |
updateEngineUI(data.mode);
|
| 989 |
} catch(e) {
|
| 990 |
-
|
|
|
|
| 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 |
|