설명 없음
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Discovery.cs 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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. var res = RiderPathLocator.GetAllRiderPaths()
  21. .Select(riderInfo => new CodeEditor.Installation
  22. {
  23. Path = riderInfo.Path,
  24. Name = riderInfo.Presentation
  25. })
  26. .ToList();
  27. var editorPath = RiderScriptEditor.CurrentEditor;
  28. if (RiderScriptEditor.IsRiderInstallation(editorPath) &&
  29. !res.Any(a => a.Path == editorPath) &&
  30. FileSystemUtil.EditorPathExists(editorPath))
  31. {
  32. // External editor manually set from custom location
  33. var info = new RiderPathLocator.RiderInfo(editorPath, false);
  34. var installation = new CodeEditor.Installation
  35. {
  36. Path = info.Path,
  37. Name = info.Presentation
  38. };
  39. res.Add(installation);
  40. }
  41. return res.ToArray();
  42. }
  43. }
  44. /// <summary>
  45. /// This code is a modified version of the JetBrains resharper-unity plugin listed here:
  46. /// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs
  47. /// </summary>
  48. internal static class RiderPathLocator
  49. {
  50. #if !(UNITY_4_7 || UNITY_5_5)
  51. public static RiderInfo[] GetAllRiderPaths()
  52. {
  53. try
  54. {
  55. switch (SystemInfo.operatingSystemFamily)
  56. {
  57. case OperatingSystemFamily.Windows:
  58. {
  59. return CollectRiderInfosWindows();
  60. }
  61. case OperatingSystemFamily.MacOSX:
  62. {
  63. return CollectRiderInfosMac();
  64. }
  65. case OperatingSystemFamily.Linux:
  66. {
  67. return CollectAllRiderPathsLinux();
  68. }
  69. }
  70. }
  71. catch (Exception e)
  72. {
  73. Debug.LogException(e);
  74. }
  75. return new RiderInfo[0];
  76. }
  77. #endif
  78. #if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider
  79. internal static RiderInfo[] GetAllFoundInfos(OperatingSystemFamilyRider operatingSystemFamily)
  80. {
  81. try
  82. {
  83. switch (operatingSystemFamily)
  84. {
  85. case OperatingSystemFamilyRider.Windows:
  86. {
  87. return CollectRiderInfosWindows();
  88. }
  89. case OperatingSystemFamilyRider.MacOSX:
  90. {
  91. return CollectRiderInfosMac();
  92. }
  93. case OperatingSystemFamilyRider.Linux:
  94. {
  95. return CollectAllRiderPathsLinux();
  96. }
  97. }
  98. }
  99. catch (Exception e)
  100. {
  101. Debug.LogException(e);
  102. }
  103. return new RiderInfo[0];
  104. }
  105. internal static string[] GetAllFoundPaths(OperatingSystemFamilyRider operatingSystemFamily)
  106. {
  107. return GetAllFoundInfos(operatingSystemFamily).Select(a=>a.Path).ToArray();
  108. }
  109. #endif
  110. private static RiderInfo[] CollectAllRiderPathsLinux()
  111. {
  112. var installInfos = new List<RiderInfo>();
  113. var home = Environment.GetEnvironmentVariable("HOME");
  114. if (!string.IsNullOrEmpty(home))
  115. {
  116. var toolboxRiderRootPath = GetToolboxBaseDir();
  117. installInfos.AddRange(CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider.sh", false)
  118. .Select(a => new RiderInfo(a, true)).ToList());
  119. //$Home/.local/share/applications/jetbrains-rider.desktop
  120. var shortcut = new FileInfo(Path.Combine(home, @".local/share/applications/jetbrains-rider.desktop"));
  121. if (shortcut.Exists)
  122. {
  123. var lines = File.ReadAllLines(shortcut.FullName);
  124. foreach (var line in lines)
  125. {
  126. if (!line.StartsWith("Exec=\""))
  127. continue;
  128. var path = line.Split('"').Where((item, index) => index == 1).SingleOrDefault();
  129. if (string.IsNullOrEmpty(path))
  130. continue;
  131. if (installInfos.Any(a => a.Path == path)) // avoid adding similar build as from toolbox
  132. continue;
  133. installInfos.Add(new RiderInfo(path, false));
  134. }
  135. }
  136. }
  137. // snap install
  138. var snapInstallPath = "/snap/rider/current/bin/rider.sh";
  139. if (new FileInfo(snapInstallPath).Exists)
  140. installInfos.Add(new RiderInfo(snapInstallPath, false));
  141. return installInfos.ToArray();
  142. }
  143. private static RiderInfo[] CollectRiderInfosMac()
  144. {
  145. var installInfos = new List<RiderInfo>();
  146. // "/Applications/*Rider*.app"
  147. var folder = new DirectoryInfo("/Applications");
  148. if (folder.Exists)
  149. {
  150. installInfos.AddRange(folder.GetDirectories("*Rider*.app")
  151. .Select(a => new RiderInfo(a.FullName, false))
  152. .ToList());
  153. }
  154. // /Users/user/Library/Application Support/JetBrains/Toolbox/apps/Rider/ch-1/181.3870.267/Rider EAP.app
  155. var toolboxRiderRootPath = GetToolboxBaseDir();
  156. var paths = CollectPathsFromToolbox(toolboxRiderRootPath, "", "Rider*.app", true)
  157. .Select(a => new RiderInfo(a, true));
  158. installInfos.AddRange(paths);
  159. return installInfos.ToArray();
  160. }
  161. private static RiderInfo[] CollectRiderInfosWindows()
  162. {
  163. var installInfos = new List<RiderInfo>();
  164. var toolboxRiderRootPath = GetToolboxBaseDir();
  165. var installPathsToolbox = CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider64.exe", false).ToList();
  166. installInfos.AddRange(installPathsToolbox.Select(a => new RiderInfo(a, true)).ToList());
  167. var installPaths = new List<string>();
  168. const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
  169. CollectPathsFromRegistry(registryKey, installPaths);
  170. const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
  171. CollectPathsFromRegistry(wowRegistryKey, installPaths);
  172. installInfos.AddRange(installPaths.Select(a => new RiderInfo(a, false)).ToList());
  173. return installInfos.ToArray();
  174. }
  175. private static string GetToolboxBaseDir()
  176. {
  177. switch (SystemInfo.operatingSystemFamily)
  178. {
  179. case OperatingSystemFamily.Windows:
  180. {
  181. var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
  182. return GetToolboxRiderRootPath(localAppData);
  183. }
  184. case OperatingSystemFamily.MacOSX:
  185. {
  186. var home = Environment.GetEnvironmentVariable("HOME");
  187. if (!string.IsNullOrEmpty(home))
  188. {
  189. var localAppData = Path.Combine(home, @"Library/Application Support");
  190. return GetToolboxRiderRootPath(localAppData);
  191. }
  192. break;
  193. }
  194. case OperatingSystemFamily.Linux:
  195. {
  196. var home = Environment.GetEnvironmentVariable("HOME");
  197. if (!string.IsNullOrEmpty(home))
  198. {
  199. var localAppData = Path.Combine(home, @".local/share");
  200. return GetToolboxRiderRootPath(localAppData);
  201. }
  202. break;
  203. }
  204. }
  205. return string.Empty;
  206. }
  207. private static string GetToolboxRiderRootPath(string localAppData)
  208. {
  209. var toolboxPath = Path.Combine(localAppData, @"JetBrains/Toolbox");
  210. var settingsJson = Path.Combine(toolboxPath, ".settings.json");
  211. if (File.Exists(settingsJson))
  212. {
  213. var path = SettingsJson.GetInstallLocationFromJson(File.ReadAllText(settingsJson));
  214. if (!string.IsNullOrEmpty(path))
  215. toolboxPath = path;
  216. }
  217. var toolboxRiderRootPath = Path.Combine(toolboxPath, @"apps/Rider");
  218. return toolboxRiderRootPath;
  219. }
  220. internal static ProductInfo GetBuildVersion(string path)
  221. {
  222. var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
  223. var dir = buildTxtFileInfo.DirectoryName;
  224. if (!Directory.Exists(dir))
  225. return null;
  226. var buildVersionFile = new FileInfo(Path.Combine(dir, "product-info.json"));
  227. if (!buildVersionFile.Exists)
  228. return null;
  229. var json = File.ReadAllText(buildVersionFile.FullName);
  230. return ProductInfo.GetProductInfo(json);
  231. }
  232. internal static Version GetBuildNumber(string path)
  233. {
  234. var file = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
  235. if (!file.Exists)
  236. return null;
  237. var text = File.ReadAllText(file.FullName);
  238. var index = text.IndexOf("-", StringComparison.Ordinal) + 1; // RD-191.7141.355
  239. if (index <= 0)
  240. return null;
  241. var versionText = text.Substring(index);
  242. return Version.TryParse(versionText, out var v) ? v : null;
  243. }
  244. internal static bool GetIsToolbox(string path)
  245. {
  246. return Path.GetFullPath(path).StartsWith(Path.GetFullPath(GetToolboxBaseDir()));
  247. }
  248. private static string GetRelativePathToBuildTxt()
  249. {
  250. switch (SystemInfo.operatingSystemFamily)
  251. {
  252. case OperatingSystemFamily.Windows:
  253. case OperatingSystemFamily.Linux:
  254. return "../../build.txt";
  255. case OperatingSystemFamily.MacOSX:
  256. return "Contents/Resources/build.txt";
  257. }
  258. throw new Exception("Unknown OS");
  259. }
  260. private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths)
  261. {
  262. using (var key = Registry.CurrentUser.OpenSubKey(registryKey))
  263. {
  264. CollectPathsFromRegistry(installPaths, key);
  265. }
  266. using (var key = Registry.LocalMachine.OpenSubKey(registryKey))
  267. {
  268. CollectPathsFromRegistry(installPaths, key);
  269. }
  270. }
  271. private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key)
  272. {
  273. if (key == null) return;
  274. foreach (var subkeyName in key.GetSubKeyNames())
  275. {
  276. using (var subkey = key.OpenSubKey(subkeyName))
  277. {
  278. var folderObject = subkey?.GetValue("InstallLocation");
  279. if (folderObject == null) continue;
  280. var folder = folderObject.ToString();
  281. if (folder.Length == 0) continue;
  282. var displayName = subkey.GetValue("DisplayName");
  283. if (displayName == null) continue;
  284. if (!displayName.ToString().Contains("Rider")) continue;
  285. try // possible "illegal characters in path"
  286. {
  287. var possiblePath = Path.Combine(folder, @"bin\rider64.exe");
  288. if (File.Exists(possiblePath))
  289. installPaths.Add(possiblePath);
  290. }
  291. catch (ArgumentException) { }
  292. }
  293. }
  294. }
  295. private static string[] CollectPathsFromToolbox(string toolboxRiderRootPath, string dirName, string searchPattern,
  296. bool isMac)
  297. {
  298. if (!Directory.Exists(toolboxRiderRootPath))
  299. return new string[0];
  300. var channelDirs = Directory.GetDirectories(toolboxRiderRootPath);
  301. var paths = channelDirs.SelectMany(channelDir =>
  302. {
  303. try
  304. {
  305. // use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D
  306. var historyFile = Path.Combine(channelDir, ".history.json");
  307. if (File.Exists(historyFile))
  308. {
  309. var json = File.ReadAllText(historyFile);
  310. var build = ToolboxHistory.GetLatestBuildFromJson(json);
  311. if (build != null)
  312. {
  313. var buildDir = Path.Combine(channelDir, build);
  314. var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
  315. if (executablePaths.Any())
  316. return executablePaths;
  317. }
  318. }
  319. var channelFile = Path.Combine(channelDir, ".channel.settings.json");
  320. if (File.Exists(channelFile))
  321. {
  322. var json = File.ReadAllText(channelFile).Replace("active-application", "active_application");
  323. var build = ToolboxInstallData.GetLatestBuildFromJson(json);
  324. if (build != null)
  325. {
  326. var buildDir = Path.Combine(channelDir, build);
  327. var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
  328. if (executablePaths.Any())
  329. return executablePaths;
  330. }
  331. }
  332. // changes in toolbox json files format may brake the logic above, so return all found Rider installations
  333. return Directory.GetDirectories(channelDir)
  334. .SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir));
  335. }
  336. catch (Exception e)
  337. {
  338. // do not write to Debug.Log, just log it.
  339. Logger.Warn($"Failed to get RiderPath from {channelDir}", e);
  340. }
  341. return new string[0];
  342. })
  343. .Where(c => !string.IsNullOrEmpty(c))
  344. .ToArray();
  345. return paths;
  346. }
  347. private static string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir)
  348. {
  349. var folder = new DirectoryInfo(Path.Combine(buildDir, dirName));
  350. if (!folder.Exists)
  351. return new string[0];
  352. if (!isMac)
  353. return new[] {Path.Combine(folder.FullName, searchPattern)}.Where(File.Exists).ToArray();
  354. return folder.GetDirectories(searchPattern).Select(f => f.FullName)
  355. .Where(Directory.Exists).ToArray();
  356. }
  357. // Disable the "field is never assigned" compiler warning. We never assign it, but Unity does.
  358. // Note that Unity disable this warning in the generated C# projects
  359. #pragma warning disable 0649
  360. [Serializable]
  361. class SettingsJson
  362. {
  363. // ReSharper disable once InconsistentNaming
  364. public string install_location;
  365. [CanBeNull]
  366. public static string GetInstallLocationFromJson(string json)
  367. {
  368. try
  369. {
  370. #if UNITY_4_7 || UNITY_5_5
  371. return JsonConvert.DeserializeObject<SettingsJson>(json).install_location;
  372. #else
  373. return JsonUtility.FromJson<SettingsJson>(json).install_location;
  374. #endif
  375. }
  376. catch (Exception)
  377. {
  378. Logger.Warn($"Failed to get install_location from json {json}");
  379. }
  380. return null;
  381. }
  382. }
  383. [Serializable]
  384. class ToolboxHistory
  385. {
  386. public List<ItemNode> history;
  387. [CanBeNull]
  388. public static string GetLatestBuildFromJson(string json)
  389. {
  390. try
  391. {
  392. #if UNITY_4_7 || UNITY_5_5
  393. return JsonConvert.DeserializeObject<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
  394. #else
  395. return JsonUtility.FromJson<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
  396. #endif
  397. }
  398. catch (Exception)
  399. {
  400. Logger.Warn($"Failed to get latest build from json {json}");
  401. }
  402. return null;
  403. }
  404. }
  405. [Serializable]
  406. class ItemNode
  407. {
  408. public BuildNode item;
  409. }
  410. [Serializable]
  411. class BuildNode
  412. {
  413. public string build;
  414. }
  415. [Serializable]
  416. internal class ProductInfo
  417. {
  418. public string version;
  419. public string versionSuffix;
  420. [CanBeNull]
  421. internal static ProductInfo GetProductInfo(string json)
  422. {
  423. try
  424. {
  425. var productInfo = JsonUtility.FromJson<ProductInfo>(json);
  426. return productInfo;
  427. }
  428. catch (Exception)
  429. {
  430. Logger.Warn($"Failed to get version from json {json}");
  431. }
  432. return null;
  433. }
  434. }
  435. // ReSharper disable once ClassNeverInstantiated.Global
  436. [Serializable]
  437. class ToolboxInstallData
  438. {
  439. // ReSharper disable once InconsistentNaming
  440. public ActiveApplication active_application;
  441. [CanBeNull]
  442. public static string GetLatestBuildFromJson(string json)
  443. {
  444. try
  445. {
  446. #if UNITY_4_7 || UNITY_5_5
  447. var toolbox = JsonConvert.DeserializeObject<ToolboxInstallData>(json);
  448. #else
  449. var toolbox = JsonUtility.FromJson<ToolboxInstallData>(json);
  450. #endif
  451. var builds = toolbox.active_application.builds;
  452. if (builds != null && builds.Any())
  453. return builds.First();
  454. }
  455. catch (Exception)
  456. {
  457. Logger.Warn($"Failed to get latest build from json {json}");
  458. }
  459. return null;
  460. }
  461. }
  462. [Serializable]
  463. class ActiveApplication
  464. {
  465. // ReSharper disable once InconsistentNaming
  466. public List<string> builds;
  467. }
  468. #pragma warning restore 0649
  469. internal struct RiderInfo
  470. {
  471. public bool IsToolbox;
  472. public string Presentation;
  473. public Version BuildNumber;
  474. public ProductInfo ProductInfo;
  475. public string Path;
  476. public RiderInfo(string path, bool isToolbox)
  477. {
  478. if (path == RiderScriptEditor.CurrentEditor)
  479. {
  480. RiderScriptEditorData.instance.Init();
  481. BuildNumber = RiderScriptEditorData.instance.editorBuildNumber.ToVersion();
  482. ProductInfo = RiderScriptEditorData.instance.productInfo;
  483. }
  484. else
  485. {
  486. BuildNumber = GetBuildNumber(path);
  487. ProductInfo = GetBuildVersion(path);
  488. }
  489. Path = new FileInfo(path).FullName; // normalize separators
  490. var presentation = $"Rider {BuildNumber}";
  491. if (ProductInfo != null && !string.IsNullOrEmpty(ProductInfo.version))
  492. {
  493. var suffix = string.IsNullOrEmpty(ProductInfo.versionSuffix) ? "" : $" {ProductInfo.versionSuffix}";
  494. presentation = $"Rider {ProductInfo.version}{suffix}";
  495. }
  496. if (isToolbox)
  497. presentation += " (JetBrains Toolbox)";
  498. Presentation = presentation;
  499. IsToolbox = isToolbox;
  500. }
  501. }
  502. private static class Logger
  503. {
  504. internal static void Warn(string message, Exception e = null)
  505. {
  506. #if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider
  507. Log.GetLog(typeof(RiderPathLocator).Name).Warn(message);
  508. if (e != null)
  509. Log.GetLog(typeof(RiderPathLocator).Name).Warn(e);
  510. #else
  511. Debug.LogError(message);
  512. if (e != null)
  513. Debug.LogException(e);
  514. #endif
  515. }
  516. }
  517. }
  518. }