Exemple #1
0
        private static string[] ConstructArgs(string projectPath, string schemaCompilerPath, string workerJsonPath)
        {
            var baseArgs = new List <string>
            {
                "run",
                "-p",
                $"\"{projectPath}\"",
                "--",
                $"--json-dir=\"{ImprobableJsonDir}\"",
                $"--schema-compiler-path=\"{schemaCompilerPath}\"",
                $"--worker-json-dir=\"{workerJsonPath}\""
            };

            var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            baseArgs.Add($"--native-output-dir=\"{toolsConfig.CodegenOutputDir}\"");

            // Add user defined schema directories
            baseArgs.AddRange(toolsConfig.SchemaSourceDirs
                              .Where(Directory.Exists)
                              .Select(directory => $"--schema-path=\"{directory}\""));

            // Add package schema directories
            baseArgs.AddRange(GetSchemaDirectories()
                              .Select(directory => $"--schema-path=\"{directory}\""));

            // Schema Descriptor
            baseArgs.Add($"--descriptor-dir=\"{toolsConfig.DescriptorOutputDir}\"");

            baseArgs.AddRange(
                toolsConfig.SerializationOverrides.Select(@override => $"--serialization-override=\"{@override}\""));

            return(baseArgs.ToArray());
        }
        private static void CopySchema()
        {
            try
            {
                var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();
                // Safe as we validate there is at least one entry.
                var schemaRoot = toolsConfig.SchemaSourceDirs[0];
                CleanDestination(schemaRoot);

                var packages = Common.GetManifestDependencies().Where(kv => kv.Value.StartsWith("file:"))
                               .ToDictionary(kv => kv.Key, RemoveFilePrefix)
                               .ToDictionary(kv => kv.Key, kv => Path.Combine("Packages", kv.Value));
                var schemaSources = packages.Where(SchemaPathExists)
                                    .ToDictionary(kv => kv.Key, GetSchemaPath);

                foreach (var source in schemaSources)
                {
                    CopySchemaFiles(schemaRoot, source);
                }
            }
            catch (Exception e)
            {
                Console.Error.WriteLine(e.Message);
                Environment.Exit(1);
            }
        }
        private static void CopySchema()
        {
            try
            {
                var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();
                // Safe as we validate there is at least one entry.
                var schemaRoot = toolsConfig.SchemaSourceDirs[0];
                CleanDestination(schemaRoot);

                // Get all packages we depend on
                var request = Client.List(offlineMode: true);
                while (!request.IsCompleted)
                {
                    // Wait for the request to complete
                }

                var schemaSources = request.Result.ToDictionary(package => package.name,
                                                                package => Path.Combine(package.resolvedPath, "Schema"))
                                    .Where(kv => Directory.Exists(kv.Value));

                foreach (var source in schemaSources)
                {
                    CopySchemaFiles(schemaRoot, source.Key, source.Value);
                }
            }
            catch (Exception e)
            {
                Console.Error.WriteLine(e.Message);
                Environment.Exit(1);
            }
        }
        private static string[] ConstructArgs(string projectPath, string schemaCompilerPath, string workerJsonPath)
        {
            var baseArgs = new List <string>
            {
                "run",
                "-p",
                $"\"{projectPath}\"",
                "--",
                $"--json-dir=\"{ImprobableJsonDir}\"",
                $"--schema-compiler-path=\"{schemaCompilerPath}\"",
                $"--worker-json-dir=\"{workerJsonPath}\""
            };

            var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            baseArgs.Add($"--native-output-dir=\"{toolsConfig.CodegenOutputDir}\"");
            baseArgs.Add($"--schema-path=\"{toolsConfig.SchemaStdLibDir}\"");

            foreach (var schemaSourceDir in toolsConfig.SchemaSourceDirs)
            {
                baseArgs.Add($"--schema-path=\"{schemaSourceDir}\"");
            }

            return(baseArgs.ToArray());
        }
