esiaccel 0.1.5.dev205__cp39-cp39-win_amd64.whl → 0.1.5.dev533__cp39-cp39-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. esiaccel/CosimBackend.dll +0 -0
  2. esiaccel/CosimBackend.lib +0 -0
  3. esiaccel/ESICppRuntime.dll +0 -0
  4. esiaccel/ESICppRuntime.lib +0 -0
  5. esiaccel/EsiCosimDpiServer.dll +0 -0
  6. esiaccel/EsiCosimDpiServer.lib +0 -0
  7. esiaccel/MtiPli.dll +0 -0
  8. esiaccel/MtiPli.lib +0 -0
  9. esiaccel/__init__.py +12 -3
  10. esiaccel/abseil_dll.dll +0 -0
  11. esiaccel/accelerator.py +37 -5
  12. esiaccel/cares.dll +0 -0
  13. esiaccel/codegen.py +3 -3
  14. esiaccel/cosim/Cosim_Endpoint.sv +0 -18
  15. esiaccel/cosim/driver.cpp +6 -6
  16. esiaccel/cosim/driver.sv +14 -0
  17. esiaccel/cosim/questa.py +67 -4
  18. esiaccel/cosim/simulator.py +185 -38
  19. esiaccel/cosim/verilator.py +36 -5
  20. esiaccel/esiCppAccel.cp312-win_amd64.pyd +0 -0
  21. esiaccel/esiquery.exe +0 -0
  22. esiaccel/include/esi/Accelerator.h +3 -16
  23. esiaccel/include/esi/CLI.h +5 -5
  24. esiaccel/include/esi/Common.h +11 -1
  25. esiaccel/include/esi/Context.h +17 -9
  26. esiaccel/include/esi/Design.h +9 -4
  27. esiaccel/include/esi/Manifest.h +0 -2
  28. esiaccel/include/esi/Ports.h +72 -15
  29. esiaccel/include/esi/Services.h +50 -18
  30. esiaccel/include/esi/Types.h +108 -31
  31. esiaccel/include/esi/Values.h +313 -0
  32. esiaccel/libcrypto-3-x64.dll +0 -0
  33. esiaccel/libprotobuf.dll +0 -0
  34. esiaccel/libssl-3-x64.dll +0 -0
  35. esiaccel/re2.dll +0 -0
  36. esiaccel/types.py +6 -4
  37. esiaccel/zlib1.dll +0 -0
  38. {esiaccel-0.1.5.dev205.dist-info → esiaccel-0.1.5.dev533.dist-info}/METADATA +1 -1
  39. esiaccel-0.1.5.dev533.dist-info/RECORD +54 -0
  40. esiaccel/esiCppAccel.cp39-win_amd64.pyd +0 -0
  41. esiaccel-0.1.5.dev205.dist-info/RECORD +0 -46
  42. {esiaccel-0.1.5.dev205.dist-info → esiaccel-0.1.5.dev533.dist-info}/WHEEL +0 -0
  43. {esiaccel-0.1.5.dev205.dist-info → esiaccel-0.1.5.dev533.dist-info}/entry_points.txt +0 -0
  44. {esiaccel-0.1.5.dev205.dist-info → esiaccel-0.1.5.dev533.dist-info}/licenses/LICENSE +0 -0
  45. {esiaccel-0.1.5.dev205.dist-info → esiaccel-0.1.5.dev533.dist-info}/top_level.txt +0 -0
esiaccel/CosimBackend.dll CHANGED
Binary file
esiaccel/CosimBackend.lib CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
esiaccel/MtiPli.dll CHANGED
Binary file
esiaccel/MtiPli.lib CHANGED
Binary file
esiaccel/__init__.py CHANGED
@@ -3,14 +3,15 @@
3
3
  # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
4
  import sys
5
5
  import os
6
- from .accelerator import AcceleratorConnection
6
+ from .accelerator import AcceleratorConnection, Context, LogLevel
7
7
 
