TRNRun 0.9.2
dotnet add package TRNRun --version 0.9.2
NuGet\Install-Package TRNRun -Version 0.9.2
<PackageReference Include="TRNRun" Version="0.9.2" />
<PackageVersion Include="TRNRun" Version="0.9.2" />
<PackageReference Include="TRNRun" />
paket add TRNRun --version 0.9.2
#r "nuget: TRNRun, 0.9.2"
#:package TRNRun@0.9.2
#addin nuget:?package=TRNRun&version=0.9.2
#tool nuget:?package=TRNRun&version=0.9.2

Thin .NET client for running and monitoring
TRNSYS batch simulations. The package includes the
trnrun.exe runner and the
trnrund.exe
daemon. TRNRunDaemon has one async method per trnrund command and returns
simulations as typed Simulation records.
Contents
- Requirements
- Installation
- Quick start
- Run a batch
SimulationSettingsTRNRunDaemonSimulation- Follow progress
- Examples
Requirements
- Windows x64
- .NET 10
- TRNSYS 17 or 18
- Optional: Type3830 Progress Tracker for progress and stall monitoring
Installation
dotnet add package TRNRun
Quick start
using TRNRun;
await using TRNRunDaemon daemon = await TRNRunDaemon.StartAsync(workers: 1);
long simId = await daemon.AddAsync(@"C:\path\to\deck.dck", new SimulationSettings { KeepAwake = true });
Simulation simulation = await daemon.WaitForFinishedAsync(simId);
if (simulation.Succeeded == true)
{
Console.WriteLine($"Completed: {simulation.DeckFile}");
}
else
{
Console.WriteLine($"Failed: {simulation.DeckFile}");
}
Run a batch
using TRNRun;
string[] decks = [.. Directory.EnumerateFiles(@"C:\path\to\decks", "*.dck").Order()];
var settings = new SimulationSettings { KeepAwake = true };
await using TRNRunDaemon daemon = await TRNRunDaemon.StartAsync(workers: 4);
var simIds = new List<long>();
foreach (string deck in decks)
{
simIds.Add(await daemon.AddAsync(deck, settings));
}
Simulation[] simulations = await Task.WhenAll(simIds.Select(simId => daemon.WaitForFinishedAsync(simId)));
int succeeded = simulations.Count(sim => sim.Succeeded == true);
Console.WriteLine($"{succeeded} succeeded, {decks.Length - succeeded} failed");
SimulationSettings
SimulationSettings defines how one deck is launched and monitored. Each
property that is set becomes one
TRNRun Runner option;
a property left at null keeps the runner's default, listed below. TRNRun
checks the values when the simulation starts, so a bad value finishes it with
SimulationStatus.Error.
Durations are TimeSpans, passed as whole milliseconds rounded up. For the
timeouts, Timeout.InfiniteTimeSpan and TimeSpan.Zero both mean no limit.
It is an immutable record: derive variants with with:
using TRNRun;
var settings = new SimulationSettings { KeepAwake = true, PollInterval = TimeSpan.FromMilliseconds(10) };
await daemon.AddAsync(deck, settings); // KeepAwake = true, PollInterval = 10 ms
await daemon.AddAsync(deck); // every runner default
await daemon.AddAsync(deck, settings with { PollInterval = TimeSpan.FromMilliseconds(20) }); // KeepAwake = true, PollInterval = 20 ms
Executable and window
TrnexePath(string?, default:C:\TRNSYS18\Exe\TrnEXE64.exe)Path to the
TrnEXE64.exeorTrnEXE.exeexecutable. A relative path resolves from the current directory; a missing file throwsFileNotFoundExceptionfromAddAsyncbefore the simulation is queued.GuiVisibility(GuiVisibility?, default:Hidden)Controls the TRNSYS simulation window:
KeepOpen: show the window and leave it open after the simulation.AutoClose: show the window and close it after the simulation.Minimized: minimize the window and leave it open afterward.MinimizedAuto: minimize the window and close it afterward.Hidden: hide the window and close it after the simulation.
Launch detection
WaitForGui(bool?, default:true)Wait for a recognized TRNSYS simulation window as part of the launch-readiness checks.
WaitForLst(bool?, default:true)Wait for the component-order header in the deck's
.lstfile.WaitForTmp(bool?, default:false)Wait for a Type3830
.tmpfile. Do not enable this for a deck without Type3830, or launch detection will wait until its deadline.DetectTimeout(TimeSpan?, default: 300 seconds)Maximum time to wait for launch readiness. Set to
Timeout.InfiniteTimeSpanto wait indefinitely. The runner holds the launch mutex for the current Windows session until detection completes.ExtraDelay(TimeSpan?, default:TimeSpan.Zero)Additional delay after all enabled readiness checks pass.
Runtime monitoring
PollInterval(TimeSpan?, default: 100 milliseconds)Polling interval for the TRNSYS process and output files; at least 1 millisecond.
WatchLog(bool?, default:true)Read the deck's
.logfile and report its entries asLogs.WatchTmp(bool?, default:true)Read Type3830
.tmpupdates and report the simulation period and progress. Required for progress-basedSimulationStatus.CancelledandSimulationStatus.Stalledoutcomes. Without Type3830 in the deck, an early exit is reported asDone.WatchTimeout(TimeSpan?, default: unlimited)Maximum runtime-monitoring duration.
StallTimeout(TimeSpan?, default: disabled)Maximum time without simulation-time progress. Requires
WatchTmp = trueand a valid Type3830 snapshot.KillOnTimeout(bool?, default:false)Terminate the TRNSYS process when launch detection or runtime monitoring times out. If disabled, a detection timeout proceeds to runtime monitoring; after a runtime-monitoring timeout, the runner stops polling and waits for the process to exit.
KillOnStall(bool?, default:false)Terminate the TRNSYS process after detecting a stall. Without it, the runner waits for the process to exit.
Output and cleanup
CleanOnSuccess(bool?, default:false)Delete
.tmp,.log,.lst, and.PTIsidecar files after a successful run.Severity(LogSeverity?, default:Notice)Minimum reported log severity:
Notice,Warning, orFatal.WriteEvents(bool?, default:false)Mirror the runner events to a
.jsonlfile beside the deck, replacing any existing file when the run starts.KeepAwake(bool?, default:false)Block idle PC sleep until the run ends.
A configuration with every property set explicitly:
using TRNRun;
var settings = new SimulationSettings
{
TrnexePath = @"C:\TRNSYS18\Exe\TrnEXE64.exe",
GuiVisibility = GuiVisibility.Hidden,
WaitForGui = true,
WaitForLst = true,
WaitForTmp = true,
DetectTimeout = TimeSpan.FromMinutes(5),
ExtraDelay = TimeSpan.Zero,
PollInterval = TimeSpan.FromMilliseconds(100),
WatchLog = true,
WatchTmp = true,
WatchTimeout = TimeSpan.FromMinutes(5),
StallTimeout = TimeSpan.FromMinutes(5),
CleanOnSuccess = true,
KillOnTimeout = true,
KillOnStall = true,
Severity = LogSeverity.Notice,
WriteEvents = false,
KeepAwake = true,
};
TRNRunDaemon
TRNRunDaemon owns one trnrund.exe and has one async method per
trnrund command.
Nothing runs in the background: each Simulation it returns is a snapshot, so
ask again for the current one.
Requests are thread-safe: they take turns on the one pipe. A
CancellationToken only cancels the wait for an earlier request; a request
already sent runs to its reply, so the pipe never gets out of step. Disposing
waits for the request in progress, then closes trnrund's stdin, so it kills
every unfinished run and exits; waiting and later calls throw
ObjectDisposedException. Prefer await using: Dispose blocks while
trnrund exits.
Parameters
TRNRunDaemon.StartAsync starts trnrund and returns once it answers:
workers(int?, default: logical processor count minus one)Maximum number of simulations running at once. Additional simulations wait in the queue.
trnrundPath(string?, default: bundledtrnrund.exe)Path to the
trnrund.exeexecutable.trnrunPath(string?, default: bundledtrnrun.exe)Path to the
trnrun.exeexecutable.cancellationToken(CancellationToken, default: none)Stops waiting for trnrund to answer; the half-started trnrund is then shut down.
The bundled executables are the ones the package copies to native/ beside
your application. An explicitly configured daemon:
using TRNRun;
await using TRNRunDaemon daemon = await TRNRunDaemon.StartAsync(
workers: 4,
trnrundPath: @"C:\path\to\trnrund.exe",
trnrunPath: @"C:\path\to\trnrun.exe");
Methods
Every method also takes an optional CancellationToken.
AddAsync(string deckFile, SimulationSettings? settings = null, string? label = null) -> Task<long>Queue
deckFileand return itssimId. A relative path resolves from the current directory;labelis free text stored with the simulation.GetSimulationAsync(long simId, bool withLogs = false) -> Task<Simulation>Return one simulation, without
LogsunlesswithLogsistrue.GetSimulationsAsync(SimulationState state, bool withLogs = false) -> Task<IReadOnlyList<Simulation>>Return every unreleased simulation in
state, oldest first.WaitForFinishedAsync(long simId, TimeSpan? pollInterval = null) -> Task<Simulation>Poll one simulation, every half second by default, until it is
Finished, and return it withoutLogs. Cancelling stops the wait, not the simulation.CancelAsync(long simId) -> TaskStop a queued or running simulation; one already finished is left unchanged. It then finishes with status
Cancelled.ReleaseAsync(long simId) -> TaskForget a finished simulation and its logs.
RequestAsync(JsonObject request) -> Task<JsonObject>Send any raw trnrund request and return its raw response.
DisposeAsync() -> ValueTask,Dispose()Stop
trnrund.exe, killing unfinished runs. If it does not exit within 10 seconds, it is killed with its child processes.
RequestException is thrown when trnrund refuses a request, such as for a
missing deck, an unknown simId, or releasing an unfinished run, and
DaemonExitedException, with its exit code and stderr, when it no longer
answers. Both derive from TRNRunException, which is also thrown, wrapping the
parse error, when a reply breaks the protocol. Simulation outcomes do not
throw: inspect each simulation's Succeeded, Status, and ExitCode instead.
Example daemon workflow with every method:
using System.Text.Json.Nodes;
using TRNRun;
var settings = new SimulationSettings { KeepAwake = true };
await using TRNRunDaemon daemon = await TRNRunDaemon.StartAsync(workers: 2);
long first = await daemon.AddAsync(@"C:\path\to\first.dck", settings);
long second = await daemon.AddAsync(@"C:\path\to\second.dck", settings, label: "second");
Console.WriteLine(await daemon.GetSimulationAsync(first));
Console.WriteLine($"Running: {(await daemon.GetSimulationsAsync(SimulationState.Running)).Count}");
await daemon.CancelAsync(second);
await daemon.WaitForFinishedAsync(second);
Simulation cancelled = await daemon.GetSimulationAsync(second, withLogs: true);
Console.WriteLine($"Second status: {cancelled.Status}, {cancelled.Logs.Count} log entries");
await daemon.ReleaseAsync(second);
try
{
await daemon.ReleaseAsync(first);
}
catch (RequestException error)
{
Console.WriteLine($"First not finished: {error.Message}");
}
JsonObject raw = await daemon.RequestAsync(new JsonObject { ["cmd"] = "simulations", ["state"] = "RUNNING" });
Console.WriteLine($"Raw response: {raw}");
Simulation
Simulation is an immutable snapshot of one run, holding trnrund's
simulation fields
in PascalCase. A nullable property is null until the runner reports it.
Properties
SimId(long)Identifier assigned by the daemon, never reused.
DeckFile(string)Absolute path to the submitted deck.
Label(string?)Label it was added with.
TrnrunOptions(JsonElement?)Runner options it was added with, as a JSON object such as
{"keepAwake":true}.State(SimulationState)Queuedwhile waiting for a worker,Runningonce the runner started, andFinishedonce it completed, failed, or was cancelled. Wait onState, notStatus:Statuscan already beDonewhileStateisRunning.Status,StatusMessage(SimulationStatus?,string?)Latest runner status and its detail; trnrund's
trnrunStatusandtrnrunMessage.Succeeded(bool?)Set once finished.
trueonly with statusDone, exit code0, and noError.ExitCode(int?)Runner exit code;
nullif it was cancelled or never started.Error(string?)Why the daemon failed or cancelled the run.
SimTime,Percent(double?),Elapsed,Eta(TimeSpan?)Latest Type3830 progress.
SimTimeis in simulated hours andPercentis a fraction from0to1.StartTime,StopTime,TimeStep(double?)Simulation period reported by Type3830, in simulated hours.
Setting(JsonElement?)Settings the runner applied, its defaults included, keyed by the runner's camelCase names.
Notices,Warnings,Fatals(int)Number of log entries so far, by severity, counted even when
Logsis empty.Logs(IReadOnlyList<LogEvent>)Log entries, oldest first; empty unless asked for with
withLogs.SubmittedAt,StartedAt,FinishedAt(DateTimeOffset?)UTC times;
StartedAtstaysnullfor a simulation that never started.
An example inspecting every property:
using TRNRun;
await using TRNRunDaemon daemon = await TRNRunDaemon.StartAsync(workers: 1);
long simId = await daemon.AddAsync(@"C:\path\to\deck.dck", new SimulationSettings { KeepAwake = true });
await daemon.WaitForFinishedAsync(simId);
Simulation simulation = await daemon.GetSimulationAsync(simId, withLogs: true);
Console.WriteLine($"ID: {simulation.SimId}");
Console.WriteLine($"Deck: {simulation.DeckFile}");
Console.WriteLine($"Label: {simulation.Label}");
Console.WriteLine($"Runner options: {simulation.TrnrunOptions}");
Console.WriteLine($"State: {simulation.State}");
Console.WriteLine($"Status: {simulation.Status} {simulation.StatusMessage}");
Console.WriteLine($"Succeeded: {simulation.Succeeded}");
Console.WriteLine($"Exit code: {simulation.ExitCode}");
Console.WriteLine($"Error: {simulation.Error}");
Console.WriteLine($"Progress: {simulation.Percent} at {simulation.SimTime} h");
Console.WriteLine($"Elapsed: {simulation.Elapsed}, ETA: {simulation.Eta}");
Console.WriteLine($"Period: {simulation.StartTime} to {simulation.StopTime} by {simulation.TimeStep} h");
Console.WriteLine($"Runner settings: {simulation.Setting}");
Console.WriteLine($"Notices: {simulation.Notices}");
Console.WriteLine($"Warnings: {simulation.Warnings}");
Console.WriteLine($"Fatals: {simulation.Fatals}");
Console.WriteLine($"Logs: {string.Join(Environment.NewLine, simulation.Logs)}");
Console.WriteLine($"Times: {simulation.SubmittedAt}, {simulation.StartedAt}, {simulation.FinishedAt}");
Follow progress
To draw your own display, poll the running simulations and release each one once finished:
using TRNRun;
string[] decks = [@"C:\path\to\first.dck", @"C:\path\to\second.dck"];
var settings = new SimulationSettings { KeepAwake = true };
await using TRNRunDaemon daemon = await TRNRunDaemon.StartAsync(workers: 2);
foreach (string deck in decks)
{
await daemon.AddAsync(deck, settings);
}
int remaining = decks.Length;
while (remaining > 0)
{
foreach (Simulation sim in await daemon.GetSimulationsAsync(SimulationState.Running))
{
string progress = sim.Percent is { } percent ? $"{percent:P0}" : "no progress yet";
Console.WriteLine($"[{sim.SimId}] {Path.GetFileName(sim.DeckFile)}: {progress} ({sim.Status})");
}
foreach (Simulation sim in await daemon.GetSimulationsAsync(SimulationState.Finished))
{
Console.WriteLine($"[{sim.SimId}] {Path.GetFileName(sim.DeckFile)}: {sim.Status}");
await daemon.ReleaseAsync(sim.SimId);
remaining--;
}
await Task.Delay(TimeSpan.FromSeconds(1));
}
Examples
Runnable examples are in
examples.
# One simulation in the console.
dotnet run --project examples/TRNRun.Example
# 10 simulations, two at a time.
dotnet run --project examples/TRNRun.BatchExample -- 2
# A minimal GUI, and a fuller one.
dotnet run --project examples/TRNRun.SimpleGuiExample
dotnet run --project examples/TRNRun.GuiExample
The console examples take an optional TrnEXE path as their last argument
(default C:\TRNSYS18\Exe\TrnEXE64.exe).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.