Exemple #5
0
        private static GdkToolsConfiguration CreateInstance()
        {
            var config = new GdkToolsConfiguration();

            File.WriteAllText(JsonFilePath, JsonUtility.ToJson(config, true));

            return(config);
        }
        private static void ForceGenerate()
        {
            var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            if (Directory.Exists(toolsConfig.CodegenOutputDir))
            {
                Directory.Delete(toolsConfig.CodegenOutputDir, true);
            }

            Generate();
        }
        public override void OnActivate(string searchContext, VisualElement rootElement)
        {
            if (toolsConfig != null)
            {
                return;
            }

            toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            Undo.undoRedoPerformed += () => { configErrors = toolsConfig.Validate(); };
        }
        private static void Generate()
        {
            var devAuthToken              = string.Empty;
            var gdkToolsConfiguration     = GdkToolsConfiguration.GetOrCreateInstance();
            var devAuthTokenFullDir       = gdkToolsConfiguration.DevAuthTokenFullDir;
            var devAuthTokenFilePath      = gdkToolsConfiguration.DevAuthTokenFilepath;
            var devAuthTokenLifetimeHours = $"{gdkToolsConfiguration.DevAuthTokenLifetimeHours}h";

            var receivedMessage = string.Empty;

            RedirectedProcess
            .Command(Common.SpatialBinary)
            .WithArgs("project", "auth", "dev-auth-token", "create", "--description", "\"Dev Auth Token\"",
                      "--lifetime", devAuthTokenLifetimeHours, "--json_output")
            .InDirectory(Common.SpatialProjectRootDir)
            .AddOutputProcessing(message => receivedMessage = message)
            .RedirectOutputOptions(OutputRedirectBehaviour.None)
            .Run();

            try
            {
                if (Json.Deserialize(receivedMessage).TryGetValue(JsonDataKey, out var jsonData) &&
                    ((Dictionary <string, object>)jsonData).TryGetValue(TokenSecretKey, out var tokenSecret))
                {
                    devAuthToken = (string)tokenSecret;
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"Unable to generate Dev Auth Token. {e.Message}");
                return;
            }

            if (!Directory.Exists(devAuthTokenFullDir))
            {
                Directory.CreateDirectory(devAuthTokenFullDir);
            }

            try
            {
                File.WriteAllText(devAuthTokenFilePath, devAuthToken);
            }
            catch (Exception e)
            {
                Debug.LogError($"Unable to save Dev Auth Token asset. {e.Message}");
                return;
            }

            Debug.Log($"Saving token {devAuthToken} to {devAuthTokenFilePath}.");
            AssetDatabase.ImportAsset(
                Path.Combine("Assets", gdkToolsConfiguration.DevAuthTokenDir, "DevAuthToken.txt"),
                ImportAssetOptions.ForceUpdate);
            AssetDatabase.Refresh();
        }
        private void OnEnable()
        {
            if (toolsConfig != null)
            {
                return;
            }

            titleContent = new GUIContent("GDK Tools");
            toolsConfig  = GdkToolsConfiguration.GetOrCreateInstance();

            Undo.undoRedoPerformed += () => { configErrors = toolsConfig.Validate(); };
        }
        public static bool TryGenerate()
        {
            var devAuthToken          = string.Empty;
            var gdkToolsConfiguration = GdkToolsConfiguration.GetOrCreateInstance();

            var devAuthTokenLifetimeHours = $"{gdkToolsConfiguration.DevAuthTokenLifetimeHours}h";

            var receivedMessage = string.Empty;

            RedirectedProcess
            .Spatial("project", "auth", "dev-auth-token", "create")
            .WithArgs("--description", "\"Dev Auth Token\"",
                      "--lifetime", devAuthTokenLifetimeHours,
                      "--json_output")
            .InDirectory(Common.SpatialProjectRootDir)
            .AddOutputProcessing(message => receivedMessage = message)
            .RedirectOutputOptions(OutputRedirectBehaviour.None)
            .Run();

            try
            {
                var deserializedMessage = Json.Deserialize(receivedMessage);
                if (deserializedMessage.TryGetValue(JsonDataKey, out var jsonData) &&
                    ((Dictionary <string, object>)jsonData).TryGetValue(JsonTokenSecretKey, out var tokenSecret))
                {
                    devAuthToken = (string)tokenSecret;
                }
                else
                {
                    if (deserializedMessage.TryGetValue(JsonErrorKey, out var errorMessage))
                    {
                        throw new Exception(errorMessage.ToString());
                    }

                    throw new Exception(string.Empty);
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"Unable to generate Dev Auth Token. {e.Message}");
                return(false);
            }

            Debug.Log($"Saving token to Player Preferences.");
            PlayerPrefs.SetString(PlayerPrefDevAuthTokenKey, devAuthToken);

            if (gdkToolsConfiguration.SaveDevAuthTokenToFile)
            {
                return(SaveTokenToFile());
            }

            return(true);
        }
