6.6 KiB
6.6 KiB
Summary of Changes
Problem Fixed
PLC2 crashed at startup when attempting Modbus TCP write to PLC1 before PLC1 was ready, causing ConnectionRefusedError and container crash.
Files Changed
1. tools/compile_ir.py (CRITICAL FIX)
Location: Lines 17-37 in render_plc_rules() function
Changes:
- Added
import timeto generated PLC logic files - Added
_safe_callback()function with retry logic (30 retries × 0.2s = 6s) - Modified
_write()to call_safe_callback(cbs[key])instead of directcbs[key]()
Impact: All generated PLC logic files now include safe callback wrapper that prevents crashes from connection failures.
2. build_scenario.py (NEW FILE)
Purpose: Deterministic scenario builder that uses correct Python venv
Features:
- Uses
sys.executableto ensure correct Python interpreter - Orchestrates: configuration.json → IR → logic/*.py → validation
- Creates complete scenario directory at
outputs/scenario_run/ - Validates all generated files
Usage:
.venv/bin/python3 build_scenario.py --out outputs/scenario_run --overwrite
3. test_simlab.sh (NEW FILE)
Purpose: Interactive ICS-SimLab test launcher
Usage:
./test_simlab.sh
4. diagnose_runtime.sh (NEW FILE)
Purpose: Diagnostic script to check scenario files and Docker state
Usage:
./diagnose_runtime.sh
5. RUNTIME_FIX.md (NEW FILE)
Purpose: Complete documentation of the fix, testing procedures, and troubleshooting
Testing Commands
Build Scenario
.venv/bin/python3 build_scenario.py --out outputs/scenario_run --overwrite
Verify Fix
# Should show _safe_callback function
grep -A5 "_safe_callback" outputs/scenario_run/logic/plc2.py
Run ICS-SimLab
cd ~/projects/ICS-SimLab-main/curtin-ics-simlab
sudo ./start.sh ~/projects/ics-simlab-config-gen_claude/outputs/scenario_run
Monitor PLC2 Logs
# Find container name
sudo docker ps | grep plc2
# View logs (look for: NO "Exception in thread" errors)
sudo docker logs <plc2_container_name> -f
Stop ICS-SimLab
cd ~/projects/ICS-SimLab-main/curtin-ics-simlab
sudo ./stop.sh
Expected Runtime Behavior
Before Fix
PLC2 container:
Exception in thread Thread-1:
Traceback (most recent call last):
...
ConnectionRefusedError: [Errno 111] Connection refused
[Container crashes]
After Fix (Success Case)
PLC2 container:
[Silent retries for ~6 seconds]
[Normal operation once PLC1 is ready]
[No exceptions, no crashes]
After Fix (PLC1 Never Starts)
PLC2 container:
WARNING: Callback failed after 30 attempts: [Errno 111] Connection refused
[Container continues running]
[Retries on next write attempt]
Code Diff
tools/compile_ir.py
# BEFORE (lines 17-37):
def render_plc_rules(plc_name: str, rules: List[object]) -> str:
lines = []
lines.append('"""\n')
lines.append(f"PLC logic for {plc_name}: IR-compiled rules.\n\n")
lines.append("Autogenerated by ics-simlab-config-gen (IR compiler).\n")
lines.append('"""\n\n')
lines.append("from typing import Any, Callable, Dict\n\n\n")
lines.append("def _get_float(regs: Dict[str, Any], key: str, default: float = 0.0) -> float:\n")
lines.append(" try:\n")
lines.append(" return float(regs[key]['value'])\n")
lines.append(" except Exception:\n")
lines.append(" return float(default)\n\n\n")
lines.append("def _write(out_regs: Dict[str, Any], cbs: Dict[str, Callable[[], None]], key: str, value: int) -> None:\n")
lines.append(" if key not in out_regs:\n")
lines.append(" return\n")
lines.append(" cur = out_regs[key].get('value', None)\n")
lines.append(" if cur == value:\n")
lines.append(" return\n")
lines.append(" out_regs[key]['value'] = value\n")
lines.append(" if key in cbs:\n")
lines.append(" cbs[key]()\n\n\n") # <-- CRASHES HERE
# AFTER (lines 17-46):
def render_plc_rules(plc_name: str, rules: List[object]) -> str:
lines = []
lines.append('"""\n')
lines.append(f"PLC logic for {plc_name}: IR-compiled rules.\n\n")
lines.append("Autogenerated by ics-simlab-config-gen (IR compiler).\n")
lines.append('"""\n\n')
lines.append("import time\n") # <-- ADDED
lines.append("from typing import Any, Callable, Dict\n\n\n")
lines.append("def _get_float(regs: Dict[str, Any], key: str, default: float = 0.0) -> float:\n")
lines.append(" try:\n")
lines.append(" return float(regs[key]['value'])\n")
lines.append(" except Exception:\n")
lines.append(" return float(default)\n\n\n")
# ADDED: Safe callback wrapper
lines.append("def _safe_callback(cb: Callable[[], None], retries: int = 30, delay: float = 0.2) -> None:\n")
lines.append(" \"\"\"Invoke callback with retry logic to handle startup race conditions.\"\"\"\n")
lines.append(" for attempt in range(retries):\n")
lines.append(" try:\n")
lines.append(" cb()\n")
lines.append(" return\n")
lines.append(" except Exception as e:\n")
lines.append(" if attempt == retries - 1:\n")
lines.append(" print(f\"WARNING: Callback failed after {retries} attempts: {e}\")\n")
lines.append(" return\n")
lines.append(" time.sleep(delay)\n\n\n")
lines.append("def _write(out_regs: Dict[str, Any], cbs: Dict[str, Callable[[], None]], key: str, value: int) -> None:\n")
lines.append(" if key not in out_regs:\n")
lines.append(" return\n")
lines.append(" cur = out_regs[key].get('value', None)\n")
lines.append(" if cur == value:\n")
lines.append(" return\n")
lines.append(" out_regs[key]['value'] = value\n")
lines.append(" if key in cbs:\n")
lines.append(" _safe_callback(cbs[key])\n\n\n") # <-- NOW SAFE
Validation Checklist
- Fix implemented in
tools/compile_ir.py - Build script created (
build_scenario.py) - Build script uses correct venv (
sys.executable) - Generated files include
_safe_callback() - Generated files call
_safe_callback(cbs[key])notcbs[key]() - Only uses stdlib (
time.sleep) - Never raises from callbacks
- Preserves PLC logic contract (no signature changes)
- Test scripts created
- Documentation created
Next Steps
- Run
./diagnose_runtime.shto verify scenario files - Run
./test_simlab.shto start ICS-SimLab - Monitor PLC2 logs for crashes (should see none)
- Verify callbacks eventually succeed once PLC1 is ready