using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; using Microsoft.Win32; using Packages.Rider.Editor.Util; using Unity.CodeEditor; using UnityEngine; namespace Packages.Rider.Editor { internal interface IDiscovery { CodeEditor.Installation[] PathCallback(); } internal class Discovery : IDiscovery { public CodeEditor.Installation[] PathCallback() { var res = RiderPathLocator.GetAllRiderPaths() .Select(riderInfo => new CodeEditor.Installation { Path = riderInfo.Path, Name = riderInfo.Presentation }) .ToList(); var editorPath = RiderScriptEditor.CurrentEditor; if (RiderScriptEditor.IsRiderOrFleetInstallation(editorPath) && !res.Any(a => a.Path == editorPath) && FileSystemUtil.EditorPathExists(editorPath)) { // External editor manually set from custom location var info = new RiderPathLocator.RiderInfo(editorPath, false); var installation = new CodeEditor.Installation { Path = info.Path, Name = info.Presentation }; res.Add(installation); } return res.ToArray(); } } /// /// This code is a modified version of the JetBrains resharper-unity plugin listed here: /// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs /// internal static class RiderPathLocator { #if !(UNITY_4_7 || UNITY_5_5) public static RiderInfo[] GetAllRiderPaths() { try { switch (SystemInfo.operatingSystemFamily) { case OperatingSystemFamily.Windows: { return CollectRiderInfosWindows(); } case OperatingSystemFamily.MacOSX: { return CollectRiderInfosMac(); } case OperatingSystemFamily.Linux: { return CollectAllRiderPathsLinux(); } } } catch (Exception e) { Debug.LogException(e); } return Array.Empty(); } #endif private static RiderInfo[] CollectAllRiderPathsLinux() { var installInfos = new List(); var appsPath = GetAppsRootPathInToolbox(); var riderRootPath = Path.Combine(appsPath, "Rider"); installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider.sh", false) .Select(a => new RiderInfo(a, true)).ToList()); var fleetRootPath = Path.Combine(appsPath, "Fleet"); installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "bin", "Fleet", false) .Select(a => new RiderInfo(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 (installInfos.Any(a => a.Path == path)) // avoid adding similar build as from toolbox continue; installInfos.Add(new RiderInfo(path, false)); } } } // snap install var snapInstallPath = "/snap/rider/current/bin/rider.sh"; if (new FileInfo(snapInstallPath).Exists) installInfos.Add(new RiderInfo(snapInstallPath, false)); return installInfos.ToArray(); } private static RiderInfo[] CollectRiderInfosMac() { var installInfos = new List(); // "/Applications/*Rider*.app" var folder = new DirectoryInfo("/Applications"); if (folder.Exists) { installInfos.AddRange(folder.GetDirectories("*Rider*.app") .Select(a => new RiderInfo(a.FullName, false)) .ToList()); } var appsPath = GetAppsRootPathInToolbox(); var riderRootPath = Path.Combine(appsPath, "Rider"); installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "", "Rider*.app", true) .Select(a => new RiderInfo(a, true))); var fleetRootPath = Path.Combine(appsPath, "Fleet"); installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "", "Fleet*.app", true) .Select(a => new RiderInfo(a, true))); return installInfos.ToArray(); } private static RiderInfo[] CollectRiderInfosWindows() { var appsPath = GetAppsRootPathInToolbox(); var installInfos = new List(); var riderRootPath = Path.Combine(appsPath, "Rider"); installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider64.exe", false).ToList() .Select(a => new RiderInfo(a, true)).ToList()); var fleetRootPath = Path.Combine(appsPath, "Fleet"); installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, string.Empty, "Fleet.exe", false).ToList() .Select(a => new RiderInfo(a, true)).ToList()); var installPaths = new List(); const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; CollectPathsFromRegistry(registryKey, installPaths); const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"; CollectPathsFromRegistry(wowRegistryKey, installPaths); installInfos.AddRange(installPaths.Select(a => new RiderInfo(a, false)).ToList()); return installInfos.ToArray(); } private static string GetAppsRootPathInToolbox() { string localAppData = string.Empty; switch (SystemInfo.operatingSystemFamily) { case OperatingSystemFamily.Windows: { localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); break; } case OperatingSystemFamily.MacOSX: { var home = Environment.GetEnvironmentVariable("HOME"); if (!string.IsNullOrEmpty(home)) { localAppData = Path.Combine(home, @"Library/Application Support"); } break; } case OperatingSystemFamily.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"); var settingsJson = Path.Combine(toolboxPath, ".settings.json"); if (File.Exists(settingsJson)) { var path = SettingsJson.GetInstallLocationFromJson(File.ReadAllText(settingsJson)); if (!string.IsNullOrEmpty(path)) toolboxPath = path; } return Path.Combine(toolboxPath, "apps"); } internal static 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(json); } internal static Version GetBuildNumber(string path) { var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt())); return GetBuildNumberWithBuildTxt(buildTxtFileInfo) ?? GetBuildNumberFromPath(path); } private static Version GetBuildNumberWithBuildTxt(FileInfo file) { if (!file.Exists) return null; var text = File.ReadAllText(file.FullName); var index = text.IndexOf("-", StringComparison.Ordinal) + 1; // RD-191.7141.355 if (index <= 0) return null; var versionText = text.Substring(index); return Version.TryParse(versionText, out var v) ? v : null; } [CanBeNull] private static Version GetBuildNumberFromPath(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) { version = Version.Parse($"{groups["major"].Value}.{groups["minor"].Value}.{groups["build"].Value}"); } return version; } public static bool GetIsToolbox(string path) { return Path.GetFullPath(path).StartsWith(Path.GetFullPath(GetAppsRootPathInToolbox())); } private static string GetRelativePathToBuildTxt() { switch (SystemInfo.operatingSystemFamily) { case OperatingSystemFamily.Windows: case OperatingSystemFamily.Linux: return "../../build.txt"; case OperatingSystemFamily.MacOSX: return "Contents/Resources/build.txt"; } throw new Exception("Unknown OS"); } private static 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); } } private static 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")) continue; try // possible "illegal characters in path" { var possiblePath = Path.Combine(folder, @"bin\rider64.exe"); if (File.Exists(possiblePath)) installPaths.Add(possiblePath); } catch (ArgumentException) { } } } } private static 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(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(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. Logger.Warn($"Failed to get path from {channelDir}", e); } return new string[0]; }) .Where(c => !string.IsNullOrEmpty(c)) .ToArray(); return paths; } private static 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 SettingsJson { // ReSharper disable once InconsistentNaming public string install_location; [CanBeNull] public static string GetInstallLocationFromJson(string json) { try { #if UNITY_4_7 || UNITY_5_5 return JsonConvert.DeserializeObject(json).install_location; #else return JsonUtility.FromJson(json).install_location; #endif } catch (Exception) { Logger.Warn($"Failed to get install_location from json {json}"); } return null; } } [Serializable] class ToolboxHistory { public List history; [CanBeNull] public static string GetLatestBuildFromJson(string json) { try { #if UNITY_4_7 || UNITY_5_5 return JsonConvert.DeserializeObject(json).history.LastOrDefault()?.item.build; #else return JsonUtility.FromJson(json).history.LastOrDefault()?.item.build; #endif } catch (Exception) { Logger.Warn($"Failed to get latest build from json {json}"); } return null; } } [Serializable] class ItemNode { public BuildNode item; } [Serializable] class BuildNode { public string build; } [Serializable] internal class ProductInfo { public string version; public string versionSuffix; [CanBeNull] internal static ProductInfo GetProductInfo(string json) { try { var productInfo = JsonUtility.FromJson(json); return productInfo; } catch (Exception) { Logger.Warn($"Failed to get version from json {json}"); } return null; } } // ReSharper disable once ClassNeverInstantiated.Global [Serializable] class ToolboxInstallData { // ReSharper disable once InconsistentNaming public ActiveApplication active_application; [CanBeNull] public static string GetLatestBuildFromJson(string json) { try { #if UNITY_4_7 || UNITY_5_5 var toolbox = JsonConvert.DeserializeObject(json); #else var toolbox = JsonUtility.FromJson(json); #endif var builds = toolbox.active_application.builds; if (builds != null && builds.Any()) return builds.First(); } catch (Exception) { Logger.Warn($"Failed to get latest build from json {json}"); } return null; } } [Serializable] class ActiveApplication { // ReSharper disable once InconsistentNaming public List builds; } #pragma warning restore 0649 internal struct RiderInfo { public bool IsToolbox; public string Presentation; public Version BuildNumber; public ProductInfo ProductInfo; public string Path; public RiderInfo(string path, bool isToolbox) { if (path == RiderScriptEditor.CurrentEditor) { RiderScriptEditorData.instance.Init(); BuildNumber = RiderScriptEditorData.instance.editorBuildNumber.ToVersion(); ProductInfo = RiderScriptEditorData.instance.productInfo; } else { BuildNumber = GetBuildNumber(path); ProductInfo = GetBuildVersion(path); } var fileInfo = new FileInfo(path); var productName = RiderScriptEditor.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; } } private static class Logger { internal static void Warn(string message, Exception e = null) { #if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider Log.GetLog(typeof(RiderPathLocator).Name).Warn(message); if (e != null) Log.GetLog(typeof(RiderPathLocator).Name).Warn(e); #else Debug.LogError(message); if (e != null) Debug.LogException(e); #endif } } } }