8
8
  from .esiCppAccel import (AppID, Type, BundleType, ChannelType, ArrayType,
9
9
  StructType, BitsType, UIntType, SIntType)
10
10
 
11
11
  __all__ = [
12
- "AcceleratorConnection", "AppID", "Type", "BundleType", "ChannelType",
13
- "ArrayType", "StructType", "BitsType", "UIntType", "SIntType"
12
+ "AcceleratorConnection", "AppID", "Context", "LogLevel", "Type",
13
+ "BundleType", "ChannelType", "ArrayType", "StructType", "BitsType",
14
+ "UIntType", "SIntType"
14
15
  ]
15
16
 
16
17
  if sys.platform == "win32":
@@ -20,3 +21,11 @@ if sys.platform == "win32":
20
21
  """
21
22
  from .utils import get_dll_dir
22
23
  os.add_dll_directory(str(get_dll_dir()))
24
+
25
+
26
+ def connect(platform: str, connection_str: str) -> "AcceleratorConnection":
27
+ """Connect to an accelerator using the specified platform and connection
28
+ string."""
29
+ ctxt = Context.default()
30
+ return AcceleratorConnection(ctxt,
31
+ ctxt.cpp_ctxt.connect(platform, connection_str))
Binary file
esiaccel/accelerator.py CHANGED
@@ -15,19 +15,51 @@ from typing import Dict, List, Optional
15
15
  from .types import BundlePort
16
16
  from . import esiCppAccel as cpp
17
17
 
18
- # Global context for the C++ side.
19
- ctxt = cpp.Context()
18
+ LogLevel = cpp.LogLevel
19
+
20
+
21
+ class Context:
22
+ """A context for ESI accelerator connections. The underlying C++ context owns
23
+ everything assocated with it including types, accelerator connections, and
24
+ the accelerator facade/interface (aka Accelerator) itself. It must not be
25
+ garbage collected while any accelerators or connections that it owns are still
26
+ in use as they will be disconnected and destroyed when the context is
27
+ destroyed."""
28
+
29
+ _default: Optional["Context"] = None
30
+
31
+ def __init__(self, log_level: cpp.LogLevel = cpp.LogLevel.Warning):
32
+ self.cpp_ctxt = cpp.Context()
33
+ self.set_stdio_logger(log_level)
34
+
35
+ @staticmethod
36
+ def default() -> "Context":
37
+ if Context._default is None:
38
+ Context._default = Context()
39
+ return Context._default
40
+
41
+ def set_stdio_logger(self, level: cpp.LogLevel):
42
+ self.cpp_ctxt.set_stdio_logger(level)
43
+
44
+ def connect(self, platform: str,
45
+ connection_str: str) -> "AcceleratorConnection":
46
+ return AcceleratorConnection(
47
+ self, self.cpp_ctxt.connect(platform, connection_str))
20
48
 
21
49
 
22
50
  class AcceleratorConnection:
23
51
  """A connection to an ESI accelerator."""
24
52
 
25
- def __init__(self, platform: str, connection_str: str):
26
- self.cpp_accel = cpp.AcceleratorConnection(ctxt, platform, connection_str)
53
+ def __init__(self, ctxt: Context, cpp_accel: cpp.AcceleratorConnection):
54
+ if not isinstance(ctxt, Context):
55
+ raise TypeError("ctxt must be a Context")
56
+ self.ctxt = ctxt
57
+ self.cpp_accel = cpp_accel
27
58
 
28
59
  def manifest(self) -> cpp.Manifest:
29
60
  """Get and parse the accelerator manifest."""
30
- return cpp.Manifest(ctxt, self.cpp_accel.sysinfo().json_manifest())
61
+ return cpp.Manifest(self.ctxt.cpp_ctxt,
62
+ self.cpp_accel.sysinfo().json_manifest())
31
63
 
32
64
  def sysinfo(self) -> cpp.SysInfo:
33
65
  return self.cpp_accel.sysinfo()
esiaccel/cares.dll ADDED
Binary file
esiaccel/codegen.py CHANGED
@@ -6,7 +6,7 @@
6
6
  # with the runtime, though it is intended to be extensible for other languages.
7
7
 
8
8
  from typing import List, TextIO, Type, Optional
9
- from .accelerator import AcceleratorConnection
9
+ from .accelerator import AcceleratorConnection, Context
10
10
  from .esiCppAccel import ModuleInfo
11
11
  from . import types
12
12
 
@@ -171,12 +171,12 @@ def run(generator: Type[Generator] = CppGenerator,
171
171
 
172
172
  conn: AcceleratorConnection
173
173
  if args.file is not None:
174
- conn = AcceleratorConnection("trace", f"-:{args.file}")
174
+ conn = Context.default().connect("trace", f"-:{args.file}")
175
175
  elif args.platform is not None:
176
176
  if args.connection is None:
177
177
  print("Must specify --connection with --platform")
178
178
  return 1
179
- conn = AcceleratorConnection(args.platform, args.connection)
179
+ conn = Context.default().connect(args.platform, args.connection)
180
180
  else:
181
181
  print("Must specify either --file or --platform")
182
182
  return 1
@@ -82,15 +82,6 @@ module Cosim_Endpoint_ToHost
82
82
  TO_HOST_SIZE_BYTES_FLOOR_IN_BITS];
83
83
  endgenerate
84
84
 
85
- initial begin
86
- $display("TO_HOST_SIZE_BITS: %d", TO_HOST_SIZE_BITS);
87
- $display("TO_HOST_SIZE_BYTES: %d", TO_HOST_SIZE_BYTES);
88
- $display("TO_HOST_SIZE_BITS_DIFF: %d", TO_HOST_SIZE_BITS_DIFF);
89
- $display("TO_HOST_SIZE_BYTES_FLOOR: %d", TO_HOST_SIZE_BYTES_FLOOR);
90
- $display("TO_HOST_SIZE_BYTES_FLOOR_IN_BITS: %d",
91
- TO_HOST_SIZE_BYTES_FLOOR_IN_BITS);
92
- end
93
-
94
85
  endmodule
95
86
 
96
87
  module Cosim_Endpoint_FromHost
@@ -224,13 +215,4 @@ module Cosim_Endpoint_FromHost
224
215
  assign DataOutValid = DataOut_x_valid;
225
216
  assign DataOut = DataOut_x;
226
217
 
227
- initial begin
228
- $display("FROM_HOST_SIZE_BITS: %d", FROM_HOST_SIZE_BITS);
229
- $display("FROM_HOST_SIZE_BYTES: %d", FROM_HOST_SIZE_BYTES);
230
- $display("FROM_HOST_SIZE_BITS_DIFF: %d", FROM_HOST_SIZE_BITS_DIFF);
231
- $display("FROM_HOST_SIZE_BYTES_FLOOR: %d", FROM_HOST_SIZE_BYTES_FLOOR);
232
- $display("FROM_HOST_SIZE_BYTES_FLOOR_IN_BITS: %d",
233
- FROM_HOST_SIZE_BYTES_FLOOR_IN_BITS);
234
- end
235
-
236
218
  endmodule
esiaccel/cosim/driver.cpp CHANGED
@@ -30,7 +30,7 @@
30
30
  #include CONCAT3(V,TOP_MODULE,.h)
31
31
  // clang-format on
32
32
 
33
- #include "verilated_vcd_c.h"
33
+ #include "verilated_fst_c.h"
34
34
 
35
35
  #include "signal.h"
36
36
  #include <iostream>
@@ -64,12 +64,12 @@ int main(int argc, char **argv) {
64
64
  }
65
65
 
66
66
  #ifdef TRACE
67
- VerilatedVcdC *tfp = nullptr;
67
+ VerilatedFstC *tfp = nullptr;
68
68
  #endif
69
69
 
70
70
  if (waveformFile) {
71
71
  #ifdef TRACE
72
- tfp = new VerilatedVcdC();
72
+ tfp = new VerilatedFstC();
73
73
  Verilated::traceEverOn(true);
74
74
  dut.trace(tfp, 99); // Trace 99 levels of hierarchy
75
75
  tfp->open(waveformFile);
@@ -95,11 +95,11 @@ int main(int argc, char **argv) {
95
95
  // Run for a few cycles with reset held.
96
96
  for (timeStamp = 0; timeStamp < 8 && !Verilated::gotFinish(); timeStamp++) {
97
97
  dut.eval();
98
- dut.clk = !dut.clk;
99
98
  #ifdef TRACE
100
99
  if (tfp)
101
100
  tfp->dump(timeStamp);
102
101
  #endif
102
+ dut.clk = !dut.clk;
103
103
  }
104
104
 
105
105
  // Take simulation out of reset.
@@ -108,12 +108,12 @@ int main(int argc, char **argv) {
108
108
  // Run for the specified number of cycles out of reset.
109
109
  for (; !Verilated::gotFinish() && !stopSimulation; timeStamp++) {
110
110
  dut.eval();
111
- dut.clk = !dut.clk;
112
-
113
111
  #ifdef TRACE
114
112
  if (tfp)
115
113
  tfp->dump(timeStamp);
116
114
  #endif
115
+ dut.clk = !dut.clk;
116
+
117
117
  if (debugPeriod)
118
118
  std::this_thread::sleep_for(std::chrono::milliseconds(debugPeriod));
119
119
  }
esiaccel/cosim/driver.sv CHANGED
@@ -18,8 +18,22 @@
18
18
  `define TOP_MODULE ESI_Cosim_Top
19
19
  `endif
20
20
 
21
+ // Allow reading environment variables to control waveform dumping.
22
+ import "DPI-C" function string getenv(input string env_name);
23
+
21
24
  module driver();
22
25
 
26
+ // If the SAVE_WAVE environment variable is set, dump a VCD waveform to that
27
+ // filename.
28
+ initial begin
29
+ string save_wave = getenv("SAVE_WAVE");
30
+ if (save_wave != "") begin
31
+ $display("[driver] Saving waveform to %s", save_wave);
32
+ $dumpfile(save_wave);
33
+ $dumpvars(0, driver);
34
+ end
35
+ end
36
+
23
37
  logic clk = 0;
24
38
  logic rst = 0;
25
39
 
esiaccel/cosim/questa.py CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  import os
6
6
  from pathlib import Path
7
- from typing import List
7
+ from typing import List, Optional, Callable, Dict
8
8
 
9
- from .simulator import CosimCollateralDir, Simulator
9
+ from .simulator import CosimCollateralDir, Simulator, SourceFiles
10
10
 
11
11
 
12
12
  class Questa(Simulator):
@@ -14,6 +14,32 @@ class Questa(Simulator):
14
14
 
15
15
  DefaultDriver = CosimCollateralDir / "driver.sv"
16
16
 
17
+ def __init__(
18
+ self,
19
+ sources: SourceFiles,
20
+ run_dir: Path,
21
+ debug: bool,
22
+ run_stdout_callback: Optional[Callable[[str], None]] = None,
23
+ run_stderr_callback: Optional[Callable[[str], None]] = None,
24
+ compile_stdout_callback: Optional[Callable[[str], None]] = None,
25
+ compile_stderr_callback: Optional[Callable[[str], None]] = None,
26
+ make_default_logs: bool = True,
27
+ macro_definitions: Optional[Dict[str, str]] = None,
28
+ # An optional list of questa error codes to suppress
29
+ suppressed_questa_errors: Optional[List[int]] = None):
30
+ super().__init__(
31
+ sources=sources,
32
+ run_dir=run_dir,
33
+ debug=debug,
34
+ run_stdout_callback=run_stdout_callback,
35
+ run_stderr_callback=run_stderr_callback,
36
+ compile_stdout_callback=compile_stdout_callback,
37
+ compile_stderr_callback=compile_stderr_callback,
38
+ make_default_logs=make_default_logs,
39
+ macro_definitions=macro_definitions,
40
+ )
41
+ self.suppressed_questa_errors = suppressed_questa_errors
42
+
17
43
  # Questa doesn't use stderr for error messages. Everything goes to stdout.
18
44
  UsesStderr = False
19
45
 
@@ -23,9 +49,27 @@ class Questa(Simulator):
23
49
  ]
24
50
  sources = self.sources.rtl_sources
25
51
  sources.append(Questa.DefaultDriver)
52
+
53
+ # Format macro definition command
54
+ if self.macro_definitions:
55
+ macro_definitions_cmd = " ".join([
56
+ f"+define+{k}={v}" if v is not None else f"+define+{k}"
57
+ for k, v in self.macro_definitions.items()
58
+ ])
59
+ else:
60
+ macro_definitions_cmd = ""
61
+
62
+ # Format error suppression command
63
+ if self.suppressed_questa_errors:
64
+ suppressed_questa_errors_cmd = " ".join(
65
+ [f"-suppress {ec}" for ec in self.suppressed_questa_errors])
66
+ else:
67
+ suppressed_questa_errors_cmd = ""
68
+
26
69
  for src in sources:
27
- cmds.append(f"vlog -incr +acc -sv +define+TOP_MODULE={self.sources.top}"
28
- f" +define+SIMULATION {str(src)}")
70
+ cmds.append(
71
+ f"vlog -incr +acc -sv {macro_definitions_cmd} {suppressed_questa_errors_cmd} +define+TOP_MODULE={self.sources.top}"
72
+ f" +define+SIMULATION {src.as_posix()}")
29
73
  cmds.append(f"vopt -incr driver -o driver_opt +acc")
30
74
  return cmds
31
75
 
@@ -53,9 +97,28 @@ class Questa(Simulator):
53
97
  vsim,
54
98
  "driver_opt",
55
99
  "-batch",
100
+ ]
101
+
102
+ if self.debug:
103
+ # Create waveform dump .do file
104
+ wave_file = Path("wave.do")
105
+ with wave_file.open("w") as f:
106
+ f.write("log -r /*\n")
107
+ cmd += [
108
+ "-do",
109
+ str(wave_file.resolve()),
110
+ ]
111
+ # Questa will by default log to a vsim.wlf file in the current
112
+ # directory.
113
+ print(
114
+ f"Debug mode enabled - waveform will be in {wave_file.resolve().parent / 'vsim.wlf'}"
115
+ )
116
+
117
+ cmd += [
56
118
  "-do",
57
119
  "run -all",
58
120
  ]
121
+
59
122
  for lib in self.sources.dpi_so_paths():
60
123
  svLib = os.path.splitext(lib)[0]
61
124
  cmd.append("-sv_lib")
@@ -9,7 +9,8 @@ import socket
9
9
  import subprocess
10
10
  import time
11
11
  from pathlib import Path
12
- from typing import Dict, List
12
+ from typing import Dict, List, Optional, Callable, IO
13
+ import threading
13
14
 
14
15
  _thisdir = Path(__file__).parent
15
16
  CosimCollateralDir = _thisdir
@@ -77,9 +78,15 @@ class SourceFiles:
77
78
 
78
79
  class SimProcess:
79
80
 
80
- def __init__(self, proc: subprocess.Popen, port: int):
81
+ def __init__(self,
82
+ proc: subprocess.Popen,
83
+ port: int,
84
+ threads: Optional[List[threading.Thread]] = None,
85
+ gui: bool = False):
81
86
  self.proc = proc
82
87
  self.port = port
88
+ self.threads: List[threading.Thread] = threads or []
89
+ self.gui = gui
83
90
 
84
91
  def force_stop(self):
85
92
  """Make sure to stop the simulation no matter what."""
@@ -92,6 +99,10 @@ class SimProcess:
92
99
  # If the simulation doesn't exit of its own free will, kill it.
93
100
  self.proc.kill()
94
101
 
102
+ # Join reader threads (they should exit once pipes are closed).
103
+ for t in self.threads:
104
+ t.join()
105
+
95
106
 
96
107
  class Simulator:
97
108
 
@@ -100,10 +111,75 @@ class Simulator:
100
111
  # broken behavior by overriding this.
101
112
  UsesStderr = True
102
113
 
103
- def __init__(self, sources: SourceFiles, run_dir: Path, debug: bool):
114
+ def __init__(self,
115
+ sources: SourceFiles,
116
+ run_dir: Path,
117
+ debug: bool,
118
+ run_stdout_callback: Optional[Callable[[str], None]] = None,
119
+ run_stderr_callback: Optional[Callable[[str], None]] = None,
120
+ compile_stdout_callback: Optional[Callable[[str], None]] = None,
121
+ compile_stderr_callback: Optional[Callable[[str], None]] = None,
122
+ make_default_logs: bool = True,
123
+ macro_definitions: Optional[Dict[str, str]] = None):
124
+ """Simulator base class.
125
+
126
+ Optional sinks can be provided for capturing output. If not provided,
127
+ the simulator will write to log files in `run_dir`.
128
+
129
+ Args:
130
+ sources: SourceFiles describing RTL/DPI inputs.
131
+ run_dir: Directory where build/run artifacts are placed.
132
+ debug: Enable cosim debug mode.
133
+ run_stdout_callback: Line-based callback for runtime stdout.
134
+ run_stderr_callback: Line-based callback for runtime stderr.
135
+ compile_stdout_callback: Line-based callback for compile stdout.
136
+ compile_stderr_callback: Line-based callback for compile stderr.
137
+ make_default_logs: If True and corresponding callback is not supplied,
138
+ create log file and emit via internally-created callback.
139
+ macro_definitions: Optional dictionary of macro definitions to be defined
140
+ during compilation.
141
+ """
104
142
  self.sources = sources
105
143
  self.run_dir = run_dir
106
144
  self.debug = debug
145
+ self.macro_definitions = macro_definitions
146
+
147
+ # Unified list of any log file handles we opened.
148
+ self._default_files: List[IO[str]] = []
149
+
150
+ def _ensure_default(cb: Optional[Callable[[str], None]], filename: str):
151
+ """Return (callback, file_handle_or_None) with optional file creation.
152
+
153
+ Behavior:
154
+ * If a callback is provided, return it unchanged with no file.
155
+ * If no callback and make_default_logs is False, return (None, None).
156
+ * If no callback and make_default_logs is True, create a log file and
157
+ return a writer callback plus the opened file handle.
158
+ """
159
+ if cb is not None:
160
+ return cb, None
161
+ if not make_default_logs:
162
+ return None, None
163
+ p = self.run_dir / filename
164
+ p.parent.mkdir(parents=True, exist_ok=True)
165
+ logf = p.open("w+")
166
+ self._default_files.append(logf)
167
+
168
+ def _writer(line: str, _lf=logf):
169
+ _lf.write(line + "\n")
170
+ _lf.flush()
171
+
172
+ return _writer, logf
173
+
174
+ # Initialize all four (compile/run stdout/stderr) uniformly.
175
+ self._compile_stdout_cb, self._compile_stdout_log = _ensure_default(
176
+ compile_stdout_callback, 'compile_stdout.log')
177
+ self._compile_stderr_cb, self._compile_stderr_log = _ensure_default(
178
+ compile_stderr_callback, 'compile_stderr.log')
179
+ self._run_stdout_cb, self._run_stdout_log = _ensure_default(
180
+ run_stdout_callback, 'sim_stdout.log')
181
+ self._run_stderr_cb, self._run_stderr_log = _ensure_default(
182
+ run_stderr_callback, 'sim_stderr.log')
107
183
 
108
184
  @staticmethod
109
185
  def get_env() -> Dict[str, str]:
@@ -123,23 +199,29 @@ class Simulator:
123
199
  def compile(self) -> int:
124
200
  cmds = self.compile_commands()
125
201
  self.run_dir.mkdir(parents=True, exist_ok=True)
126
- with (self.run_dir / "compile_stdout.log").open("w") as stdout, (
127
- self.run_dir / "compile_stderr.log").open("w") as stderr:
128
- for cmd in cmds:
129
- stderr.write(" ".join(cmd) + "\n")
130
- cp = subprocess.run(cmd,
131
- env=Simulator.get_env(),
132
- capture_output=True,
133
- text=True)
134
- stdout.write(cp.stdout)
135
- stderr.write(cp.stderr)
136
- if cp.returncode != 0:
137
- print("====== Compilation failure:")
138
- if self.UsesStderr:
139
- print(cp.stderr)
140
- else:
141
- print(cp.stdout)
142
- return cp.returncode
202
+ for cmd in cmds:
203
+ ret = self._start_process_with_callbacks(
204
+ cmd,
205
+ env=Simulator.get_env(),
206
+ cwd=None,
207
+ stdout_cb=self._compile_stdout_cb,
208
+ stderr_cb=self._compile_stderr_cb,
209
+ wait=True)
210
+ if isinstance(ret, int) and ret != 0:
211
+ print("====== Compilation failure")
212
+
213
+ # If we have the default file loggers, print the compilation logs to
214
+ # console. Else, assume that the user has already captured them.
215
+ if self.UsesStderr:
216
+ if self._compile_stderr_log is not None:
217
+ self._compile_stderr_log.seek(0)
218
+ print(self._compile_stderr_log.read())
219
+ else:
220
+ if self._compile_stdout_log is not None:
221
+ self._compile_stdout_log.seek(0)
222
+ print(self._compile_stdout_log.read())
223
+
224
+ return ret
143
225
  return 0
144
226
 
145
227
  def run_command(self, gui: bool) -> List[str]:
@@ -148,11 +230,16 @@ class Simulator:
148
230
 
149
231
  def run_proc(self, gui: bool = False) -> SimProcess:
150
232
  """Run the simulation process. Returns the Popen object and the port which
