Space Ad Mobile 9.5.1
Space Ad Mobile makes ads easy to add into your mobile game: one unified workflow for LevelPlay, AdMob, Yandex.Ads across banners, interstitials and rewarded.
Loading...
Searching...
No Matches
YandexSdkUpdaterLauncher.cs
1using System;
2using System.Diagnostics;
3using System.IO;
4using System.Linq;
5using System.Threading.Tasks;
6#if UNITY_EDITOR_LINUX
7using System.Collections.Generic;
8#endif
9#if UNITY_EDITOR_LINUX
10using System.Threading;
11#endif
12using UnityEditor;
13using UnityEngine;
14using Debug = UnityEngine.Debug;
15
16namespace SCG.SpaceAdMobile
17{
21 internal static class YandexSdkUpdaterLauncher
22 {
23 #region Constants
24
25 private const string Python3Command = "python3";
26 private const string PythonCommand = "python";
27
29 internal const string MinimumPythonVersion = "3.7";
30 private const int MinimumPythonMajorVersion = 3;
31 private const int MinimumPythonMinorVersion = 7;
32
33#if UNITY_EDITOR_WIN
34 private const string WindowsPythonLauncherCommand = "py";
35 private const string Python3PrefixArguments = "-3";
36#endif
37
38 private const string UpdaterScriptFileName = "update_yandex_sdk.py";
39#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
40 private const string UpdaterBridgeDirectoryName = "YandexSdkUpdater";
41 private const string ControllerStartupAttemptEnvironmentVariable = "YANDEX_SDK_UPDATER_STARTUP_ATTEMPT";
42 private const string ControllerStartupStartedExtension = ".started";
43 private const string ControllerStartupCancelledExtension = ".cancelled";
44#endif
45#if UNITY_EDITOR_LINUX
46 private const int ControllerStartupTimeoutMilliseconds = 5000;
47 private const int ControllerStartupPollIntervalMilliseconds = 100;
48#endif
49#if UNITY_EDITOR_OSX
50 private const int MacControllerStartupExitGraceMilliseconds = 30000;
51#endif
52 private const int InterpreterProbeTimeoutMilliseconds = 2000;
53#if UNITY_EDITOR_LINUX
54 private const string LinuxShellPath = "/bin/sh";
55#endif
56
57 #endregion
58
59 #region State
60
61 private static string s_pythonExecutable;
62 private static string s_pythonPrefixArguments;
63 private static Task<(string Executable, string PrefixArguments)> s_pythonAvailabilityTask;
64 private static Action s_pythonAvailabilityCompleted;
65
66#if UNITY_EDITOR_OSX
67 private static Process s_macTerminalProcess;
68 private static string s_macStartupAttemptPath;
69 private static Action<bool, string> s_macStartupCompleted;
70 private static DateTime? s_macTerminalExitTime;
71 private static string s_macStartupError;
72#endif
73
74#if UNITY_EDITOR_LINUX
75 private static readonly (string Executable, string ArgumentPrefix)[] s_linuxTerminalCandidates =
76 {
77 ("x-terminal-emulator", "-e"),
78 ("gnome-terminal", "--"),
79 ("konsole", "-e"),
80 ("xterm", "-e")
81 };
82#endif
83
84 #endregion
85
86 #region Availability
87
91 internal static bool IsPythonAvailable => IsPythonAvailabilityChecked && !string.IsNullOrEmpty(s_pythonExecutable);
92
96 internal static bool IsPythonAvailabilityChecked { get; private set; }
97
102 internal static void RefreshPythonAvailability(Action onCompleted)
103 {
104 if (s_pythonAvailabilityTask != null)
105 {
106 s_pythonAvailabilityCompleted = onCompleted;
107 return;
108 }
109
110 IsPythonAvailabilityChecked = false;
111 s_pythonExecutable = null;
112 s_pythonPrefixArguments = null;
113 s_pythonAvailabilityCompleted = onCompleted;
114 s_pythonAvailabilityTask = Task.Run(DetectPython3);
115 EditorApplication.update -= CompletePythonAvailabilityCheck;
116 EditorApplication.update += CompletePythonAvailabilityCheck;
117 }
118
125 internal static bool TryLaunch(Action<bool, string> onCompleted, out string error)
126 {
127 error = null;
128
129#if UNITY_EDITOR_OSX
130 if (!string.IsNullOrEmpty(s_macStartupAttemptPath))
131 {
132 error = "The updater is already waiting for Terminal to start.";
133 return false;
134 }
135#endif
136
137 if (!IsPythonAvailabilityChecked)
138 {
139 error = "Python availability is still being checked.";
140 return false;
141 }
142
143 if (string.IsNullOrEmpty(s_pythonExecutable))
144 {
145 error = $"Python {MinimumPythonVersion} or newer was not found. Add python, python3, or the Windows Python Launcher to PATH.";
146 return false;
147 }
148
149 var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
150 var scriptPath = Path.GetFullPath(Path.Combine(Application.dataPath, "YandexMobileAds", UpdaterScriptFileName));
151 if (!File.Exists(scriptPath))
152 {
153 error = $"The Yandex SDK updater script was not found: {scriptPath}";
154 return false;
155 }
156
157 try
158 {
159#if UNITY_EDITOR_WIN
160 var started = TryStartTerminal(projectRoot, scriptPath, out error);
161 if (started)
162 onCompleted?.Invoke(true, null);
163
164 return started;
165#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
166 return TryStartShellTerminal(projectRoot, scriptPath, onCompleted, out error);
167#else
168 error = "Opening an interactive terminal is not supported on this Editor platform.";
169 return false;
170#endif
171 }
172 catch (Exception exception)
173 {
174 error = $"Unable to start the Yandex SDK updater: {exception.Message}";
175 return false;
176 }
177 }
178
179 #endregion
180
181 #region Interpreter Detection
182
187 private static (string Executable, string PrefixArguments) DetectPython3()
188 {
189 if (TryVerifyPython3(Python3Command, string.Empty))
190 return (Python3Command, string.Empty);
191
192 if (TryVerifyPython3(PythonCommand, string.Empty))
193 return (PythonCommand, string.Empty);
194
195#if UNITY_EDITOR_WIN
196 return TryVerifyPython3(WindowsPythonLauncherCommand, Python3PrefixArguments)
197 ? (WindowsPythonLauncherCommand, Python3PrefixArguments)
198 : (null, null);
199#else
200 return (null, null);
201#endif
202 }
203
207 private static void CompletePythonAvailabilityCheck()
208 {
209 if (s_pythonAvailabilityTask is not { IsCompleted: true })
210 return;
211
212 EditorApplication.update -= CompletePythonAvailabilityCheck;
213
214 try
215 {
216 var (pythonExecutable, pythonPrefixArguments)= s_pythonAvailabilityTask.GetAwaiter().GetResult();
217 s_pythonExecutable = pythonExecutable;
218 s_pythonPrefixArguments = pythonPrefixArguments;
219 }
220 catch (Exception exception)
221 {
222 Debug.LogException(exception);
223 s_pythonExecutable = null;
224 s_pythonPrefixArguments = null;
225 }
226
227 s_pythonAvailabilityTask = null;
228 IsPythonAvailabilityChecked = true;
229 var onCompleted = s_pythonAvailabilityCompleted;
230 s_pythonAvailabilityCompleted = null;
231 onCompleted?.Invoke();
232 }
233
240 private static bool TryVerifyPython3(string executable, string prefixArguments)
241 {
242 try
243 {
244 var arguments = string.IsNullOrEmpty(prefixArguments)
245 ? "--version"
246 : $"{prefixArguments} --version";
247 var startInfo = new ProcessStartInfo
248 {
249 FileName = executable,
250 Arguments = arguments,
251 CreateNoWindow = true,
252 UseShellExecute = false,
253 RedirectStandardOutput = true,
254 RedirectStandardError = true
255 };
256
257 using var process = Process.Start(startInfo);
258 if (process == null)
259 return false;
260
261 if (!process.WaitForExit(InterpreterProbeTimeoutMilliseconds))
262 {
263 process.Kill();
264 process.WaitForExit();
265 return false;
266 }
267
268 var version = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd();
269 return process.ExitCode == 0 &&
270 (from line in version.Split(new[]
271 { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
272 select line.TrimStart() into trimmedLine
273 where trimmedLine.StartsWith("Python ", StringComparison.OrdinalIgnoreCase)
274 select trimmedLine["Python ".Length..].Split('.') into versionParts
275 select versionParts.Length >= 2 && int.TryParse(versionParts[0], out var majorVersion) &&
276 int.TryParse(versionParts[1], out var minorVersion) &&
277 majorVersion == MinimumPythonMajorVersion && minorVersion >= MinimumPythonMinorVersion)
278 .FirstOrDefault();
279 }
280 catch (Exception)
281 {
282 return false;
283 }
284 }
285
286 #endregion
287
288 #region Terminal Launch
289
290#if UNITY_EDITOR_WIN
298 private static bool TryStartTerminal(string projectRoot, string scriptPath, out string error)
299 {
300 var arguments = string.IsNullOrEmpty(s_pythonPrefixArguments)
301 ? QuoteProcessArgument(scriptPath)
302 : $"{s_pythonPrefixArguments} {QuoteProcessArgument(scriptPath)}";
303 return TryStartProcess(new ProcessStartInfo
304 {
305 FileName = s_pythonExecutable,
306 Arguments = arguments,
307 WorkingDirectory = projectRoot,
308 UseShellExecute = true,
309 CreateNoWindow = false
310 }, out error);
311 }
312#endif
313
314#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
323 private static bool TryStartShellTerminal(
324 string projectRoot,
325 string scriptPath,
326 Action<bool, string> onCompleted,
327 out string error)
328 {
329#if UNITY_EDITOR_OSX
330 string startupAttemptPath = null;
331 try
332 {
333 startupAttemptPath = CreateControllerStartupAttemptPath(projectRoot);
334 var shellCommand = BuildShellCommand(projectRoot, scriptPath, startupAttemptPath);
335 var appleScript =
336 "tell application \"Terminal\"\n" +
337 "activate\n" +
338 $"do script \"{EscapeAppleScriptString(shellCommand)}\"\n" +
339 "end tell";
340 var process = Process.Start(new ProcessStartInfo
341 {
342 FileName = "/usr/bin/osascript",
343 Arguments = $"-e {QuoteProcessArgument(appleScript)}",
344 WorkingDirectory = projectRoot,
345 UseShellExecute = false,
346 CreateNoWindow = true,
347 RedirectStandardError = true
348 });
349 if (process == null)
350 return CompleteMacStartupFailure(
351 startupAttemptPath,
352 "The operating system did not start the macOS terminal launcher.",
353 onCompleted,
354 out error);
355
356 s_macTerminalProcess = process;
357 s_macStartupAttemptPath = startupAttemptPath;
358 s_macStartupCompleted = onCompleted;
359 s_macTerminalExitTime = null;
360 s_macStartupError = null;
361 EditorApplication.update -= CheckMacTerminalStartup;
362 EditorApplication.update += CheckMacTerminalStartup;
363 AssemblyReloadEvents.beforeAssemblyReload -= CancelMacStartupBeforeAssemblyReload;
364 AssemblyReloadEvents.beforeAssemblyReload += CancelMacStartupBeforeAssemblyReload;
365 error = null;
366 return true;
367 }
368 catch (Exception exception)
369 {
370 if (string.IsNullOrEmpty(startupAttemptPath))
371 {
372 error = exception.Message;
373 return false;
374 }
375
376 return CompleteMacStartupFailure(
377 startupAttemptPath,
378 exception.Message,
379 onCompleted,
380 out error);
381 }
382#elif UNITY_EDITOR_LINUX
383 var errors = new List<string>();
384 foreach (var candidate in s_linuxTerminalCandidates)
385 {
386 var startupAttemptPath = CreateControllerStartupAttemptPath(projectRoot);
387 try
388 {
389 var shellCommand = BuildShellCommand(projectRoot, scriptPath, startupAttemptPath);
390 var started = TryStartLinuxTerminal(
391 candidate.Executable,
392 candidate.ArgumentPrefix,
393 projectRoot,
394 shellCommand,
395 startupAttemptPath,
396 out var attemptRevoked,
397 out var candidateError);
398 if (started)
399 {
400 error = null;
401 onCompleted?.Invoke(true, null);
402 return true;
403 }
404
405 errors.Add($"{candidate.Executable}: {candidateError}");
406 if (!attemptRevoked)
407 {
408 error = "Unable to safely continue to another terminal because the previous startup attempt could not be revoked. " +
409 string.Join(" ", errors);
410 return false;
411 }
412 }
413 finally
414 {
415 DeleteControllerStartupAttemptFiles(startupAttemptPath);
416 }
417 }
418
419 error = "Unable to start the updater in a supported terminal. " + string.Join(" ", errors);
420 return false;
421#endif
422 }
423#endif
424
425#if UNITY_EDITOR_LINUX
437 private static bool TryStartLinuxTerminal(
438 string terminalExecutable,
439 string argumentPrefix,
440 string projectRoot,
441 string shellCommand,
442 string startupAttemptPath,
443 out bool attemptRevoked,
444 out string error)
445 {
446 var arguments = $"{argumentPrefix} {LinuxShellPath} -c {QuoteProcessArgument(shellCommand)}";
447 return TryStartProcessAndConfirmController(new ProcessStartInfo
448 {
449 FileName = terminalExecutable,
450 Arguments = arguments,
451 WorkingDirectory = projectRoot,
452 UseShellExecute = false,
453 CreateNoWindow = false
454 }, startupAttemptPath, out attemptRevoked, out error);
455 }
456#endif
457
458#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
464 private static string CreateControllerStartupAttemptPath(string projectRoot)
465 {
466 var directory = Path.Combine(projectRoot, "Library", UpdaterBridgeDirectoryName);
467 Directory.CreateDirectory(directory);
468 var attemptPath = Path.Combine(directory, $"launcher-{Guid.NewGuid():N}.pending");
469 using (File.Create(attemptPath))
470 {
471 }
472
473 return attemptPath;
474 }
475
480 private static void DeleteControllerStartupAttemptFiles(string startupAttemptPath)
481 {
482 DeleteTemporaryStartupFile(startupAttemptPath);
483 DeleteTemporaryStartupFile(Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension));
484 DeleteTemporaryStartupFile(Path.ChangeExtension(startupAttemptPath, ControllerStartupCancelledExtension));
485 }
486
491 private static void DeleteTemporaryStartupFile(string path)
492 {
493 try
494 {
495 File.Delete(path);
496 }
497 catch (IOException exception)
498 {
499 Debug.LogWarning($"Could not remove updater startup file '{path}': {exception.Message}");
500 }
501 catch (UnauthorizedAccessException exception)
502 {
503 Debug.LogWarning($"Could not remove updater startup file '{path}': {exception.Message}");
504 }
505 }
506
514 private static bool TryResolveControllerStartupAttempt(
515 string startupAttemptPath,
516 out bool controllerStarted,
517 out string error)
518 {
519 var startedPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension);
520 var cancelledPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupCancelledExtension);
521
522 if (File.Exists(startedPath))
523 {
524 controllerStarted = true;
525 error = null;
526 return true;
527 }
528
529 if (File.Exists(cancelledPath))
530 {
531 controllerStarted = false;
532 error = null;
533 return true;
534 }
535
536 try
537 {
538 File.Move(startupAttemptPath, cancelledPath);
539 controllerStarted = false;
540 error = null;
541 return true;
542 }
543 catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
544 {
545 controllerStarted = File.Exists(startedPath);
546 if (controllerStarted || File.Exists(cancelledPath))
547 {
548 error = null;
549 return true;
550 }
551
552 error = exception.Message;
553 return false;
554 }
555 }
556
557#if UNITY_EDITOR_LINUX
566 private static bool TryStartProcessAndConfirmController(
567 ProcessStartInfo startInfo,
568 string startupAttemptPath,
569 out bool attemptRevoked,
570 out string error)
571 {
572 try
573 {
574 using (var process = Process.Start(startInfo))
575 {
576 if (process == null)
577 {
578 return ResolveFailedControllerStartupAttempt(
579 startupAttemptPath,
580 "The operating system did not start the terminal process.",
581 out attemptRevoked,
582 out error);
583 }
584
585 var timeout = DateTime.UtcNow.AddMilliseconds(ControllerStartupTimeoutMilliseconds);
586 var startedPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension);
587 while (DateTime.UtcNow < timeout)
588 {
589 if (File.Exists(startedPath))
590 {
591 error = null;
592 attemptRevoked = false;
593 return true;
594 }
595
596 if (process.HasExited && process.ExitCode != 0)
597 {
598 var details = startInfo.RedirectStandardError
599 ? process.StandardError.ReadToEnd().Trim()
600 : string.Empty;
601 return ResolveFailedControllerStartupAttempt(
602 startupAttemptPath,
603 $"The terminal launcher exited with code {process.ExitCode}." +
604 (string.IsNullOrEmpty(details) ? string.Empty : $" {details}"),
605 out attemptRevoked,
606 out error);
607 }
608
609 Thread.Sleep(ControllerStartupPollIntervalMilliseconds);
610 }
611 }
612
613 return ResolveFailedControllerStartupAttempt(
614 startupAttemptPath,
615 $"The terminal did not confirm that the updater controller started within {ControllerStartupTimeoutMilliseconds / 1000} seconds.",
616 out attemptRevoked,
617 out error);
618 }
619 catch (Exception exception)
620 {
621 return ResolveFailedControllerStartupAttempt(
622 startupAttemptPath,
623 exception.Message,
624 out attemptRevoked,
625 out error);
626 }
627 }
628#endif
629
638 private static bool ResolveFailedControllerStartupAttempt(
639 string startupAttemptPath,
640 string failure,
641 out bool attemptRevoked,
642 out string error)
643 {
644 if (!TryResolveControllerStartupAttempt(startupAttemptPath, out var controllerStarted, out var revokeError))
645 {
646 error = $"{failure} The pending startup attempt could not be revoked: {revokeError}";
647 attemptRevoked = false;
648 return false;
649 }
650
651 if (controllerStarted)
652 {
653 error = null;
654 attemptRevoked = false;
655 return true;
656 }
657
658 error = failure;
659 attemptRevoked = true;
660 return false;
661 }
662#endif
663
664#if UNITY_EDITOR_OSX
668 internal static void CancelPendingLaunch()
669 {
670 if (!TryCancelMacStartup(out var error) && !string.IsNullOrEmpty(error))
671 Debug.LogError($"Unable to cancel the pending Yandex updater launch: {error}");
672 }
673
674 private static void CancelMacStartupBeforeAssemblyReload()
675 {
676 if (!TryCancelMacStartup(out var error) && !string.IsNullOrEmpty(error))
677 Debug.LogError($"Unable to cancel the pending Yandex updater launch before assembly reload: {error}");
678 }
679
680 private static bool TryCancelMacStartup(out string error)
681 {
682 var startupAttemptPath = s_macStartupAttemptPath;
683 if (string.IsNullOrEmpty(startupAttemptPath))
684 {
685 error = null;
686 return true;
687 }
688
689 if (!TryResolveControllerStartupAttempt(startupAttemptPath, out var controllerStarted, out error))
690 return false;
691
692 EditorApplication.update -= CheckMacTerminalStartup;
693 AssemblyReloadEvents.beforeAssemblyReload -= CancelMacStartupBeforeAssemblyReload;
694 var process = s_macTerminalProcess;
695 s_macTerminalProcess = null;
696 s_macStartupAttemptPath = null;
697 s_macStartupCompleted = null;
698 s_macTerminalExitTime = null;
699 s_macStartupError = null;
700
701 if (!controllerStarted)
702 StopMacTerminalProcess(process);
703
704 process?.Dispose();
705 DeleteControllerStartupAttemptFiles(startupAttemptPath);
706 error = null;
707 return true;
708 }
709
710 private static void StopMacTerminalProcess(Process process)
711 {
712 if (process == null)
713 return;
714
715 try
716 {
717 if (!process.HasExited)
718 process.Kill();
719 }
720 catch (Exception exception)
721 {
722 Debug.LogWarning($"Could not stop the macOS terminal launcher: {exception.Message}");
723 }
724 }
725
734 private static bool CompleteMacStartupFailure(
735 string startupAttemptPath,
736 string failure,
737 Action<bool, string> onCompleted,
738 out string error)
739 {
740 var started = ResolveFailedControllerStartupAttempt(
741 startupAttemptPath,
742 failure,
743 out var attemptRevoked,
744 out error);
745 if (started)
746 {
747 DeleteControllerStartupAttemptFiles(startupAttemptPath);
748 onCompleted?.Invoke(true, null);
749 return true;
750 }
751
752 if (attemptRevoked)
753 DeleteControllerStartupAttemptFiles(startupAttemptPath);
754
755 return false;
756 }
757
761 private static void CheckMacTerminalStartup()
762 {
763 var process = s_macTerminalProcess;
764 var startupAttemptPath = s_macStartupAttemptPath;
765 if (process == null || string.IsNullOrEmpty(startupAttemptPath))
766 {
767 EditorApplication.update -= CheckMacTerminalStartup;
768 return;
769 }
770
771 try
772 {
773 var startedPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension);
774 if (File.Exists(startedPath))
775 {
776 CompleteMacTerminalStartup(true, null);
777 return;
778 }
779
780 if (!process.HasExited)
781 return;
782
783 if (s_macTerminalExitTime == null)
784 {
785 s_macTerminalExitTime = DateTime.UtcNow;
786 if (process.ExitCode != 0)
787 {
788 var details = process.StandardError.ReadToEnd().Trim();
789 s_macStartupError = $"The terminal launcher exited with code {process.ExitCode}." +
790 (string.IsNullOrEmpty(details) ? string.Empty : $" {details}");
791 }
792 }
793
794 if (s_macStartupError == null &&
795 DateTime.UtcNow - s_macTerminalExitTime.Value <
796 TimeSpan.FromMilliseconds(MacControllerStartupExitGraceMilliseconds))
797 {
798 return;
799 }
800
801 s_macStartupError ??= "The terminal launcher exited before the updater controller started.";
802 var controllerStarted = ResolveFailedControllerStartupAttempt(
803 startupAttemptPath,
804 s_macStartupError,
805 out var attemptRevoked,
806 out var error);
807 if (controllerStarted)
808 CompleteMacTerminalStartup(true, null);
809 else if (attemptRevoked)
810 CompleteMacTerminalStartup(false, error);
811 }
812 catch (Exception exception)
813 {
814 var controllerStarted = ResolveFailedControllerStartupAttempt(
815 startupAttemptPath,
816 exception.Message,
817 out var attemptRevoked,
818 out var error);
819 if (controllerStarted)
820 CompleteMacTerminalStartup(true, null);
821 else if (attemptRevoked)
822 CompleteMacTerminalStartup(false, error);
823 }
824 }
825
829 private static void CompleteMacTerminalStartup(bool started, string error)
830 {
831 EditorApplication.update -= CheckMacTerminalStartup;
832 AssemblyReloadEvents.beforeAssemblyReload -= CancelMacStartupBeforeAssemblyReload;
833 var startupAttemptPath = s_macStartupAttemptPath;
834 var process = s_macTerminalProcess;
835 var onCompleted = s_macStartupCompleted;
836 s_macStartupAttemptPath = null;
837 s_macTerminalProcess = null;
838 s_macStartupCompleted = null;
839 s_macTerminalExitTime = null;
840 s_macStartupError = null;
841 process?.Dispose();
842
843 if (!string.IsNullOrEmpty(startupAttemptPath))
844 DeleteControllerStartupAttemptFiles(startupAttemptPath);
845
846 onCompleted?.Invoke(started, error);
847 }
848#endif
849
850#if UNITY_EDITOR_WIN
857 private static bool TryStartProcess(ProcessStartInfo startInfo, out string error)
858 {
859 try
860 {
861 using (var process = Process.Start(startInfo))
862 {
863 if (process == null)
864 {
865 error = "The operating system did not start the updater process.";
866 return false;
867 }
868 }
869
870 error = null;
871 return true;
872 }
873 catch (Exception exception)
874 {
875 error = exception.Message;
876 return false;
877 }
878 }
879#endif
880
881#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
889 private static string BuildShellCommand(string projectRoot, string scriptPath, string startupAttemptPath)
890 {
891 var interpreter = QuoteShellArgument(s_pythonExecutable);
892 var prefixArguments = string.IsNullOrEmpty(s_pythonPrefixArguments)
893 ? string.Empty
894 : $"{s_pythonPrefixArguments} ";
895 return $"cd {QuoteShellArgument(projectRoot)} && " +
896 $"export {ControllerStartupAttemptEnvironmentVariable}={QuoteShellArgument(startupAttemptPath)} && " +
897 $"exec {interpreter} {prefixArguments}{QuoteShellArgument(scriptPath)}";
898 }
899
905 private static string QuoteShellArgument(string value) =>
906 $"'{value.Replace("'", "'\\''")}'";
907#endif
908
914 private static string QuoteProcessArgument(string value) =>
915 $"\"{value.Replace("\\", @"\\").Replace("\"", "\\\"")}\"";
916
917#if UNITY_EDITOR_OSX
923 private static string EscapeAppleScriptString(string value) =>
924 value.Replace("\\", "\\\\").Replace("\"", "\\\"");
925#endif
926
927 #endregion
928 }
929}