From a07e763d000e84c755c501eefe429550c917d25d Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 25 Sep 2026 14:44:11 -0400 Subject: [PATCH] Launch Lean through the dotnet host in the console optimizer The native launcher apphost only runs on the architecture it was built for, so the optimizer could not start backtests in the linux/arm64 Docker image. Default the Lean location to the launcher assembly and run .dll locations with dotnet; explicitly configured executables are still run as they are. --- Optimizer.Launcher/ConsoleLeanOptimizer.cs | 39 +++++++++--- Optimizer.Launcher/Properties/AssemblyInfo.cs | 4 +- Tests/Optimizer/ConsoleLeanOptimizerTests.cs | 62 +++++++++++++++++++ Tests/QuantConnect.Tests.csproj | 1 + 4 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 Tests/Optimizer/ConsoleLeanOptimizerTests.cs diff --git a/Optimizer.Launcher/ConsoleLeanOptimizer.cs b/Optimizer.Launcher/ConsoleLeanOptimizer.cs index 768fc1a7628f..a0c41ad1c26c 100644 --- a/Optimizer.Launcher/ConsoleLeanOptimizer.cs +++ b/Optimizer.Launcher/ConsoleLeanOptimizer.cs @@ -47,7 +47,7 @@ public ConsoleLeanOptimizer(OptimizationNodePacket nodePacket) : base(nodePacket Directory.CreateDirectory(_rootResultDirectory); _leanLocation = Configuration.Config.Get("lean-binaries-location", - Path.Combine(Directory.GetCurrentDirectory(), "../../../Launcher/bin/Debug/QuantConnect.Lean.Launcher")); + Path.Combine(Directory.GetCurrentDirectory(), "../../../Launcher/bin/Debug/QuantConnect.Lean.Launcher.dll")); var closeLeanAutomatically = Configuration.Config.GetBool("optimizer-close-automatically", true); _extraLeanArguments = $"--close-automatically {closeLeanAutomatically}"; @@ -85,18 +85,11 @@ protected override string RunLean(ParameterSet parameterSet, string backtestName var resultDirectory = Path.Combine(_rootResultDirectory, backtestId); Directory.CreateDirectory(resultDirectory); - // Use ProcessStartInfo class - var startInfo = new ProcessStartInfo - { - FileName = _leanLocation, - WorkingDirectory = Directory.GetParent(_leanLocation).FullName, - Arguments = $"--results-destination-folder \"{resultDirectory}\" --algorithm-id \"{backtestId}\" --optimization-id \"{optimizationId}\" --parameters {parameterSet} --backtest-name \"{backtestName}\" {_extraLeanArguments}", - WindowStyle = ProcessWindowStyle.Minimized - }; + var arguments = $"--results-destination-folder \"{resultDirectory}\" --algorithm-id \"{backtestId}\" --optimization-id \"{optimizationId}\" --parameters {parameterSet} --backtest-name \"{backtestName}\" {_extraLeanArguments}"; var process = new Process { - StartInfo = startInfo, + StartInfo = CreateLeanStartInfo(_leanLocation, arguments), EnableRaisingEvents = true }; _processByBacktestId[backtestId] = process; @@ -121,6 +114,32 @@ protected override string RunLean(ParameterSet parameterSet, string backtestName return backtestId; } + /// + /// Creates the start info to launch Lean from the given location + /// + /// The Lean launcher assembly (.dll) or executable to run + /// The Lean command line arguments + /// The start info for the Lean process + internal static ProcessStartInfo CreateLeanStartInfo(string leanLocation, string arguments) + { + var startInfo = new ProcessStartInfo + { + FileName = leanLocation, + WorkingDirectory = Directory.GetParent(leanLocation).FullName, + Arguments = arguments, + WindowStyle = ProcessWindowStyle.Minimized + }; + + // Run assemblies through the dotnet host: the native apphost only runs on the architecture it was built for + if (leanLocation.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + startInfo.FileName = "dotnet"; + startInfo.Arguments = $"\"{leanLocation}\" {arguments}"; + } + + return startInfo; + } + /// /// Stops lean process /// diff --git a/Optimizer.Launcher/Properties/AssemblyInfo.cs b/Optimizer.Launcher/Properties/AssemblyInfo.cs index e74da607af3e..316bdba0a908 100644 --- a/Optimizer.Launcher/Properties/AssemblyInfo.cs +++ b/Optimizer.Launcher/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -14,4 +15,5 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d46d2a8d-340c-4b40-8ee6-6baa7b1198ab")] \ No newline at end of file +[assembly: Guid("d46d2a8d-340c-4b40-8ee6-6baa7b1198ab")] +[assembly: InternalsVisibleTo("QuantConnect.Tests")] \ No newline at end of file diff --git a/Tests/Optimizer/ConsoleLeanOptimizerTests.cs b/Tests/Optimizer/ConsoleLeanOptimizerTests.cs new file mode 100644 index 000000000000..5f87eb73f0a0 --- /dev/null +++ b/Tests/Optimizer/ConsoleLeanOptimizerTests.cs @@ -0,0 +1,62 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System.IO; +using NUnit.Framework; +using QuantConnect.Optimizer.Launcher; + +namespace QuantConnect.Tests.Optimizer +{ + [TestFixture] + public class ConsoleLeanOptimizerTests + { + [TestCase("QuantConnect.Lean.Launcher.dll")] + [TestCase("QuantConnect.Lean.Launcher.DLL")] + public void LaunchesAssemblyThroughTheDotnetHost(string fileName) + { + var leanLocation = Path.Combine("Lean", "Launcher", "bin", "Debug", fileName); + + var startInfo = ConsoleLeanOptimizer.CreateLeanStartInfo(leanLocation, "--algorithm-id \"backtest-id\""); + + Assert.AreEqual("dotnet", startInfo.FileName); + Assert.AreEqual($"\"{leanLocation}\" --algorithm-id \"backtest-id\"", startInfo.Arguments); + Assert.AreEqual(Path.GetFullPath(Path.Combine("Lean", "Launcher", "bin", "Debug")), startInfo.WorkingDirectory); + } + + [TestCase("QuantConnect.Lean.Launcher")] + [TestCase("QuantConnect.Lean.Launcher.exe")] + public void LaunchesExecutableDirectly(string fileName) + { + var directory = Directory.CreateTempSubdirectory().FullName; + try + { + // an explicitly configured executable is used as is, even with the assembly next to it + File.WriteAllText(Path.Combine(directory, "QuantConnect.Lean.Launcher.dll"), string.Empty); + var leanLocation = Path.Combine(directory, fileName); + + var startInfo = ConsoleLeanOptimizer.CreateLeanStartInfo(leanLocation, "--algorithm-id \"backtest-id\""); + + Assert.AreEqual(leanLocation, startInfo.FileName); + Assert.AreEqual("--algorithm-id \"backtest-id\"", startInfo.Arguments); + Assert.AreEqual(directory, startInfo.WorkingDirectory); + } + finally + { + Directory.Delete(directory, true); + } + } + } +} diff --git a/Tests/QuantConnect.Tests.csproj b/Tests/QuantConnect.Tests.csproj index 53842e014757..30dab6a1cacc 100644 --- a/Tests/QuantConnect.Tests.csproj +++ b/Tests/QuantConnect.Tests.csproj @@ -421,6 +421,7 @@ +