151
- the simulation is listening on."""
152
- # Open log files
233
+ the simulation is listening on.
234
+
235
+ If user-provided stdout/stderr sinks were supplied in the constructor,
236
+ they are used. Otherwise, log files are created in `run_dir`.
237
+ """
153
238
  self.run_dir.mkdir(parents=True, exist_ok=True)
154
- simStdout = open(self.run_dir / "sim_stdout.log", "w")
155
- simStderr = open(self.run_dir / "sim_stderr.log", "w")
239
+
240
+ env_gui = os.environ.get("COSIM_GUI", "0")
241
+ if env_gui != "0":
242
+ gui = True
156
243
 
157
244
  # Erase the config file if it exists. We don't want to read
158
245
  # an old config.
@@ -168,19 +255,19 @@ class Simulator:
168
255
  # Slow the simulation down to one tick per millisecond.
169
256
  simEnv["DEBUG_PERIOD"] = "1"
170
257
  rcmd = self.run_command(gui)
171
- simProc = subprocess.Popen(self.run_command(gui),
172
- stdout=simStdout,
173
- stderr=simStderr,
174
- env=simEnv,
175
- cwd=self.run_dir,
176
- preexec_fn=os.setsid)
177
- simStderr.close()
178
- simStdout.close()
258
+ # Start process with asynchronous output capture.
259
+ proc, threads = self._start_process_with_callbacks(
260
+ rcmd,
261
+ env=simEnv,
262
+ cwd=self.run_dir,
263
+ stdout_cb=self._run_stdout_cb,
264
+ stderr_cb=self._run_stderr_cb,
265
+ wait=False)
179
266
 
