ScriptRunner is a console application that serves as the target process for debugging in NodeDev. It executes user-compiled DLLs in a separate process, providing the foundation for advanced debugging features via the ICorDebug API.
NodeDev uses a two-process architecture for code execution:
┌─────────────────────────────┐
│ Host Process (IDE) │
│ - NodeDev.Blazor.Server │
│ - NodeDev.Blazor.MAUI │
│ - Compiles user code │
│ - Manages UI │
└──────────┬──────────────────┘
│ Launches
▼
┌─────────────────────────────┐
│ Target Process │
│ - NodeDev.ScriptRunner │
│ - Loads compiled DLL │
│ - Executes user code │
│ - Reports output/errors │
└─────────────────────────────┘
- Process Isolation: User code runs in a separate process, protecting the IDE from crashes
- Debugging Support: Enables ICorDebug attachment for advanced debugging features
- Resource Management: Easier to manage memory and resources of user code
- Security: Sandboxing opportunities for untrusted code execution
When Project.Run() is called:
var assemblyPath = Build(options); // Compiles to bin/Debug/project.exeThe build process creates:
project.exe(orproject.dll) - The compiled user codeproject.pdb- Debug symbolsproject.runtimeconfig.json- Runtime configuration
The IDE launches ScriptRunner with the compiled DLL:
dotnet NodeDev.ScriptRunner.dll "/absolute/path/to/project.exe"ScriptRunner loads the assembly:
Assembly assembly = Assembly.LoadFrom(dllPath);ScriptRunner searches for entry points in this order:
-
Static Program.Main method (in any namespace)
public static class Program { public static int Main() { ... } }
-
IRunnable implementation (future extensibility)
public class MyClass : IRunnable { public void Run() { ... } }
ScriptRunner invokes the entry point with proper exception handling:
try
{
int exitCode = InvokeEntryPoint(assembly, userArgs);
return exitCode;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Fatal error: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"Stack trace:\n{ex.StackTrace}");
return 3;
}NodeDev.ScriptRunner <path-to-dll> [args...]<path-to-dll>: Required. Absolute or relative path to the compiled DLL to execute[args...]: Optional. Arguments to pass to the entry point (if it acceptsstring[] args)
| Code | Meaning |
|---|---|
| 0 | Success - Program executed successfully |
| 1 | Invalid usage - Missing DLL path argument |
| 2 | File not found - DLL doesn't exist at specified path |
| 3 | Fatal error - Unhandled exception during execution |
| 4 | No entry point - No valid entry point found in assembly |
| N | User-defined - Return value from Program.Main() |
# Execute a simple program
dotnet NodeDev.ScriptRunner.dll /path/to/myprogram.dll
# Execute with arguments
dotnet NodeDev.ScriptRunner.dll /path/to/myprogram.dll arg1 arg2 "arg with spaces"ScriptRunner is automatically copied to dependent projects using MSBuild targets:
NodeDev.Core/NodeDev.Core.csproj:
<ItemGroup>
<ProjectReference Include="..\NodeDev.ScriptRunner\NodeDev.ScriptRunner.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<Target Name="CopyScriptRunner" AfterTargets="Build">
<ItemGroup>
<ScriptRunnerFiles Include="..\NodeDev.ScriptRunner\bin\$(Configuration)\$(TargetFramework)\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(ScriptRunnerFiles)" DestinationFolder="$(OutputPath)%(RecursiveDir)" />
</Target>This ensures ScriptRunner is available wherever NodeDev.Core is built.
The FindScriptRunnerExecutable() method locates ScriptRunner:
- Same directory as NodeDev.Core assembly (production)
- Sibling directory in build output (development)
private static string FindScriptRunnerExecutable()
{
string coreDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string scriptRunnerDll = Path.Combine(coreDirectory, "NodeDev.ScriptRunner.dll");
if (File.Exists(scriptRunnerDll))
return scriptRunnerDll;
// Try sibling directories for development scenarios
// ...
}ScriptRunner functionality is tested in NodeDev.Tests/ScriptRunnerTests.cs:
- ScriptRunner_ShouldExecuteSimpleProgram - Verifies basic execution and console output
- ScriptRunner_ShouldHandleExceptions - Tests graceful error handling
- ScriptRunner_ShouldReturnExitCode - Validates exit code propagation
[Fact]
public void ScriptRunner_ShouldExecuteSimpleProgram()
{
// Arrange - Create a project with a WriteLine node
var project = Project.CreateNewDefaultProject(out var mainMethod);
// ... add nodes, connect them ...
// Act - Run via ScriptRunner
var result = project.Run(BuildOptions.Debug);
// Assert - Verify output and behavior
Assert.NotEmpty(consoleOutput);
Assert.Contains(consoleOutput, line => line.Contains("ScriptRunner Test Output"));
}ScriptRunner lays the groundwork for advanced debugging features:
- Breakpoints: Set breakpoints on visual nodes
- Step Execution: Step through node execution one at a time
- Variable Inspection: Inspect connection values at runtime
- Call Stack: View the execution path through the graph
- Exception Catching: Catch and inspect exceptions in the debugger
The ICorDebug API provides:
- Process creation and attachment
- Thread control (suspend, resume)
- Breakpoint management
- Stack walking
- Variable inspection
- Exception handling
// Future debugging implementation (pseudocode)
var debugger = new ICorDebug();
var process = debugger.CreateProcess("dotnet", "NodeDev.ScriptRunner.dll project.exe");
process.OnBreakpoint += (sender, e) => {
// Highlight the corresponding node in the UI
// Show connection values
// Enable step controls
};Error: FileNotFoundException: ScriptRunner executable not found
Solution: Rebuild the solution to trigger the MSBuild copy target:
dotnet buildError: Could not load file or assembly
Cause: The compiled DLL might have missing dependencies
Solution: Ensure all dependencies are in the output directory with the DLL
Error: No entry point found. Expected: - Static method Program.Main()
Cause: The compiled assembly doesn't have a valid entry point
Solution: Verify the project has a class named "Program" with a static "Main" method
To add support for new entry point patterns:
-
Add detection logic to
InvokeEntryPoint():// Strategy 3: Look for custom entry point Type? customType = assembly.GetTypes() .FirstOrDefault(t => t.GetCustomAttribute<EntryPointAttribute>() != null);
-
Add invocation logic
-
Update documentation and tests
When modifying ScriptRunner:
- Keep it lightweight and fast
- Preserve exit code semantics
- Maintain backward compatibility
- Add corresponding tests
- Update this documentation
- Project.cs -
Run()andFindScriptRunnerExecutable()methods - Program.cs - ScriptRunner implementation
- ScriptRunnerTests.cs - Test suite