Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 194 additions & 10 deletions SAM_OpenStudio/SAM.Core.OpenStudio/Query/OpenStudioCliPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public static partial class Query
/// 2. directories on the PATH environment variable;
/// 3. conventional direct installations (system drive root openstudio-* and %ProgramFiles%\OpenStudio*), newest first;
/// 4. Ladybug Tools bundled installation (%ProgramFiles%\ladybug_tools\openstudio).
/// Without an explicit path, when more than one CLI is found, the first one (in that order) whose version equals the
/// loaded OpenStudio SDK version (<see cref="OpenStudioVersion"/>) is returned, so that another OpenStudio earlier on
/// PATH is not picked by accident; when none matches, the first one found is returned, as before.
/// </summary>
/// <param name="path">Optional explicit executable or installation directory path.</param>
/// <returns>Full path to openstudio.exe or null when not found.</returns>
Expand Down Expand Up @@ -48,6 +51,194 @@ public static string OpenStudioCliPath(string path = null)
return null;
}

List<string> candidates = OpenStudioCliCandidates(executableName);
if (candidates.Count < 2)
{
return candidates.Count == 0 ? null : candidates[0];
}

string sdkVersion;
try
{
sdkVersion = OpenStudioVersion();
}
catch (Exception)
{
sdkVersion = null;
}

return OpenStudioCliPath(candidates, sdkVersion, OpenStudioCliVersion);
}

/// <summary>
/// Chooses among discovered OpenStudio CLI candidates: the first whose version equals <paramref name="sdkVersion"/>
/// (compared without +build metadata), otherwise the first candidate.
/// </summary>
/// <param name="candidates">Candidate openstudio.exe paths in discovery order.</param>
/// <param name="sdkVersion">Loaded OpenStudio SDK version; when null or empty the first candidate is returned.</param>
/// <param name="cliVersion">Returns a candidate's version (for example "3.10.0+86d7e215a1"), or null when unknown.</param>
/// <returns>The selected candidate, or null when there is none.</returns>
public static string OpenStudioCliPath(IEnumerable<string> candidates, string sdkVersion, Func<string, string> cliVersion)
{
if (candidates == null)
{
return null;
}

string sdkCoreVersion = CoreVersion(sdkVersion);
string first = null;
foreach (string candidate in candidates)
{
if (string.IsNullOrWhiteSpace(candidate))
{
continue;
}

if (first == null)
{
first = candidate;
}

if (sdkCoreVersion != null && cliVersion != null && string.Equals(CoreVersion(cliVersion(candidate)), sdkCoreVersion, StringComparison.OrdinalIgnoreCase))
{
return candidate;
}
}

return first;
}

/// <summary>
/// Version reported by an OpenStudio CLI (<c>openstudio.exe --version</c>, for example "3.10.0+86d7e215a1").
/// The result is cached per executable path, size and write time.
/// </summary>
/// <param name="cliPath">Path to openstudio.exe.</param>
/// <returns>The first non-empty line the CLI printed, or null when it could not be run or printed nothing.</returns>
public static string OpenStudioCliVersion(string cliPath)
{
if (string.IsNullOrWhiteSpace(cliPath) || !File.Exists(cliPath))
{
return null;
}

string key;
try
{
FileInfo fileInfo = new FileInfo(cliPath);
key = string.Format("{0}|{1}|{2}", fileInfo.FullName.ToUpperInvariant(), fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks);
}
catch (Exception)
{
return null;
}

lock (openStudioCliVersions)
{
if (openStudioCliVersions.TryGetValue(key, out string cached))
{
return cached;
}
}

string result = null;
try
{
System.Diagnostics.ProcessStartInfo processStartInfo = new System.Diagnostics.ProcessStartInfo(cliPath, "--version")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};

using (System.Diagnostics.Process process = System.Diagnostics.Process.Start(processStartInfo))
{
System.Threading.Tasks.Task<string> output = process.StandardOutput.ReadToEndAsync();
System.Threading.Tasks.Task<string> error = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(30000))
{
try
{
process.Kill();
}
catch (Exception)
{
// the CLI may already have exited
}

return null;
}

foreach (string line in output.Result.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
{
result = line.Trim();
break;
}
}
}
}
catch (Exception)
{
result = null;
}

lock (openStudioCliVersions)
{
openStudioCliVersions[key] = result;
}

return result;
}

private static readonly Dictionary<string, string> openStudioCliVersions = new Dictionary<string, string>();

private static string CoreVersion(string version)
{
if (string.IsNullOrWhiteSpace(version))
{
return null;
}

string result = version.Trim();
int index = result.IndexOf('+');
if (index >= 0)
{
result = result.Substring(0, index).Trim();
}

return result.Length == 0 ? null : result;
}