180
267
  # Get the port which the simulation RPC selected.
181
268
  checkCount = 0
182
269
  while (not os.path.exists(portFileName)) and \
183
- simProc.poll() is None:
270
+ proc.poll() is None:
184
271
  time.sleep(0.1)
185
272
  checkCount += 1
186
273
  if checkCount > 500 and not gui:
@@ -200,10 +287,66 @@ class Simulator:
200
287
  checkCount += 1
201
288
  if checkCount > 200:
202
289
  raise Exception(f"Cosim RPC port ({port}) never opened")
203
- if simProc.poll() is not None:
290
+ if proc.poll() is not None:
204
291
  raise Exception("Simulation exited early")
205
292
  time.sleep(0.05)
206
- return SimProcess(proc=simProc, port=port)
293
+ return SimProcess(proc=proc, port=port, threads=threads, gui=gui)
294
+
295
+ def _start_process_with_callbacks(
296
+ self, cmd: List[str], env: Optional[Dict[str, str]], cwd: Optional[Path],
297
+ stdout_cb: Optional[Callable[[str],
298
+ None]], stderr_cb: Optional[Callable[[str],
299
+ None]],
300
+ wait: bool) -> int | tuple[subprocess.Popen, List[threading.Thread]]:
301
+ """Start a subprocess and stream its stdout/stderr to callbacks.
302
+
303
+ If wait is True, blocks until process completes and returns its exit code.
304
+ If wait is False, returns the Popen object (threads keep streaming).
305
+ """
306
+ if os.name == "posix":
307
+ proc = subprocess.Popen(cmd,
308
+ stdout=subprocess.PIPE,
309
+ stderr=subprocess.PIPE,
310
+ env=env,
311
+ cwd=cwd,
312
+ text=True,
313
+ preexec_fn=os.setsid)
314
+ else: # windows
315
+ proc = subprocess.Popen(cmd,
316
+ stdout=subprocess.PIPE,
317
+ stderr=subprocess.PIPE,
318
+ env=env,
319
+ cwd=cwd,
320
+ text=True,
321
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
322
+
323
+ def _reader(pipe, cb):
324
+ if pipe is None:
325
+ return
326
+ for raw in pipe:
327
+ if raw.endswith('\n'):
328
+ raw = raw[:-1]
329
+ if cb:
330
+ try:
331
+ cb(raw)
332
+ except Exception as e:
333
+ print(f"Exception in simulator output callback: {e}")
334
+
335
+ threads: List[threading.Thread] = [
336
+ threading.Thread(target=_reader,
337
+ args=(proc.stdout, stdout_cb),
338
+ daemon=True),
339
+ threading.Thread(target=_reader,
340
+ args=(proc.stderr, stderr_cb),
341
+ daemon=True),
342
+ ]
343
+ for t in threads:
344
+ t.start()
345
+ if wait:
346
+ for t in threads:
347
+ t.join()
348
+ return proc.wait()
349
+ return proc, threads
207
350
 
208
351
  def run(self,
209
352
  inner_command: str,
@@ -228,8 +371,12 @@ class Simulator:
228
371
  testEnv = os.environ.copy()
229
372
  testEnv["ESI_COSIM_PORT"] = str(simProc.port)
230
373
  testEnv["ESI_COSIM_HOST"] = "localhost"
231
- return subprocess.run(inner_command, cwd=os.getcwd(),
232
- env=testEnv).returncode
374
+ ret = subprocess.run(inner_command, cwd=os.getcwd(),
375
+ env=testEnv).returncode
376
+ if simProc.gui:
377
+ print("GUI mode - waiting for simulator to exit...")
378
+ simProc.proc.wait()
379
+ return ret
233
380
  finally:
234
- if simProc:
381
+ if simProc and simProc.proc.poll() is None:
235
382
  simProc.force_stop()