Discovery.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using JetBrains.Annotations;
  6. using Microsoft.Win32;
  7. using Unity.CodeEditor;
  8. using UnityEngine;
  9. namespace Packages.Rider.Editor
  10. {
  11. public interface IDiscovery
  12. {
  13. CodeEditor.Installation[] PathCallback();
  14. }
  15. public class Discovery : IDiscovery
  16. {
  17. public CodeEditor.Installation[] PathCallback()
  18. {
  19. return RiderPathLocator.GetAllRiderPaths()
  20. .Select(riderInfo => new CodeEditor.Installation
  21. {
  22. Path = riderInfo.Path,
  23. Name = riderInfo.Presentation
  24. })
  25. .OrderBy(a=>a.Name)
  26. .ToArray();
  27. }
  28. }
  29. /// <summary>
  30. /// This code is a modified version of the JetBrains resharper-unity plugin listed here:
  31. /// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs
  32. /// </summary>
  33. public static class RiderPathLocator
  34. {
  35. #if !(UNITY_4_7 || UNITY_5_5)
  36. [UsedImplicitly] // Used in com.unity.ide.rider
  37. public static RiderInfo[] GetAllRiderPaths()
  38. {
  39. try
  40. {
  41. switch (SystemInfo.operatingSystemFamily)
  42. {
  43. case OperatingSystemFamily.Windows:
  44. {
  45. return CollectRiderInfosWindows();
  46. }
  47. case OperatingSystemFamily.MacOSX:
  48. {
  49. return CollectRiderInfosMac();
  50. }
  51. case OperatingSystemFamily.Linux:
  52. {
  53. return CollectAllRiderPathsLinux();
  54. }
  55. }
  56. }
  57. catch (Exception e)
  58. {
  59. Debug.LogException(e);
  60. }
  61. return new RiderInfo[0];
  62. }
  63. #endif
  64. #if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider
  65. internal static RiderInfo[] GetAllFoundInfos(OperatingSystemFamilyRider operatingSystemFamily)
  66. {
  67. try
  68. {
  69. switch (operatingSystemFamily)
  70. {
  71. case OperatingSystemFamilyRider.Windows:
  72. {
  73. return CollectRiderInfosWindows();
  74. }
  75. case OperatingSystemFamilyRider.MacOSX:
  76. {
  77. return CollectRiderInfosMac();
  78. }
  79. case OperatingSystemFamilyRider.Linux:
  80. {
  81. return CollectAllRiderPathsLinux();
  82. }
  83. }
  84. }
  85. catch (Exception e)
  86. {
  87. Debug.LogException(e);
  88. }
  89. return new RiderInfo[0];
  90. }
  91. internal static string[] GetAllFoundPaths(OperatingSystemFamilyRider operatingSystemFamily)
  92. {
  93. return GetAllFoundInfos(operatingSystemFamily).Select(a=>a.Path).ToArray();
  94. }
  95. #endif
  96. private static RiderInfo[] CollectAllRiderPathsLinux()
  97. {
  98. var installInfos = new List<RiderInfo>();
  99. var home = Environment.GetEnvironmentVariable("HOME");
  100. if (!string.IsNullOrEmpty(home))
  101. {
  102. var toolboxRiderRootPath = GetToolboxBaseDir();
  103. installInfos.AddRange(CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider.sh", false)
  104. .Select(a => new RiderInfo(a, true)).ToList());
  105. //$Home/.local/share/applications/jetbrains-rider.desktop
  106. var shortcut = new FileInfo(Path.Combine(home, @".local/share/applications/jetbrains-rider.desktop"));
  107. if (shortcut.Exists)
  108. {
  109. var lines = File.ReadAllLines(shortcut.FullName);
  110. foreach (var line in lines)
  111. {
  112. if (!line.StartsWith("Exec=\""))
  113. continue;
  114. var path = line.Split('"').Where((item, index) => index == 1).SingleOrDefault();
  115. if (string.IsNullOrEmpty(path))
  116. continue;
  117. if (installInfos.Any(a => a.Path == path)) // avoid adding similar build as from toolbox
  118. continue;
  119. installInfos.Add(new RiderInfo(path, false));
  120. }
  121. }
  122. }
  123. // snap install
  124. var snapInstallPath = "/snap/rider/current/bin/rider.sh";
  125. if (new FileInfo(snapInstallPath).Exists)
  126. installInfos.Add(new RiderInfo(snapInstallPath, false));
  127. return installInfos.ToArray();
  128. }
  129. private static RiderInfo[] CollectRiderInfosMac()
  130. {
  131. var installInfos = new List<RiderInfo>();
  132. // "/Applications/*Rider*.app"
  133. var folder = new DirectoryInfo("/Applications");
  134. if (folder.Exists)
  135. {
  136. installInfos.AddRange(folder.GetDirectories("*Rider*.app")
  137. .Select(a => new RiderInfo(a.FullName, false))
  138. .ToList());
  139. }
  140. // /Users/user/Library/Application Support/JetBrains/Toolbox/apps/Rider/ch-1/181.3870.267/Rider EAP.app
  141. var toolboxRiderRootPath = GetToolboxBaseDir();
  142. var paths = CollectPathsFromToolbox(toolboxRiderRootPath, "", "Rider*.app", true)
  143. .Select(a => new RiderInfo(a, true));
  144. installInfos.AddRange(paths);
  145. return installInfos.ToArray();
  146. }
  147. private static RiderInfo[] CollectRiderInfosWindows()
  148. {
  149. var installInfos = new List<RiderInfo>();
  150. var toolboxRiderRootPath = GetToolboxBaseDir();
  151. var installPathsToolbox = CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider64.exe", false).ToList();
  152. installInfos.AddRange(installPathsToolbox.Select(a => new RiderInfo(a, true)).ToList());
  153. var installPaths = new List<string>();
  154. const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
  155. CollectPathsFromRegistry(registryKey, installPaths);
  156. const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
  157. CollectPathsFromRegistry(wowRegistryKey, installPaths);
  158. installInfos.AddRange(installPaths.Select(a => new RiderInfo(a, false)).ToList());
  159. return installInfos.ToArray();
  160. }
  161. private static string GetToolboxBaseDir()
  162. {
  163. switch (SystemInfo.operatingSystemFamily)
  164. {
  165. case OperatingSystemFamily.Windows:
  166. {
  167. var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
  168. return Path.Combine(localAppData, @"JetBrains\Toolbox\apps\Rider");
  169. }
  170. case OperatingSystemFamily.MacOSX:
  171. {
  172. var home = Environment.GetEnvironmentVariable("HOME");
  173. if (!string.IsNullOrEmpty(home))
  174. {
  175. return Path.Combine(home, @"Library/Application Support/JetBrains/Toolbox/apps/Rider");
  176. }
  177. break;
  178. }
  179. case OperatingSystemFamily.Linux:
  180. {
  181. var home = Environment.GetEnvironmentVariable("HOME");
  182. if (!string.IsNullOrEmpty(home))
  183. {
  184. return Path.Combine(home, @".local/share/JetBrains/Toolbox/apps/Rider");
  185. }
  186. break;
  187. }
  188. }
  189. return string.Empty;
  190. }
  191. internal static string GetBuildNumber(string path)
  192. {
  193. var file = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
  194. if (!file.Exists)
  195. return string.Empty;
  196. var text = File.ReadAllText(file.FullName);
  197. if (text.Length > 3)
  198. return text.Substring(3);
  199. return string.Empty;
  200. }
  201. internal static bool IsToolbox(string path)
  202. {
  203. return path.StartsWith(GetToolboxBaseDir());
  204. }
  205. private static string GetRelativePathToBuildTxt()
  206. {
  207. switch (SystemInfo.operatingSystemFamily)
  208. {
  209. case OperatingSystemFamily.Windows:
  210. case OperatingSystemFamily.Linux:
  211. return "../../build.txt";
  212. case OperatingSystemFamily.MacOSX:
  213. return "Contents/Resources/build.txt";
  214. }
  215. throw new Exception("Unknown OS");
  216. }
  217. private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths)
  218. {
  219. using (var key = Registry.LocalMachine.OpenSubKey(registryKey))
  220. {
  221. if (key == null) return;
  222. foreach (var subkeyName in key.GetSubKeyNames().Where(a => a.Contains("Rider")))
  223. {
  224. using (var subkey = key.OpenSubKey(subkeyName))
  225. {
  226. var folderObject = subkey?.GetValue("InstallLocation");
  227. if (folderObject == null) continue;
  228. var folder = folderObject.ToString();
  229. var possiblePath = Path.Combine(folder, @"bin\rider64.exe");
  230. if (File.Exists(possiblePath))
  231. installPaths.Add(possiblePath);
  232. }
  233. }
  234. }
  235. }
  236. private static string[] CollectPathsFromToolbox(string toolboxRiderRootPath, string dirName, string searchPattern,
  237. bool isMac)
  238. {
  239. if (!Directory.Exists(toolboxRiderRootPath))
  240. return new string[0];
  241. var channelDirs = Directory.GetDirectories(toolboxRiderRootPath);
  242. var paths = channelDirs.SelectMany(channelDir =>
  243. {
  244. try
  245. {
  246. // use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D
  247. var historyFile = Path.Combine(channelDir, ".history.json");
  248. if (File.Exists(historyFile))
  249. {
  250. var json = File.ReadAllText(historyFile);
  251. var build = ToolboxHistory.GetLatestBuildFromJson(json);
  252. if (build != null)
  253. {
  254. var buildDir = Path.Combine(channelDir, build);
  255. var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
  256. if (executablePaths.Any())
  257. return executablePaths;
  258. }
  259. }
  260. var channelFile = Path.Combine(channelDir, ".channel.settings.json");
  261. if (File.Exists(channelFile))
  262. {
  263. var json = File.ReadAllText(channelFile).Replace("active-application", "active_application");
  264. var build = ToolboxInstallData.GetLatestBuildFromJson(json);
  265. if (build != null)
  266. {
  267. var buildDir = Path.Combine(channelDir, build);
  268. var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
  269. if (executablePaths.Any())
  270. return executablePaths;
  271. }
  272. }
  273. // changes in toolbox json files format may brake the logic above, so return all found Rider installations
  274. return Directory.GetDirectories(channelDir)
  275. .SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir));
  276. }
  277. catch (Exception e)
  278. {
  279. // do not write to Debug.Log, just log it.
  280. Logger.Warn($"Failed to get RiderPath from {channelDir}", e);
  281. }
  282. return new string[0];
  283. })
  284. .Where(c => !string.IsNullOrEmpty(c))
  285. .ToArray();
  286. return paths;
  287. }
  288. private static string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir)
  289. {
  290. var folder = new DirectoryInfo(Path.Combine(buildDir, dirName));
  291. if (!folder.Exists)
  292. return new string[0];
  293. if (!isMac)
  294. return new[] {Path.Combine(folder.FullName, searchPattern)}.Where(File.Exists).ToArray();
  295. return folder.GetDirectories(searchPattern).Select(f => f.FullName)
  296. .Where(Directory.Exists).ToArray();
  297. }
  298. // Disable the "field is never assigned" compiler warning. We never assign it, but Unity does.
  299. // Note that Unity disable this warning in the generated C# projects
  300. #pragma warning disable 0649
  301. [Serializable]
  302. class ToolboxHistory
  303. {
  304. public List<ItemNode> history;
  305. [CanBeNull]
  306. public static string GetLatestBuildFromJson(string json)
  307. {
  308. try
  309. {
  310. #if UNITY_4_7 || UNITY_5_5
  311. return JsonConvert.DeserializeObject<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
  312. #else
  313. return JsonUtility.FromJson<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
  314. #endif
  315. }
  316. catch (Exception)
  317. {
  318. Logger.Warn($"Failed to get latest build from json {json}");
  319. }
  320. return null;
  321. }
  322. }
  323. [Serializable]
  324. class ItemNode
  325. {
  326. public BuildNode item;
  327. }
  328. [Serializable]
  329. class BuildNode
  330. {
  331. public string build;
  332. }
  333. // ReSharper disable once ClassNeverInstantiated.Global
  334. [Serializable]
  335. class ToolboxInstallData
  336. {
  337. // ReSharper disable once InconsistentNaming
  338. public ActiveApplication active_application;
  339. [CanBeNull]
  340. public static string GetLatestBuildFromJson(string json)
  341. {
  342. try
  343. {
  344. #if UNITY_4_7 || UNITY_5_5
  345. var toolbox = JsonConvert.DeserializeObject<ToolboxInstallData>(json);
  346. #else
  347. var toolbox = JsonUtility.FromJson<ToolboxInstallData>(json);
  348. #endif
  349. var builds = toolbox.active_application.builds;
  350. if (builds != null && builds.Any())
  351. return builds.First();
  352. }
  353. catch (Exception)
  354. {
  355. Logger.Warn($"Failed to get latest build from json {json}");
  356. }
  357. return null;
  358. }
  359. }
  360. [Serializable]
  361. class ActiveApplication
  362. {
  363. // ReSharper disable once InconsistentNaming
  364. public List<string> builds;
  365. }
  366. #pragma warning restore 0649
  367. public struct RiderInfo
  368. {
  369. public bool IsToolbox;
  370. public string Presentation;
  371. public string BuildVersion;
  372. public string Path;
  373. public RiderInfo(string path, bool isToolbox)
  374. {
  375. if (path == RiderScriptEditor.CurrentEditor)
  376. {
  377. RiderScriptEditorData.instance.Init();
  378. BuildVersion = RiderScriptEditorData.instance.currentEditorVersion;
  379. }
  380. else
  381. BuildVersion = GetBuildNumber(path);
  382. Path = new FileInfo(path).FullName; // normalize separators
  383. var presentation = "Rider " + BuildVersion;
  384. if (isToolbox)
  385. presentation += " (JetBrains Toolbox)";
  386. Presentation = presentation;
  387. IsToolbox = isToolbox;
  388. }
  389. }
  390. private static class Logger
  391. {
  392. internal static void Warn(string message, Exception e = null)
  393. {
  394. #if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider
  395. Log.GetLog(typeof(RiderPathLocator).Name).Warn(message);
  396. if (e != null)
  397. Log.GetLog(typeof(RiderPathLocator).Name).Warn(e);
  398. #else
  399. Debug.LogError(message);
  400. if (e != null)
  401. Debug.LogException(e);
  402. #endif
  403. }
  404. }
  405. }
  406. }