Exemple #11
0
        private static void LaunchMenu()
        {
            GdkToolsConfiguration toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            if (!File.Exists(toolsConfig.CustomSnapshotPath))
            {
                Debug.LogError($"Snapshot {toolsConfig.CustomSnapshotPath} not found. Make sure the file exists and it has not been moved or renamed");
                return;
            }

            Debug.Log($"Launching SpatialOS locally with snapshot {toolsConfig.CustomSnapshotPath}.");
            EditorApplication.delayCall += LaunchLocalDeployment;
        }
Exemple #12
0
        private void OnEnable()
        {
            if (toolsConfig != null)
            {
                return;
            }

            toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            errorLayoutOption.normal.textColor = Color.red;

            Undo.undoRedoPerformed += () => { configErrors = toolsConfig.Validate(); };
        }
Exemple #13
0
        private static void ForceGenerate()
        {
            File.Delete(StartupCodegenMarkerFile);

            var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            if (Directory.Exists(toolsConfig.CodegenOutputDir))
            {
                Directory.Delete(toolsConfig.CodegenOutputDir, recursive: true);
            }

            SetupProject();
            Generate();
        }
Exemple #14
0
        public static RedirectedProcess Spatial(params string[] args)
        {
            var config = GdkToolsConfiguration.GetOrCreateInstance();

            if (!string.IsNullOrEmpty(config.EnvironmentPlatform))
            {
                args = args
                       .Append($"--environment={config.EnvironmentPlatform}")
                       .ToArray();
            }

            return(Command(Tools.Common.SpatialBinary)
                   .WithArgs(args));
        }
Exemple #15
0
        private static string[] ConstructArguments()
        {
            var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            var baseArgs = new List <string>
            {
                "run",
                "-p",
                $"\"{ProjectPath}\"",
                "--",
                $"\"{Common.SpatialBinary}\"",
                $"\"{Common.CoreSdkVersion}\"",
                $"\"{toolsConfig.SchemaStdLibDir}\""
            };

            return(baseArgs.ToArray());
        }
        private static void CopySchema()
        {
            try
            {
                var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();
                // Safe as we validate there is at least one entry.
                var schemaRoot = toolsConfig.SchemaSourceDirs[0];
                CleanDestination(schemaRoot);

                var packages        = new Dictionary <string, string>();
                var dependencyQueue = new Queue <string>();

                // Find and include paths of all direct and nested dependencies.
                dependencyQueue.Enqueue(Common.ManifestPath);
                while (dependencyQueue.Count > 0)
                {
                    var dependencies = GetLocalPathsInPackage(Common.ParseDependencies(dependencyQueue.Dequeue()));

                    foreach (var dependency in dependencies)
                    {
                        if (!packages.ContainsKey(dependency.Key))
                        {
                            packages.Add(dependency.Key, dependency.Value);
                            dependencyQueue.Enqueue($"{dependency.Value}/package.json");
                        }
                    }
                }

                var schemaSources = packages.Where(SchemaPathExists)
                                    .ToDictionary(kv => kv.Key, GetSchemaPath);

                foreach (var source in schemaSources)
                {
                    CopySchemaFiles(schemaRoot, source);
                }
            }
            catch (Exception e)
            {
                Console.Error.WriteLine(e.Message);
                Environment.Exit(1);
            }
        }
