Discovery.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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 Packages.Rider.Editor.Util;
  8. using Unity.CodeEditor;
  9. using UnityEngine;
  10. namespace Packages.Rider.Editor
  11. {
  12. internal interface IDiscovery
  13. {
  14. CodeEditor.Installation[] PathCallback();
  15. }
  16. internal class Discovery : IDiscovery
  17. {
  18. public CodeEditor.Installation[] PathCallback()
  19. {
  20. return RiderPathLocator.GetAllRiderPaths()
  21. .Select(riderInfo => new CodeEditor.Installation
  22. {
  23. Path = riderInfo.Path,
  24. Name = riderInfo.Presentation
  25. })
  26. .OrderBy(a=>a.Name)
  27. .ToArray();
  28. }
  29. }
  30. /// <summary>
  31. /// This code is a modified version of the JetBrains resharper-unity plugin listed here:
  32. /// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs
  33. /// </summary>
  34. internal static class RiderPathLocator
  35. {
  36. #if !(UNITY_4_7 || UNITY_5_5)
  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 GetToolboxRiderRootPath(localAppData);
  169. }
  170. case OperatingSystemFamily.MacOSX:
  171. {
  172. var home = Environment.GetEnvironmentVariable("HOME");
  173. if (!string.IsNullOrEmpty(home))
  174. {
  175. var localAppData = Path.Combine(home, @"Library/Application Support");
  176. return GetToolboxRiderRootPath(localAppData);
  177. }
  178. break;
  179. }
  180. case OperatingSystemFamily.Linux:
  181. {
  182. var home = Environment.GetEnvironmentVariable("HOME");
  183. if (!string.IsNullOrEmpty(home))
  184. {
  185. var localAppData = Path.Combine(home, @".local/share");
  186. return GetToolboxRiderRootPath(localAppData);
  187. }
  188. break;
  189. }
  190. }
  191. return string.Empty;
  192. }
  193. private static string GetToolboxRiderRootPath(string localAppData)
  194. {
  195. var toolboxPath = Path.Combine(localAppData, @"JetBrains/Toolbox");
  196. var settingsJson = Path.Combine(toolboxPath, ".settings.json");
  197. if (File.Exists(settingsJson))
  198. {
  199. var path = SettingsJson.GetInstallLocationFromJson(File.ReadAllText(settingsJson));
  200. if (!string.IsNullOrEmpty(path))
  201. toolboxPath = path;
  202. }
  203. var toolboxRiderRootPath = Path.Combine(toolboxPath, @"apps/Rider");
  204. return toolboxRiderRootPath;
  205. }
  206. internal static ProductInfo GetBuildVersion(string path)
  207. {
  208. var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
  209. var dir = buildTxtFileInfo.DirectoryName;
  210. if (!Directory.Exists(dir))
  211. return null;
  212. var buildVersionFile = new FileInfo(Path.Combine(dir, "product-info.json"));
  213. if (!buildVersionFile.Exists)
  214. return null;
  215. var json = File.ReadAllText(buildVersionFile.FullName);
  216. return ProductInfo.GetProductInfo(json);
  217. }
  218. internal static Version GetBuildNumber(string path)
  219. {
  220. var file = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
  221. if (!file.Exists)
  222. return null;
  223. var text = File.ReadAllText(file.FullName);
  224. if (text.Length <= 3)
  225. return null;
  226. var versionText = text.Substring(3);
  227. return Version.TryParse(versionText, out var v) ? v : null;
  228. }
  229. internal static bool GetIsToolbox(string path)
  230. {
  231. return path.StartsWith(GetToolboxBaseDir());
  232. }
  233. private static string GetRelativePathToBuildTxt()
  234. {
  235. switch (SystemInfo.operatingSystemFamily)
  236. {
  237. case OperatingSystemFamily.Windows:
  238. case OperatingSystemFamily.Linux:
  239. return "../../build.txt";
  240. case OperatingSystemFamily.MacOSX:
  241. return "Contents/Resources/build.txt";
  242. }
  243. throw new Exception("Unknown OS");
  244. }
  245. private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths)
  246. {
  247. using (var key = Registry.CurrentUser.OpenSubKey(registryKey))
  248. {
  249. CollectPathsFromRegistry(installPaths, key);
  250. }
  251. using (var key = Registry.LocalMachine.OpenSubKey(registryKey))
  252. {
  253. CollectPathsFromRegistry(installPaths, key);
  254. }
  255. }
  256. private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key)
  257. {
  258. if (key == null) return;
  259. foreach (var subkeyName in key.GetSubKeyNames().Where(a => a.Contains("Rider")))
  260. {
  261. using (var subkey = key.OpenSubKey(subkeyName))
  262. {
  263. var folderObject = subkey?.GetValue("InstallLocation");
  264. if (folderObject == null) continue;
  265. var folder = folderObject.ToString();
  266. var possiblePath = Path.Combine(folder, @"bin\rider64.exe");
  267. if (File.Exists(possiblePath))
  268. installPaths.Add(possiblePath);
  269. }
  270. }
  271. }
  272. private static string[] CollectPathsFromToolbox(string toolboxRiderRootPath, string dirName, string searchPattern,
  273. bool isMac)
  274. {
  275. if (!Directory.Exists(toolboxRiderRootPath))
  276. return new string[0];
  277. var channelDirs = Directory.GetDirectories(toolboxRiderRootPath);
  278. var paths = channelDirs.SelectMany(channelDir =>
  279. {
  280. try
  281. {
  282. // use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D
  283. var historyFile = Path.Combine(channelDir, ".history.json");
  284. if (File.Exists(historyFile))
  285. {
  286. var json = File.ReadAllText(historyFile);
  287. var build = ToolboxHistory.GetLatestBuildFromJson(json);
  288. if (build != null)
  289. {
  290. var buildDir = Path.Combine(channelDir, build);
  291. var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
  292. if (executablePaths.Any())
  293. return executablePaths;
  294. }
  295. }
  296. var channelFile = Path.Combine(channelDir, ".channel.settings.json");
  297. if (File.Exists(channelFile))
  298. {
  299. var json = File.ReadAllText(channelFile).Replace("active-application", "active_application");
  300. var build = ToolboxInstallData.GetLatestBuildFromJson(json);
  301. if (build != null)
  302. {
  303. var buildDir = Path.Combine(channelDir, build);
  304. var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
  305. if (executablePaths.Any())
  306. return executablePaths;
  307. }
  308. }
  309. // changes in toolbox json files format may brake the logic above, so return all found Rider installations
  310. return Directory.GetDirectories(channelDir)
  311. .SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir));
  312. }
  313. catch (Exception e)
  314. {
  315. // do not write to Debug.Log, just log it.
  316. Logger.Warn($"Failed to get RiderPath from {channelDir}", e);
  317. }
  318. return new string[0];
  319. })
  320. .Where(c => !string.IsNullOrEmpty(c))
  321. .ToArray();
  322. return paths;
  323. }
  324. private static string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir)
  325. {
  326. var folder = new DirectoryInfo(Path.Combine(buildDir, dirName));
  327. if (!folder.Exists)
  328. return new string[0];
  329. if (!isMac)
  330. return new[] {Path.Combine(folder.FullName, searchPattern)}.Where(File.Exists).ToArray();
  331. return folder.GetDirectories(searchPattern).Select(f => f.FullName)
  332. .Where(Directory.Exists).ToArray();
  333. }
  334. // Disable the "field is never assigned" compiler warning. We never assign it, but Unity does.
  335. // Note that Unity disable this warning in the generated C# projects
  336. #pragma warning disable 0649
  337. [Serializable]
  338. class SettingsJson
  339. {
  340. // ReSharper disable once InconsistentNaming
  341. public string install_location;
  342. [CanBeNull]
  343. public static string GetInstallLocationFromJson(string json)
  344. {
  345. try
  346. {
  347. #if UNITY_4_7 || UNITY_5_5
  348. return JsonConvert.DeserializeObject<SettingsJson>(json).install_location;
  349. #else
  350. return JsonUtility.FromJson<SettingsJson>(json).install_location;
  351. #endif
  352. }
  353. catch (Exception)
  354. {
  355. Logger.Warn($"Failed to get install_location from json {json}");
  356. }
  357. return null;
  358. }
  359. }
  360. [Serializable]
  361. class ToolboxHistory
  362. {
  363. public List<ItemNode> history;
  364. [CanBeNull]
  365. public static string GetLatestBuildFromJson(string json)
  366. {
  367. try
  368. {
  369. #if UNITY_4_7 || UNITY_5_5
  370. return JsonConvert.DeserializeObject<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
  371. #else
  372. return JsonUtility.FromJson<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
  373. #endif
  374. }
  375. catch (Exception)
  376. {
  377. Logger.Warn($"Failed to get latest build from json {json}");
  378. }
  379. return null;
  380. }
  381. }
  382. [Serializable]
  383. class ItemNode
  384. {
  385. public BuildNode item;
  386. }
  387. [Serializable]
  388. class BuildNode
  389. {
  390. public string build;
  391. }
  392. [Serializable]
  393. internal class ProductInfo
  394. {
  395. public string version;
  396. public string versionSuffix;
  397. [CanBeNull]
  398. internal static ProductInfo GetProductInfo(string json)
  399. {
  400. try
  401. {
  402. var productInfo = JsonUtility.FromJson<ProductInfo>(json);
  403. return productInfo;
  404. }
  405. catch (Exception)
  406. {
  407. Logger.Warn($"Failed to get version from json {json}");
  408. }
  409. return null;
  410. }
  411. }
  412. // ReSharper disable once ClassNeverInstantiated.Global
  413. [Serializable]
  414. class ToolboxInstallData
  415. {
  416. // ReSharper disable once InconsistentNaming
  417. public ActiveApplication active_application;
  418. [CanBeNull]
  419. public static string GetLatestBuildFromJson(string json)
  420. {
  421. try
  422. {
  423. #if UNITY_4_7 || UNITY_5_5
  424. var toolbox = JsonConvert.DeserializeObject<ToolboxInstallData>(json);
  425. #else
  426. var toolbox = JsonUtility.FromJson<ToolboxInstallData>(json);
  427. #endif
  428. var builds = toolbox.active_application.builds;
  429. if (builds != null && builds.Any())
  430. return builds.First();
  431. }
  432. catch (Exception)
  433. {
  434. Logger.Warn($"Failed to get latest build from json {json}");
  435. }
  436. return null;
  437. }
  438. }
  439. [Serializable]
  440. class ActiveApplication
  441. {
  442. // ReSharper disable once InconsistentNaming
  443. public List<string> builds;
  444. }
  445. #pragma warning restore 0649
  446. internal struct RiderInfo
  447. {
  448. public bool IsToolbox;
  449. public string Presentation;
  450. public Version BuildNumber;
  451. public ProductInfo ProductInfo;
  452. public string Path;
  453. public RiderInfo(string path, bool isToolbox)
  454. {
  455. if (path == RiderScriptEditor.CurrentEditor)
  456. {
  457. RiderScriptEditorData.instance.Init();
  458. BuildNumber = RiderScriptEditorData.instance.editorBuildNumber.ToVersion();
  459. ProductInfo = RiderScriptEditorData.instance.productInfo;
  460. }
  461. else
  462. {
  463. BuildNumber = GetBuildNumber(path);
  464. ProductInfo = GetBuildVersion(path);
  465. }
  466. Path = new FileInfo(path).FullName; // normalize separators
  467. var presentation = $"Rider {BuildNumber}";
  468. if (ProductInfo != null && !string.IsNullOrEmpty(ProductInfo.version))
  469. {
  470. var suffix = string.IsNullOrEmpty(ProductInfo.versionSuffix) ? "" : $" {ProductInfo.versionSuffix}";
  471. presentation = $"Rider {ProductInfo.version}{suffix}";
  472. }
  473. if (isToolbox)
  474. presentation += " (JetBrains Toolbox)";
  475. Presentation = presentation;
  476. IsToolbox = isToolbox;
  477. }
  478. }
  479. private static class Logger
  480. {
  481. internal static void Warn(string message, Exception e = null)
  482. {
  483. #if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider
  484. Log.GetLog(typeof(RiderPathLocator).Name).Warn(message);
  485. if (e != null)
  486. Log.GetLog(typeof(RiderPathLocator).Name).Warn(e);
  487. #else
  488. Debug.LogError(message);
  489. if (e != null)
  490. Debug.LogException(e);
  491. #endif
  492. }
  493. }
  494. }
  495. }