private static List<string> OpenStudioCliCandidates(string executableName)
{
List<string> result = new List<string>();
HashSet<string> fullPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

void Add(string candidate)
{
if (!File.Exists(candidate))
{
return;
}

string fullPath;
try
{
fullPath = Path.GetFullPath(candidate);
}
catch (Exception)
{
return;
}

if (fullPaths.Add(fullPath))
{
result.Add(candidate);
}
}

string pathVariable = Environment.GetEnvironmentVariable("PATH");
if (!string.IsNullOrWhiteSpace(pathVariable))
{
Expand All @@ -68,10 +259,7 @@ public static string OpenStudioCliPath(string path = null)
continue;
}

if (File.Exists(candidate))
{
return candidate;
}
Add(candidate);
}
}

Expand Down Expand Up @@ -101,14 +289,10 @@ public static string OpenStudioCliPath(string path = null)

foreach (string installationDirectory in installationDirectories)
{
string candidate = Path.Combine(installationDirectory, "bin", executableName);
if (File.Exists(candidate))
{
return candidate;
}
Add(Path.Combine(installationDirectory, "bin", executableName));
}

return null;
return result;
}

private static IEnumerable<string> InstallationDirectories(string directory, string searchPattern)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors

using System.Collections.Generic;
using System.IO;
using NUnit.Framework;

namespace SAM.Analytical.OpenStudio.Benchmark.Tests
{
/// <summary>
/// Roadmap S06: the OpenStudio CLI that runs a simulation is chosen deterministically. Among the discovered CLIs
/// (PATH, direct installations, ladybug_tools, in that order) the first whose version equals the in-process SDK version
/// wins, so another OpenStudio earlier on PATH (ladybug's 3.11 on the S05 laptop) is not picked by accident; with no
/// match the first one found is kept, as before. Fully offline: candidate versions are supplied by the test.
/// </summary>
[TestFixture]
public class OpenStudioCliSelectionTests
{
private const string Ladybug = @"C:\Program Files\ladybug_tools\openstudio\bin\openstudio.exe";
private const string Direct = @"C:\openstudio-3.10.0\bin\openstudio.exe";
private const string Other = @"D:\tools\OpenStudio-3.10.0\bin\openstudio.exe";

private static readonly Dictionary<string, string> Versions = new Dictionary<string, string>
{
{ Ladybug, "3.11.0+241b8abb4d" },
{ Direct, "3.10.0+86d7e215a1" },
{ Other, "3.10.0+86d7e215a1" },
};

private static string Version(string path)
{
return Versions.TryGetValue(path, out string version) ? version : null;
}

[Test]
public void SdkMatchingCli_IsPreferred_OverAnEarlierDifferentVersionOnPath()
{
string selected = Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Direct }, "3.10.0", Version);
Assert.That(selected, Is.EqualTo(Direct));
}

[Test]
public void FirstMatchingCli_Wins_WhenSeveralMatch()
{
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Other, Direct }, "3.10.0", Version), Is.EqualTo(Other));
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Direct, Other }, "3.10.0", Version), Is.EqualTo(Direct));
}

[Test]
public void NoMatchingCli_KeepsTheFirstCandidate()
{
string selected = Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Direct }, "3.9.0", Version);
Assert.That(selected, Is.EqualTo(Ladybug));
}

[Test]
public void UnknownSdkVersion_KeepsTheFirstCandidate()
{
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Direct }, null, Version), Is.EqualTo(Ladybug));
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Direct }, " ", Version), Is.EqualTo(Ladybug));
}

[Test]
public void BuildMetadata_IsIgnored_OnBothSides()
{
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Direct }, "3.10.0+86d7e215a1", Version), Is.EqualTo(Direct));
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { Ladybug, Direct }, "3.11.0", Version), Is.EqualTo(Ladybug));
}

[Test]
public void CliWithUnknownVersion_IsNotAMatch()
{
string unknown = @"E:\broken\openstudio.exe";
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { unknown, Direct }, "3.10.0", Version), Is.EqualTo(Direct));
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { unknown }, "3.10.0", Version), Is.EqualTo(unknown));
}

[Test]
public void NoCandidates_ReturnsNull_AndBlankEntriesAreSkipped()
{
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new string[0], "3.10.0", Version), Is.Null);
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(null, "3.10.0", Version), Is.Null);
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(new[] { "", null, Ladybug }, "3.9.0", Version), Is.EqualTo(Ladybug));
}

[Test]
public void CliVersion_OfAMissingExecutable_IsNull()
{
string missing = Path.Combine(TestContext.CurrentContext.WorkDirectory, "no-such-folder", "openstudio.exe");
Assert.That(Core.OpenStudio.Query.OpenStudioCliVersion(missing), Is.Null);
Assert.That(Core.OpenStudio.Query.OpenStudioCliVersion(null), Is.Null);
}

[Test]
public void ExplicitPath_IsUnchanged_AndNeverVersionChecked()
{
string directory = Path.Combine(TestContext.CurrentContext.WorkDirectory, "s06-explicit-cli", "bin");
Directory.CreateDirectory(directory);
string executable = Path.Combine(directory, "openstudio.exe");
File.WriteAllText(executable, "not an executable");

Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(executable), Is.EqualTo(executable));
Assert.That(Core.OpenStudio.Query.OpenStudioCliPath(Path.GetDirectoryName(directory)), Is.EqualTo(executable));
}
}
}
Loading
Loading