Exemple #17
0
        private static bool SaveTokenToFile()
        {
            var gdkToolsConfiguration = GdkToolsConfiguration.GetOrCreateInstance();
            var devAuthTokenFullDir   = gdkToolsConfiguration.DevAuthTokenFullDir;
            var devAuthTokenFilePath  = gdkToolsConfiguration.DevAuthTokenFilepath;

            if (!PlayerPrefs.HasKey(PlayerPrefDevAuthTokenKey))
            {
                // Given we call SaveTokenToFile after successfully generating a Dev Auth Token,
                // we should never see the following error.
                Debug.LogError("Cannot save Development Authentication Token, as it has not been generated.");
                return(false);
            }

            var devAuthToken = PlayerPrefs.GetString(PlayerPrefDevAuthTokenKey);

            if (!Directory.Exists(devAuthTokenFullDir))
            {
                Directory.CreateDirectory(devAuthTokenFullDir);
            }

            try
            {
                File.WriteAllText(devAuthTokenFilePath, devAuthToken);
            }
            catch (Exception e)
            {
                Debug.LogError($"Unable to save Dev Auth Token asset. {e.Message}");
                return(false);
            }

            Debug.Log($"Saving token to {devAuthTokenFilePath}.");
            AssetDatabase.ImportAsset(DevAuthTokenAssetPath, ImportAssetOptions.ForceUpdate);
            AssetDatabase.Refresh();

            return(true);
        }
        private static void Generate()
        {
            try
            {
                if (!Common.CheckDependencies())
                {
                    return;
                }

                if (!File.Exists(CodegenExe))
                {
                    SetupProject();
                }

                EditorApplication.LockReloadAssemblies();

                Profiler.BeginSample("Add modules");
                UpdateModules();
                Profiler.EndSample();

                Profiler.BeginSample("Code generation");

                using (new ShowProgressBarScope("Generating code..."))
                {
                    ResetCodegenLogCounter();

                    var toolsConfig      = GdkToolsConfiguration.GetOrCreateInstance();
                    var loggerOutputPath = toolsConfig.DefaultCodegenLogPath;

                    var exitCode = RedirectedProcess.Command(Common.DotNetBinary)
                                   .WithArgs("run", "-p", $"\"{CodegenExe}\"")
                                   .RedirectOutputOptions(OutputRedirectBehaviour.None)
                                   .AddOutputProcessing(ProcessDotnetOutput)
                                   .AddOutputProcessing(ProcessCodegenOutput)
                                   .Run();

                    var numWarnings = codegenLogCounts[CodegenLogLevel.Warn];
                    var numErrors   = codegenLogCounts[CodegenLogLevel.Error] + codegenLogCounts[CodegenLogLevel.Fatal];

                    if (exitCode.ExitCode != 0 || numErrors > 0)
                    {
                        if (!Application.isBatchMode)
                        {
                            Debug.LogError("Code generation failed! Please check the console for more information.");

                            EditorApplication.delayCall += () =>
                            {
                                if (File.Exists(loggerOutputPath))
                                {
                                    var option = EditorUtility.DisplayDialogComplex("Generate Code",
                                                                                    $"Code generation failed with {numWarnings} warnings and {numErrors} errors!{Environment.NewLine}{Environment.NewLine}"
                                                                                    + $"Please check the code generation logs for more information: {loggerOutputPath}",
                                                                                    "Open logfile",
                                                                                    "Close",
                                                                                    "");

                                    switch (option)
                                    {
                                    // Open logfile
                                    case 0:
                                        Application.OpenURL(loggerOutputPath);
                                        break;

                                    // Close
                                    case 1:
                                    // Alt
                                    case 2:
                                        break;

                                    default:
                                        throw new ArgumentOutOfRangeException(nameof(option), "Unrecognised option");
                                    }
                                }
                                else
                                {
                                    DisplayGeneralFailure();
                                }
                            };
                        }
                    }
                    else
                    {
                        if (numWarnings > 0)
                        {
                            Debug.LogWarning($"Code generation completed successfully with {numWarnings} warnings. Please check the logs for more information: {loggerOutputPath}");
                        }
                        else
                        {
                            Debug.Log("Code generation complete!");
                        }

                        File.WriteAllText(StartupCodegenMarkerFile, string.Empty);
                    }
                }

                AssetDatabase.Refresh();
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }
            finally
            {
                Profiler.EndSample();
                EditorApplication.UnlockReloadAssemblies();
            }
        }
        internal static void GenerateCodegenRunConfigs()
        {
            var toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            // Ensure tools config is valid before continuing
            var configErrors = toolsConfig.Validate();

            if (configErrors.Count > 0)
            {
                foreach (var error in configErrors)
                {
                    Debug.LogError(error);
                }

                return;
            }

            var schemaCompilerPath = GetSchemaCompilerPath();
            var logfilePath        = toolsConfig.DefaultCodegenLogPath;

            var codegenArgs = new List <string>
            {
                $"--json-dir=\"{ImprobableJsonDir}\"",
                $"--schema-compiler-path=\"{schemaCompilerPath}\"",
                $"--worker-json-dir=\"{WorkerJsonPath}\"",
                $"--log-file=\"{logfilePath}\"",
                $"--descriptor-dir=\"{toolsConfig.DescriptorOutputDir}\"",
                $"--native-output-dir=\"{toolsConfig.FullCodegenOutputPath}\""
            };

            if (toolsConfig.VerboseLogging)
            {
                codegenArgs.Add("--verbose");
            }

            // Add user defined schema directories
            codegenArgs.AddRange(toolsConfig.SchemaSourceDirs
                                 .Select(schemaDir => $"--schema-path=\"{Path.GetFullPath(schemaDir)}\""));

            // Add package schema directories
            codegenArgs.AddRange(FindDirInPackages(SchemaPackageDir)
                                 .Select(directory => $"--schema-path=\"{directory}\""));

            codegenArgs.AddRange(toolsConfig.SerializationOverrides
                                 .Select(@override => $"--serialization-override=\"{@override}\""));

            var codegenArgsString = string.Join(" ", codegenArgs);

            // For dotnet run / visual studio
            try
            {
                var csprojXml     = XDocument.Load(CodegenExe);
                var projectNode   = csprojXml.Element("Project");
                var propertyGroup = projectNode.Element("PropertyGroup");

                var args = propertyGroup.Element("StartArguments");
                args?.Remove();

                propertyGroup.Add(XElement.Parse($"<StartArguments>{codegenArgsString}</StartArguments>"));
                csprojXml.Save(CodegenExe);
            }
            catch (Exception e)
            {
                throw new Exception($"Unable to add run configuration to '{CodegenExe}'.", e);
            }

            // For jetbrains rider
            var runConfigPath = Path.Combine(CodegenExeDirectory, ".idea", ".idea.CodeGen", ".idea", "runConfigurations");

            try
            {
                Directory.CreateDirectory(runConfigPath);
                using (var w = new StreamWriter(Path.Combine(runConfigPath, "CodeGen.xml"), append: false))
                {
                    w.Write($@"
<component name=""ProjectRunConfigurationManager"">
  <configuration default=""false"" name=""CodeGen"" type=""DotNetProject"" factoryName="".NET Project"">
    <option name=""PROJECT_PATH"" value=""$PROJECT_DIR$/CodeGen/CodeGen.csproj"" />
    <option name=""PROJECT_KIND"" value=""DotNetCore"" />
    <option name=""PROGRAM_PARAMETERS"" value=""{codegenArgsString.Replace("\"", "&quot;")}"" />
  </configuration>
</component>
");
                }
            }
            catch (Exception e)
            {
                throw new Exception($"Unable to generate Rider run configuration at '{runConfigPath}'.", e);
            }
        }
Exemple #20
0
        private static void Generate()
        {
            try
            {
                if (!Common.CheckDependencies())
                {
                    return;
                }

                if (!File.Exists(CodegenExe))
                {
                    SetupProject();
                }

                EditorApplication.LockReloadAssemblies();

                Profiler.BeginSample("Add modules");
                UpdateModules();
                Profiler.EndSample();

                Profiler.BeginSample("Code generation");

                var schemaCompilerPath = SchemaCompilerPath;

                switch (Application.platform)
                {
                case RuntimePlatform.WindowsEditor:
                    schemaCompilerPath = Path.ChangeExtension(schemaCompilerPath, ".exe");
                    break;

                case RuntimePlatform.LinuxEditor:
                case RuntimePlatform.OSXEditor:
                    RedirectedProcess.Command("chmod")
                    .WithArgs("+x", $"\"{schemaCompilerPath}\"")
                    .InDirectory(Path.GetFullPath(Path.Combine(Application.dataPath, "..")))
                    .Run();
                    break;

                default:
                    throw new PlatformNotSupportedException(
                              $"The {Application.platform} platform does not support code generation.");
                }

                var workerJsonPath = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));

                var toolsConfig      = GdkToolsConfiguration.GetOrCreateInstance();
                var loggerOutputPath = Path.GetFullPath(Path.Combine(toolsConfig.CodegenLogOutputDir, "codegen-output.log"));

                using (new ShowProgressBarScope("Generating code..."))
                {
                    ResetCodegenLogCounter();

                    var exitCode = RedirectedProcess.Command(Common.DotNetBinary)
                                   .WithArgs(ConstructArgs(CodegenExe, schemaCompilerPath, workerJsonPath, loggerOutputPath))
                                   .RedirectOutputOptions(OutputRedirectBehaviour.None)
                                   .AddOutputProcessing(ProcessDotnetOutput)
                                   .AddOutputProcessing(ProcessCodegenOutput)
                                   .Run();

                    var numWarnings = codegenLogCounts[CodegenLogLevel.Warn];
                    var numErrors   = codegenLogCounts[CodegenLogLevel.Error] + codegenLogCounts[CodegenLogLevel.Fatal];

                    if (exitCode.ExitCode != 0 || numErrors > 0)
                    {
                        if (!Application.isBatchMode)
                        {
                            Debug.LogError("Code generation failed! Please check the console for more information.");

                            EditorApplication.delayCall += () =>
                            {
                                if (File.Exists(loggerOutputPath))
                                {
                                    var option = EditorUtility.DisplayDialogComplex("Generate Code",
                                                                                    $"Code generation failed with {numWarnings} warnings and {numErrors} errors!\n\nPlease check the code generation logs for more information: {loggerOutputPath}",
                                                                                    "Open logfile",
                                                                                    "Close",
                                                                                    "");

                                    switch (option)
                                    {
                                    // Open logfile
                                    case 0:
                                        Application.OpenURL(loggerOutputPath);
                                        break;

                                    // Close
                                    case 1:
                                    // Alt
                                    case 2:
                                        break;

                                    default:
                                        throw new ArgumentOutOfRangeException("Unrecognised option");
                                    }
                                }
                                else
                                {
                                    DisplayGeneralFailure();
                                }
                            };
                        }
                    }
                    else
                    {
                        if (numWarnings > 0)
                        {
                            Debug.LogWarning($"Code generation completed successfully with {numWarnings} warnings. Please check the logs for more information: {loggerOutputPath}");
                        }
                        else
                        {
                            Debug.Log("Code generation complete!");
                        }

                        File.WriteAllText(StartupCodegenMarkerFile, string.Empty);
                    }
                }

                AssetDatabase.Refresh();
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }
            finally
            {
                Profiler.EndSample();
                EditorApplication.UnlockReloadAssemblies();
            }
        }
