using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; using Microsoft.Win32; namespace JetBrains.Rider.PathLocator { /// /// This code is a modified version of the JetBrains resharper-unity plugin listed under Apache License 2.0 license: /// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs /// public class RiderPathLocator { [PublicAPI] public readonly IRiderLocatorEnvironment RiderLocatorEnvironment; public RiderPathLocator(IRiderLocatorEnvironment riderLocatorEnvironment) { RiderLocatorEnvironment = riderLocatorEnvironment; } [UsedImplicitly] // Used in com.unity.ide.rider public RiderInfo[] GetAllRiderPaths() { var results = new List(); try { AddToolboxSpecificRiderPaths(results); } catch (Exception e) { RiderLocatorEnvironment.Error("Error retrieving Rider installations from the JetBrains Toolbox.", e); } try { AddOsSpecificRiderPaths(results); } catch (Exception e) { RiderLocatorEnvironment.Error("Error retrieving OS specific Rider installations.", e); } return results.Distinct().ToArray(); } private void AddToolboxSpecificRiderPaths(List results) { var toolboxPath = GetToolboxPath(); var jsonFile = Path.Combine(toolboxPath, "state.json"); if (File.Exists(jsonFile)) results.AddRange(ToolboxState.GetStateFromJson(this, File.ReadAllText(jsonFile))); } private void AddOsSpecificRiderPaths(List results) { switch (RiderLocatorEnvironment.CurrentOS) { case OS.Windows: results.AddRange(CollectRiderInfosWindows()); break; case OS.MacOSX: results.AddRange(CollectRiderInfosMac()); break; case OS.Linux: results.AddRange(CollectAllRiderPathsLinux()); break; default: throw new ArgumentOutOfRangeException(); } } private RiderInfo[] CollectAllRiderPathsLinux() { var installInfos = new List(); var appsPath = GetAppsInstallLocation(); installInfos.AddRange(CollectToolbox20Linux(appsPath, "*rider*", "bin/rider")); installInfos.AddRange(CollectToolbox20Linux(appsPath, "*fleet*", "bin/Fleet")); var riderRootPath = Path.Combine(appsPath, "Rider"); installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider.sh", false) .Select(a => new RiderInfo(this, a, true)).ToList()); var fleetRootPath = Path.Combine(appsPath, "Fleet"); installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "bin", "Fleet", false) .Select(a => new RiderInfo(this, a, true)).ToList()); var home = Environment.GetEnvironmentVariable("HOME"); if (!string.IsNullOrEmpty(home)) { //$Home/.local/share/applications/jetbrains-rider.desktop var shortcut = new FileInfo(Path.Combine(home, @".local/share/applications/jetbrains-rider.desktop")); if (shortcut.Exists) { var lines = File.ReadAllLines(shortcut.FullName); foreach (var line in lines) { if (!line.StartsWith("Exec=\"")) continue; var path = line.Split('"').Where((_, index) => index == 1).SingleOrDefault(); if (string.IsNullOrEmpty(path)) continue; if (!File.Exists(path)) continue; installInfos.Add(new RiderInfo(this, path, false)); } } } // snap install var snapInstallPath = "/snap/rider/current/bin/rider.sh"; if (new FileInfo(snapInstallPath).Exists) installInfos.Add(new RiderInfo(this, snapInstallPath, false)); return installInfos.ToArray(); } private IEnumerable CollectToolbox20Linux(string appsPath, string pattern, string relPath) { var result = new List(); if (string.IsNullOrEmpty(appsPath) || !Directory.Exists(appsPath)) return result; CollectToolbox20(appsPath, pattern, relPath, result); return result; } private RiderInfo[] CollectRiderInfosMac() { var installInfos = new List(); installInfos.AddRange(CollectFromApplications("*Rider*.app")); installInfos.AddRange(CollectFromApplications("*Fleet*.app")); var appsPath = GetAppsInstallLocation(); var riderRootPath = Path.Combine(appsPath, "Rider"); installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "", "Rider*.app", true) .Select(a => new RiderInfo(this, a, true))); var fleetRootPath = Path.Combine(appsPath, "Fleet"); installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "", "Fleet*.app", true) .Select(a => new RiderInfo(this, a, true))); return installInfos.ToArray(); } private RiderInfo[] CollectFromApplications(string productMask) { var result = new List(); var folder = new DirectoryInfo("/Applications"); if (folder.Exists) { result.AddRange(folder.GetDirectories(productMask) .Select(a => new RiderInfo(this, a.FullName, false)) .ToList()); } var home = Environment.GetEnvironmentVariable("HOME"); if (!string.IsNullOrEmpty(home)) { var userFolder = new DirectoryInfo(Path.Combine(home, "Applications")); if (userFolder.Exists) { result.AddRange(userFolder.GetDirectories(productMask) .Select(a => new RiderInfo(this, a.FullName, false)) .ToList()); } } return result.ToArray(); } private RiderInfo[] CollectRiderInfosWindows() { var installInfos = new List(); var appsPath = GetAppsInstallLocation(); var riderRootPath = Path.Combine(appsPath, "Rider"); installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider64.exe", false).ToList() .Select(a => new RiderInfo(this, a, true)).ToList()); var fleetRootPath = Path.Combine(appsPath, "Fleet"); installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, string.Empty, "Fleet.exe", false).ToList() .Select(a => new RiderInfo(this, a, true)).ToList()); const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; CollectPathsFromRegistry(registryKey, installInfos); const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"; CollectPathsFromRegistry(wowRegistryKey, installInfos); return installInfos.ToArray(); } private void CollectToolbox20(string dir, string pattern, string relPath, List result) { var directoryInfo = new DirectoryInfo(dir); if (!directoryInfo.Exists) return; foreach (var riderDirectory in directoryInfo.GetDirectories(pattern)) { var executable = Path.Combine(riderDirectory.FullName, relPath); if (File.Exists(executable)) { result.Add(new RiderInfo(this, executable, false)); // false, because we can't check if it is Toolbox or not anyway } } } private string GetAppsInstallLocation() { var toolboxPath = GetToolboxPath(); var settingsJson = Path.Combine(toolboxPath, ".settings.json"); if (File.Exists(settingsJson)) { var path = SettingsJson.GetInstallLocationFromJson(RiderLocatorEnvironment, File.ReadAllText(settingsJson)); if (!string.IsNullOrEmpty(path)) return path; } return Path.Combine(toolboxPath, "apps"); } private string GetToolboxPath() { string localAppData = string.Empty; switch (RiderLocatorEnvironment.CurrentOS) { case OS.Windows: { localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); break; } case OS.MacOSX: { var home = Environment.GetEnvironmentVariable("HOME"); if (!string.IsNullOrEmpty(home)) { localAppData = Path.Combine(home, @"Library/Application Support"); } break; } case OS.Linux: { var home = Environment.GetEnvironmentVariable("HOME"); if (!string.IsNullOrEmpty(home)) { localAppData = Path.Combine(home, @".local/share"); } break; } default: throw new Exception("Unknown OS"); } var toolboxPath = Path.Combine(localAppData, @"JetBrains/Toolbox"); return toolboxPath; } [PublicAPI] public ProductInfo GetBuildVersion(string path) { var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt())); var dir = buildTxtFileInfo.DirectoryName; if (!Directory.Exists(dir)) return null; var buildVersionFile = new FileInfo(Path.Combine(dir, "product-info.json")); if (!buildVersionFile.Exists) return null; var json = File.ReadAllText(buildVersionFile.FullName); return ProductInfo.GetProductInfo(RiderLocatorEnvironment, json); } [PublicAPI] public Version GetBuildNumber(string riderPath) { Version buildNum = null; try { buildNum = GetBuildNumberWithBuildTxt(riderPath); } catch (Exception e) { RiderLocatorEnvironment.Warn($"Failed to get buildNum from {riderPath}", e); } return buildNum ?? GetBuildNumberFromInput(riderPath); } private Version GetBuildNumberWithBuildTxt(string riderPath) { var buildTxtFileInfo = new FileInfo(Path.Combine(riderPath, GetRelativePathToBuildTxt())); if (!buildTxtFileInfo.Exists) return null; var text = File.ReadAllText(buildTxtFileInfo.FullName); var index = text.IndexOf("-", StringComparison.Ordinal) + 1; // RD-191.7141.355 if (index <= 0) return null; var versionText = text.Substring(index); return GetBuildNumberFromInput(versionText); } [CanBeNull] private Version GetBuildNumberFromInput(string input) { if (string.IsNullOrEmpty(input)) return null; var match = Regex.Match(input, @"(?\d+)\.(?\d+)(\.(?\d+))?"); var groups = match.Groups; Version version = null; if (match.Success) { var major = match.Groups["major"].Value; var minor = match.Groups["minor"].Value; version = match.Groups["build"].Success ? new Version($"{major}.{minor}.{match.Groups["build"].Value}") : new Version($"{major}.{minor}"); } return version; } [UsedImplicitly] // Rider package public bool GetIsToolbox(string path) { return Path.GetFullPath(path).StartsWith(Path.GetFullPath(GetAppsInstallLocation())); } private string GetRelativePathToBuildTxt() { switch (RiderLocatorEnvironment.CurrentOS) { case OS.Windows: case OS.Linux: return "../../build.txt"; case OS.MacOSX: return "Contents/Resources/build.txt"; } throw new Exception("Unknown OS"); } [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private void CollectPathsFromRegistry(string registryKey, List installPaths) { using (var key = Registry.CurrentUser.OpenSubKey(registryKey)) { CollectPathsFromRegistry(installPaths, key); } using (var key = Registry.LocalMachine.OpenSubKey(registryKey)) { CollectPathsFromRegistry(installPaths, key); } } [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] private void CollectPathsFromRegistry(List installPaths, RegistryKey key) { if (key == null) return; foreach (var subkeyName in key.GetSubKeyNames()) { using (var subkey = key.OpenSubKey(subkeyName)) { var folderObject = subkey?.GetValue("InstallLocation"); if (folderObject == null) continue; var folder = folderObject.ToString(); if (folder.Length == 0) continue; var displayName = subkey.GetValue("DisplayName"); if (displayName == null) continue; if (displayName.ToString().Contains("Rider")) { try // possible "illegal characters in path" { var possiblePath = Path.Combine(folder, @"bin\rider64.exe"); if (File.Exists(possiblePath)) installPaths.Add(new RiderInfo(this, possiblePath, subkeyName.Contains("JetBrains Toolbox"))); } catch (ArgumentException) { } } else if (displayName.ToString().Contains("Fleet")) { try // possible "illegal characters in path" { var possiblePath = Path.Combine(folder, @"Fleet.exe"); if (File.Exists(possiblePath)) installPaths.Add(new RiderInfo(this, possiblePath, subkeyName.Contains("JetBrains Toolbox"))); } catch (ArgumentException) { } } } } } private string[] CollectPathsFromToolbox(string productRootPathInToolbox, string dirName, string searchPattern, bool isMac) { if (!Directory.Exists(productRootPathInToolbox)) return new string[0]; var channelDirs = Directory.GetDirectories(productRootPathInToolbox); var paths = channelDirs.SelectMany(channelDir => { try { // use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D var historyFile = Path.Combine(channelDir, ".history.json"); if (File.Exists(historyFile)) { var json = File.ReadAllText(historyFile); var build = ToolboxHistory.GetLatestBuildFromJson(RiderLocatorEnvironment, json); if (build != null) { var buildDir = Path.Combine(channelDir, build); var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir); if (executablePaths.Any()) return executablePaths; } } var channelFile = Path.Combine(channelDir, ".channel.settings.json"); if (File.Exists(channelFile)) { var json = File.ReadAllText(channelFile).Replace("active-application", "active_application"); var build = ToolboxInstallData.GetLatestBuildFromJson(RiderLocatorEnvironment, json); if (build != null) { var buildDir = Path.Combine(channelDir, build); var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir); if (executablePaths.Any()) return executablePaths; } } // changes in toolbox json files format may brake the logic above, so return all found installations return Directory.GetDirectories(channelDir) .SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir)); } catch (Exception e) { // do not write to Debug.Log, just log it. RiderLocatorEnvironment.Warn($"Failed to get path from {channelDir}", e); } return new string[0]; }) .Where(c => !string.IsNullOrEmpty(c)) .ToArray(); return paths; } private string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir) { var folder = new DirectoryInfo(Path.Combine(buildDir, dirName)); if (!folder.Exists) return new string[0]; if (!isMac) return new[] { Path.Combine(folder.FullName, searchPattern) }.Where(File.Exists).ToArray(); return folder.GetDirectories(searchPattern).Select(f => f.FullName) .Where(Directory.Exists).ToArray(); } // Disable the "field is never assigned" compiler warning. We never assign it, but Unity does. // Note that Unity disable this warning in the generated C# projects #pragma warning disable 0649 [Serializable] class ToolboxState { [UsedImplicitly] public int version; [UsedImplicitly] public List tools; [Annotations.NotNull] public static RiderInfo[] GetStateFromJson(RiderPathLocator riderPathLocator, string json) { try { var state = riderPathLocator.RiderLocatorEnvironment.FromJson(json); var version = state.version; if (version > 1) return new RiderInfo[0]; var tools = state.tools; return tools.Where(tool => tool.toolId is "Rider" or "Fleet").Select(a => new RiderInfo(true, $"{a.displayName} {a.displayVersion}", riderPathLocator.GetBuildNumberFromInput(a.buildNumber), riderPathLocator.RiderLocatorEnvironment.CurrentOS != OS.MacOSX ? Path.Combine(a.installLocation, a.launchCommand) : a.installLocation)).ToArray(); } catch (Exception) { riderPathLocator.RiderLocatorEnvironment.Warn($"Failed to get toolbox state from {json}"); } return new RiderInfo[0]; } [Serializable] public class Tool { [UsedImplicitly] public string toolId; [UsedImplicitly] public string displayName; [UsedImplicitly] public string displayVersion; [UsedImplicitly] public string buildNumber; [UsedImplicitly] public string installLocation; [UsedImplicitly] public string launchCommand; } } [Serializable] class SettingsJson { // ReSharper disable once InconsistentNaming [UsedImplicitly] public string install_location; // We never assign it, but Unity does. [CanBeNull] public static string GetInstallLocationFromJson(IRiderLocatorEnvironment riderLocatorEnvironment, string json) { try { return riderLocatorEnvironment.FromJson(json).install_location; } catch (Exception) { riderLocatorEnvironment.Warn($"Failed to get install_location from json {json}"); } return null; } } [Serializable] class ToolboxHistory { [UsedImplicitly] public List history; [CanBeNull] public static string GetLatestBuildFromJson(IRiderLocatorEnvironment riderLocatorEnvironment, string json) { try { return riderLocatorEnvironment.FromJson(json).history.LastOrDefault()?.item.build; } catch (Exception) { riderLocatorEnvironment.Warn($"Failed to get latest build from json {json}"); } return null; } } [Serializable] class ItemNode { [UsedImplicitly] public BuildNode item; } [Serializable] class BuildNode { [UsedImplicitly] public string build; } [Serializable] public class ProductInfo { [UsedImplicitly] public string version; [UsedImplicitly] public string versionSuffix; [CanBeNull] internal static ProductInfo GetProductInfo(IRiderLocatorEnvironment riderLocatorEnvironment, string json) { try { return riderLocatorEnvironment.FromJson(json); } catch (Exception) { riderLocatorEnvironment.Warn($"Failed to get version from json {json}"); } return null; } } // ReSharper disable once ClassNeverInstantiated.Global [Serializable] class ToolboxInstallData { // ReSharper disable once InconsistentNaming [UsedImplicitly] public ActiveApplication active_application; [CanBeNull] public static string GetLatestBuildFromJson(IRiderLocatorEnvironment riderLocatorEnvironment, string json) { try { var builds = riderLocatorEnvironment.FromJson(json).active_application.builds; if (builds != null && builds.Any()) return builds.First(); } catch (Exception) { riderLocatorEnvironment.Warn($"Failed to get latest build from json {json}"); } return null; } } [Serializable] class ActiveApplication { [UsedImplicitly] public List builds; } #pragma warning restore 0649 [Serializable] public struct RiderInfo { public bool IsToolbox; public string Presentation; public string BuildNumber; public ProductInfo ProductInfo; public string Path; public RiderInfo(RiderPathLocator riderPathLocator, string path, bool isToolbox) : this(path, isToolbox, riderPathLocator.GetBuildNumber(path), riderPathLocator.GetBuildVersion(path)) { } public RiderInfo(bool isToolbox, string presentation, Version buildNumber, string path) { IsToolbox = isToolbox; Presentation = presentation; BuildNumber = buildNumber != null ? buildNumber.ToString() : string.Empty; Path = new FileInfo(path).FullName; // normalize separators ProductInfo = null; } [PublicAPI] public RiderInfo(string path, bool isToolbox, Version buildNumber, ProductInfo productInfo) { BuildNumber = buildNumber != null ? buildNumber.ToString() : string.Empty; ProductInfo = productInfo; var fileInfo = new FileInfo(path); var productName = GetProductNameForPresentation(fileInfo); Path = fileInfo.FullName; // normalize separators var presentation = $"{productName} {buildNumber}"; if (productInfo != null && !string.IsNullOrEmpty(productInfo.version)) { var suffix = string.IsNullOrEmpty(productInfo.versionSuffix) ? "" : $" {productInfo.versionSuffix}"; presentation = $"{productName} {productInfo.version}{suffix}"; } if (isToolbox) presentation += " (JetBrains Toolbox)"; Presentation = presentation; IsToolbox = isToolbox; } public override bool Equals(object obj) { if (obj == null) return false; if (obj.GetType() != GetType()) return false; return Path == ((RiderInfo)obj).Path; } public override int GetHashCode() { return Path.GetHashCode(); } private static string GetProductNameForPresentation(FileInfo path) { var filename = path.Name; if (filename.StartsWith("rider", StringComparison.OrdinalIgnoreCase)) return "Rider"; if (RiderFileOpener.IsFleet(path)) return "Fleet"; return filename; } } } }