2using System.Diagnostics;
5using System.Threading.Tasks;
7using System.Collections.Generic;
10using System.Threading;
14using Debug = UnityEngine.Debug;
21 internal static class YandexSdkUpdaterLauncher
25 private const string Python3Command =
"python3";
26 private const string PythonCommand =
"python";
29 internal const string MinimumPythonVersion =
"3.7";
30 private const int MinimumPythonMajorVersion = 3;
31 private const int MinimumPythonMinorVersion = 7;
34 private const string WindowsPythonLauncherCommand =
"py";
35 private const string Python3PrefixArguments =
"-3";
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";
46 private const int ControllerStartupTimeoutMilliseconds = 5000;
47 private const int ControllerStartupPollIntervalMilliseconds = 100;
50 private const int MacControllerStartupExitGraceMilliseconds = 30000;
52 private const int InterpreterProbeTimeoutMilliseconds = 2000;
54 private const string LinuxShellPath =
"/bin/sh";
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;
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;
75 private static readonly (
string Executable,
string ArgumentPrefix)[] s_linuxTerminalCandidates =
77 (
"x-terminal-emulator",
"-e"),
78 (
"gnome-terminal",
"--"),
91 internal static bool IsPythonAvailable => IsPythonAvailabilityChecked && !
string.IsNullOrEmpty(s_pythonExecutable);
96 internal static bool IsPythonAvailabilityChecked {
get;
private set; }
102 internal static void RefreshPythonAvailability(Action onCompleted)
104 if (s_pythonAvailabilityTask !=
null)
106 s_pythonAvailabilityCompleted = onCompleted;
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;
125 internal static bool TryLaunch(Action<bool, string> onCompleted, out
string error)
130 if (!
string.IsNullOrEmpty(s_macStartupAttemptPath))
132 error =
"The updater is already waiting for Terminal to start.";
137 if (!IsPythonAvailabilityChecked)
139 error =
"Python availability is still being checked.";
143 if (
string.IsNullOrEmpty(s_pythonExecutable))
145 error = $
"Python {MinimumPythonVersion} or newer was not found. Add python, python3, or the Windows Python Launcher to PATH.";
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))
153 error = $
"The Yandex SDK updater script was not found: {scriptPath}";
160 var started = TryStartTerminal(projectRoot, scriptPath, out error);
162 onCompleted?.Invoke(
true,
null);
165#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
166 return TryStartShellTerminal(projectRoot, scriptPath, onCompleted, out error);
168 error =
"Opening an interactive terminal is not supported on this Editor platform.";
172 catch (Exception exception)
174 error = $
"Unable to start the Yandex SDK updater: {exception.Message}";
181 #region Interpreter Detection
187 private static (
string Executable,
string PrefixArguments) DetectPython3()
189 if (TryVerifyPython3(Python3Command,
string.Empty))
190 return (Python3Command,
string.Empty);
192 if (TryVerifyPython3(PythonCommand,
string.Empty))
193 return (PythonCommand,
string.Empty);
196 return TryVerifyPython3(WindowsPythonLauncherCommand, Python3PrefixArguments)
197 ? (WindowsPythonLauncherCommand, Python3PrefixArguments)
207 private static void CompletePythonAvailabilityCheck()
209 if (s_pythonAvailabilityTask is not { IsCompleted:
true })
212 EditorApplication.update -= CompletePythonAvailabilityCheck;
216 var (pythonExecutable, pythonPrefixArguments)= s_pythonAvailabilityTask.GetAwaiter().GetResult();
217 s_pythonExecutable = pythonExecutable;
218 s_pythonPrefixArguments = pythonPrefixArguments;
220 catch (Exception exception)
222 Debug.LogException(exception);
223 s_pythonExecutable =
null;
224 s_pythonPrefixArguments =
null;
227 s_pythonAvailabilityTask =
null;
228 IsPythonAvailabilityChecked =
true;
229 var onCompleted = s_pythonAvailabilityCompleted;
230 s_pythonAvailabilityCompleted =
null;
231 onCompleted?.Invoke();
240 private static bool TryVerifyPython3(
string executable,
string prefixArguments)
244 var arguments =
string.IsNullOrEmpty(prefixArguments)
246 : $
"{prefixArguments} --version";
247 var startInfo =
new ProcessStartInfo
249 FileName = executable,
250 Arguments = arguments,
251 CreateNoWindow =
true,
252 UseShellExecute =
false,
253 RedirectStandardOutput =
true,
254 RedirectStandardError =
true
257 using var process = Process.Start(startInfo);
261 if (!process.WaitForExit(InterpreterProbeTimeoutMilliseconds))
264 process.WaitForExit();
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)
288 #region Terminal Launch
298 private static bool TryStartTerminal(
string projectRoot,
string scriptPath, out
string error)
300 var arguments =
string.IsNullOrEmpty(s_pythonPrefixArguments)
301 ? QuoteProcessArgument(scriptPath)
302 : $
"{s_pythonPrefixArguments} {QuoteProcessArgument(scriptPath)}";
303 return TryStartProcess(
new ProcessStartInfo
305 FileName = s_pythonExecutable,
306 Arguments = arguments,
307 WorkingDirectory = projectRoot,
308 UseShellExecute =
true,
309 CreateNoWindow =
false
314#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
323 private static bool TryStartShellTerminal(
326 Action<bool, string> onCompleted,
330 string startupAttemptPath =
null;
333 startupAttemptPath = CreateControllerStartupAttemptPath(projectRoot);
334 var shellCommand = BuildShellCommand(projectRoot, scriptPath, startupAttemptPath);
336 "tell application \"Terminal\"\n" +
338 $
"do script \"{EscapeAppleScriptString(shellCommand)}\"\n" +
340 var process = Process.Start(
new ProcessStartInfo
342 FileName =
"/usr/bin/osascript",
343 Arguments = $
"-e {QuoteProcessArgument(appleScript)}",
344 WorkingDirectory = projectRoot,
345 UseShellExecute =
false,
346 CreateNoWindow =
true,
347 RedirectStandardError =
true
350 return CompleteMacStartupFailure(
352 "The operating system did not start the macOS terminal launcher.",
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;
368 catch (Exception exception)
370 if (
string.IsNullOrEmpty(startupAttemptPath))
372 error = exception.Message;
376 return CompleteMacStartupFailure(
382#elif UNITY_EDITOR_LINUX
383 var errors =
new List<string>();
384 foreach (var candidate
in s_linuxTerminalCandidates)
386 var startupAttemptPath = CreateControllerStartupAttemptPath(projectRoot);
389 var shellCommand = BuildShellCommand(projectRoot, scriptPath, startupAttemptPath);
390 var started = TryStartLinuxTerminal(
391 candidate.Executable,
392 candidate.ArgumentPrefix,
396 out var attemptRevoked,
397 out var candidateError);
401 onCompleted?.Invoke(
true,
null);
405 errors.Add($
"{candidate.Executable}: {candidateError}");
408 error =
"Unable to safely continue to another terminal because the previous startup attempt could not be revoked. " +
409 string.Join(
" ", errors);
415 DeleteControllerStartupAttemptFiles(startupAttemptPath);
419 error =
"Unable to start the updater in a supported terminal. " +
string.Join(
" ", errors);
425#if UNITY_EDITOR_LINUX
437 private static bool TryStartLinuxTerminal(
438 string terminalExecutable,
439 string argumentPrefix,
442 string startupAttemptPath,
443 out
bool attemptRevoked,
446 var arguments = $
"{argumentPrefix} {LinuxShellPath} -c {QuoteProcessArgument(shellCommand)}";
447 return TryStartProcessAndConfirmController(
new ProcessStartInfo
449 FileName = terminalExecutable,
450 Arguments = arguments,
451 WorkingDirectory = projectRoot,
452 UseShellExecute =
false,
453 CreateNoWindow =
false
454 }, startupAttemptPath, out attemptRevoked, out error);
458#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
464 private static string CreateControllerStartupAttemptPath(
string projectRoot)
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))
480 private static void DeleteControllerStartupAttemptFiles(
string startupAttemptPath)
482 DeleteTemporaryStartupFile(startupAttemptPath);
483 DeleteTemporaryStartupFile(Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension));
484 DeleteTemporaryStartupFile(Path.ChangeExtension(startupAttemptPath, ControllerStartupCancelledExtension));
491 private static void DeleteTemporaryStartupFile(
string path)
497 catch (IOException exception)
499 Debug.LogWarning($
"Could not remove updater startup file '{path}': {exception.Message}");
501 catch (UnauthorizedAccessException exception)
503 Debug.LogWarning($
"Could not remove updater startup file '{path}': {exception.Message}");
514 private static bool TryResolveControllerStartupAttempt(
515 string startupAttemptPath,
516 out
bool controllerStarted,
519 var startedPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension);
520 var cancelledPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupCancelledExtension);
522 if (File.Exists(startedPath))
524 controllerStarted =
true;
529 if (File.Exists(cancelledPath))
531 controllerStarted =
false;
538 File.Move(startupAttemptPath, cancelledPath);
539 controllerStarted =
false;
543 catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
545 controllerStarted = File.Exists(startedPath);
546 if (controllerStarted || File.Exists(cancelledPath))
552 error = exception.Message;
557#if UNITY_EDITOR_LINUX
566 private static bool TryStartProcessAndConfirmController(
567 ProcessStartInfo startInfo,
568 string startupAttemptPath,
569 out
bool attemptRevoked,
574 using (var process = Process.Start(startInfo))
578 return ResolveFailedControllerStartupAttempt(
580 "The operating system did not start the terminal process.",
585 var timeout = DateTime.UtcNow.AddMilliseconds(ControllerStartupTimeoutMilliseconds);
586 var startedPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension);
587 while (DateTime.UtcNow < timeout)
589 if (File.Exists(startedPath))
592 attemptRevoked =
false;
596 if (process.HasExited && process.ExitCode != 0)
598 var details = startInfo.RedirectStandardError
599 ? process.StandardError.ReadToEnd().Trim()
601 return ResolveFailedControllerStartupAttempt(
603 $
"The terminal launcher exited with code {process.ExitCode}." +
604 (
string.IsNullOrEmpty(details) ?
string.Empty : $
" {details}"),
609 Thread.Sleep(ControllerStartupPollIntervalMilliseconds);
613 return ResolveFailedControllerStartupAttempt(
615 $
"The terminal did not confirm that the updater controller started within {ControllerStartupTimeoutMilliseconds / 1000} seconds.",
619 catch (Exception exception)
621 return ResolveFailedControllerStartupAttempt(
638 private static bool ResolveFailedControllerStartupAttempt(
639 string startupAttemptPath,
641 out
bool attemptRevoked,
644 if (!TryResolveControllerStartupAttempt(startupAttemptPath, out var controllerStarted, out var revokeError))
646 error = $
"{failure} The pending startup attempt could not be revoked: {revokeError}";
647 attemptRevoked =
false;
651 if (controllerStarted)
654 attemptRevoked =
false;
659 attemptRevoked =
true;
668 internal static void CancelPendingLaunch()
670 if (!TryCancelMacStartup(out var error) && !
string.IsNullOrEmpty(error))
671 Debug.LogError($
"Unable to cancel the pending Yandex updater launch: {error}");
674 private static void CancelMacStartupBeforeAssemblyReload()
676 if (!TryCancelMacStartup(out var error) && !
string.IsNullOrEmpty(error))
677 Debug.LogError($
"Unable to cancel the pending Yandex updater launch before assembly reload: {error}");
680 private static bool TryCancelMacStartup(out
string error)
682 var startupAttemptPath = s_macStartupAttemptPath;
683 if (
string.IsNullOrEmpty(startupAttemptPath))
689 if (!TryResolveControllerStartupAttempt(startupAttemptPath, out var controllerStarted, out error))
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;
701 if (!controllerStarted)
702 StopMacTerminalProcess(process);
705 DeleteControllerStartupAttemptFiles(startupAttemptPath);
710 private static void StopMacTerminalProcess(Process process)
717 if (!process.HasExited)
720 catch (Exception exception)
722 Debug.LogWarning($
"Could not stop the macOS terminal launcher: {exception.Message}");
734 private static bool CompleteMacStartupFailure(
735 string startupAttemptPath,
737 Action<bool, string> onCompleted,
740 var started = ResolveFailedControllerStartupAttempt(
743 out var attemptRevoked,
747 DeleteControllerStartupAttemptFiles(startupAttemptPath);
748 onCompleted?.Invoke(
true,
null);
753 DeleteControllerStartupAttemptFiles(startupAttemptPath);
761 private static void CheckMacTerminalStartup()
763 var process = s_macTerminalProcess;
764 var startupAttemptPath = s_macStartupAttemptPath;
765 if (process ==
null ||
string.IsNullOrEmpty(startupAttemptPath))
767 EditorApplication.update -= CheckMacTerminalStartup;
773 var startedPath = Path.ChangeExtension(startupAttemptPath, ControllerStartupStartedExtension);
774 if (File.Exists(startedPath))
776 CompleteMacTerminalStartup(
true,
null);
780 if (!process.HasExited)
783 if (s_macTerminalExitTime ==
null)
785 s_macTerminalExitTime = DateTime.UtcNow;
786 if (process.ExitCode != 0)
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}");
794 if (s_macStartupError ==
null &&
795 DateTime.UtcNow - s_macTerminalExitTime.Value <
796 TimeSpan.FromMilliseconds(MacControllerStartupExitGraceMilliseconds))
801 s_macStartupError ??=
"The terminal launcher exited before the updater controller started.";
802 var controllerStarted = ResolveFailedControllerStartupAttempt(
805 out var attemptRevoked,
807 if (controllerStarted)
808 CompleteMacTerminalStartup(
true,
null);
809 else if (attemptRevoked)
810 CompleteMacTerminalStartup(
false, error);
812 catch (Exception exception)
814 var controllerStarted = ResolveFailedControllerStartupAttempt(
817 out var attemptRevoked,
819 if (controllerStarted)
820 CompleteMacTerminalStartup(
true,
null);
821 else if (attemptRevoked)
822 CompleteMacTerminalStartup(
false, error);
829 private static void CompleteMacTerminalStartup(
bool started,
string error)
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;
843 if (!
string.IsNullOrEmpty(startupAttemptPath))
844 DeleteControllerStartupAttemptFiles(startupAttemptPath);
846 onCompleted?.Invoke(started, error);
857 private static bool TryStartProcess(ProcessStartInfo startInfo, out
string error)
861 using (var process = Process.Start(startInfo))
865 error =
"The operating system did not start the updater process.";
873 catch (Exception exception)
875 error = exception.Message;
881#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
889 private static string BuildShellCommand(
string projectRoot,
string scriptPath,
string startupAttemptPath)
891 var interpreter = QuoteShellArgument(s_pythonExecutable);
892 var prefixArguments =
string.IsNullOrEmpty(s_pythonPrefixArguments)
894 : $
"{s_pythonPrefixArguments} ";
895 return $
"cd {QuoteShellArgument(projectRoot)} && " +
896 $
"export {ControllerStartupAttemptEnvironmentVariable}={QuoteShellArgument(startupAttemptPath)} && " +
897 $
"exec {interpreter} {prefixArguments}{QuoteShellArgument(scriptPath)}";
905 private static string QuoteShellArgument(
string value) =>
906 $
"'{value.Replace("'", "'\\
''")}'";
914 private static string QuoteProcessArgument(
string value) =>
915 $
"\"{value.Replace("\\
", @"\\
").Replace("\
"",
"\\\"")}\
"";
923 private static string EscapeAppleScriptString(
string value) =>
924 value.Replace(
"\\",
"\\\\").Replace(
"\"",
"\\\"");