Exemple #21
0
        public static void LaunchLocalDeployment()
        {
            BuildConfig();
            GdkToolsConfiguration toolsConfig = GdkToolsConfiguration.GetOrCreateInstance();

            var command = Common.SpatialBinary;

#if UNITY_EDITOR_OSX
            var commandArgs = $"local launch --enable_pre_run_check=false --snapshot '{toolsConfig.CustomSnapshotPath}' --experimental_runtime={toolsConfig.RuntimeVersion}";
#else
            var commandArgs = $"local launch --enable_pre_run_check=false --snapshot \"{toolsConfig.CustomSnapshotPath}\" --experimental_runtime={toolsConfig.RuntimeVersion}";
#endif

            var runtimeIp = EditorPrefs.GetString(Common.RuntimeIpEditorPrefKey);
            if (!string.IsNullOrEmpty(runtimeIp))
            {
                commandArgs = $"{commandArgs} --runtime_ip={runtimeIp}";
            }

            if (Application.platform == RuntimePlatform.OSXEditor)
            {
                command     = "osascript";
                commandArgs = $@"-e 'tell application ""Terminal""
                                     activate
                                     do script ""cd {Common.SpatialProjectRootDir} && {Common.SpatialBinary} {commandArgs}""
                                     end tell'";
            }

            var processInfo = new ProcessStartInfo(command, commandArgs)
            {
                CreateNoWindow   = false,
                UseShellExecute  = true,
                WorkingDirectory = Common.SpatialProjectRootDir
            };

            var process = Process.Start(processInfo);

            if (process == null)
            {
                Debug.LogError("Failed to start SpatialOS locally.");
                return;
            }

            process.EnableRaisingEvents = true;
            process.Exited += (sender, args) =>
            {
                // N.B. This callback is run on a different thread.
                if (process.ExitCode == 0)
                {
                    return;
                }

                var logPath       = Path.Combine(Common.SpatialProjectRootDir, "logs");
                var latestLogFile = Directory.GetFiles(logPath, "spatial_*.log")
                                    .Select(f => new FileInfo(f))
                                    .OrderBy(f => f.LastWriteTimeUtc).LastOrDefault();

                if (latestLogFile == null)
                {
                    Debug.LogError($"Could not find a spatial log file in {logPath}.");
                    return;
                }

                var message = $"For more information, check the spatial local launch logfile: {latestLogFile.FullName}";

                if (WasProcessKilled(process))
                {
                    Debug.Log(message);
                }
                else
                {
                    Debug.LogError($"Errors occured - {message}");
                }

                process.Dispose();
                process = null;
            };
        }