using System.IO; using UnityEditor; using UnityEngine; namespace Unity.Pipeline.Editor { /// /// Custom inspector for the settings asset: shows live server /// status (accurate running check + actual port, read from the static owner) and offers /// Start/Stop/Restart buttons, alongside the editable configuration. Watchdog edits are pushed to /// a running server immediately; port/autoStart apply on the next start. /// [CustomEditor(typeof(EditorPipelineManager))] class EditorPipelineManagerEditor : UnityEditor.Editor { // Repaint() from inside OnInspectorGUI re-triggers OnInspectorGUI on the next editor tick, so // a running server redrew this IMGUI-heavy inspector every frame (several ms) while merely // visible. Repaint from EditorApplication.update at a low rate instead. private const double StatusRepaintInterval = 0.25; private double m_NextStatusRepaint; private void OnEnable() { EditorApplication.update += ThrottledStatusRepaint; } private void OnDisable() { EditorApplication.update -= ThrottledStatusRepaint; } private void ThrottledStatusRepaint() { var mgr = target as EditorPipelineManager; if (mgr == null || !(mgr.IsServerRunning || EditorCodeReloadWatcher.IsWatching)) return; double now = EditorApplication.timeSinceStartup; if (now < m_NextStatusRepaint) return; m_NextStatusRepaint = now + StatusRepaintInterval; Repaint(); } /// Draw the default inspector plus live server status and Start/Stop/Restart controls. public override void OnInspectorGUI() { var mgr = (EditorPipelineManager)target; EditorGUI.BeginChangeCheck(); DrawDefaultInspector(); if (EditorGUI.EndChangeCheck()) { // Push edited watchdog config to the live server (owned by PipelineServerStartup), so // changes take effect without a restart. var server = PipelineServerStartup.Server; if (server != null) { server.WatchdogEnabled = mgr.WatchdogEnabled; server.WatchdogIntervalSeconds = mgr.WatchdogIntervalSeconds; server.LogRequestsResponses = mgr.LogRequestsResponses; } // Eval-usage telemetry config lives on static fields (recording happens in the // Runtime command layer, not the server), so push edits there too — applies live. // Goes through the same ApplyEvalTelemetrySettings the startup/server-start paths // use, so there is exactly one place that knows the settings->statics mapping. PipelineServerStartup.ApplyEvalTelemetrySettings(mgr); } EditorGUILayout.Space(); EditorGUILayout.LabelField("Status", EditorStyles.boldLabel); using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.Toggle("Running", mgr.IsServerRunning); EditorGUILayout.IntField("Actual Port", mgr.ActualPort); } EditorGUILayout.Space(); using (new EditorGUILayout.HorizontalScope()) { using (new EditorGUI.DisabledScope(mgr.IsServerRunning)) { if (GUILayout.Button("Start")) mgr.StartServer(); } using (new EditorGUI.DisabledScope(!mgr.IsServerRunning)) { if (GUILayout.Button("Stop")) mgr.StopServer(); } if (GUILayout.Button("Restart")) mgr.RestartServer(); } DrawCodeReloadWatchSection(); // Live status is kept current by ThrottledStatusRepaint — never Repaint() from here. } /// /// Watch the project's Assets folder and re-apply affected [CodeReload] methods on every save, /// driving . Neither scope, target, nor backend is a /// choice: the whole Assets tree is watched, each save goes to the connected player(s) if any, /// else applies in-editor, and both paths run on the interpreter; a read-only row shows the /// current target resolution. Config is locked while a watch is active. /// private void DrawCodeReloadWatchSection() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Code Reload Interpreter Watch", EditorStyles.boldLabel); bool watching = EditorCodeReloadWatcher.IsWatching; serializedObject.Update(); // Not a picker: each save is pushed to the connected player(s) if any, else applied // in-editor — resolved per save, so plugging in a device mid-watch reroutes the next save. int connected = EditorCodeReloadWatcher.ConnectedPlayerCount; using (new EditorGUI.DisabledScope(true)) EditorGUILayout.LabelField( new GUIContent("Target", "Resolved automatically at each file change: development player(s) if connected, otherwise the reload applies in this editor process. Both run on the interpreter."), new GUIContent(connected > 0 ? $"Player — {connected} connected" : "In Editor — no player connected")); // Outside the watching-locked scope: read at each player-connect event, so toggling it // takes effect for the next connection even while a watch is active. EditorGUILayout.PropertyField( serializedObject.FindProperty("m_CodeReloadRepushOnConnect"), new GUIContent("Re-push On Connect", "Re-push the watched code-reload state to any player that connects while the watch " + "is active. A restarted player boots the original build without previously pushed " + "overrides; this catches it up immediately instead of waiting for the next save.")); serializedObject.ApplyModifiedProperties(); using (new EditorGUILayout.HorizontalScope()) { using (new EditorGUI.DisabledScope(watching)) { // The UI always watches the whole Assets tree; StartWatch itself accepts any // file or folder for CLI/API callers. if (GUILayout.Button("Start Watching")) EditorCodeReloadWatcher.StartWatch(Path.GetFullPath(Application.dataPath), isFolder: true); } using (new EditorGUI.DisabledScope(!watching)) { if (GUILayout.Button("Stop Watching")) EditorCodeReloadWatcher.StopWatch(); } } if (watching) EditorGUILayout.HelpBox( "Asset auto-refresh is disabled while watching, so saved scripts code-reload instead of " + "triggering a domain reload. New or changed assets won't import until you refresh manually " + "(Cmd/Ctrl+R); Stop Watching restores your auto-refresh setting.", MessageType.Warning); using (new EditorGUI.DisabledScope(true)) { // Read WatchPath once: it goes null the instant the watch stops (domain reload / StopWatch), // which can race the `watching` captured at the top of this method — guard against null here. var watchPath = EditorCodeReloadWatcher.WatchPath; // The live Target row above shows where saves currently go; this line covers what is watched. string status = watching && !string.IsNullOrEmpty(watchPath) ? $"{(EditorCodeReloadWatcher.IsFolder ? "folder" : "file")} · {Path.GetFileName(watchPath.TrimEnd('/', '\\'))}" : "idle"; EditorGUILayout.LabelField("Watching", status); if (watching) { // Liveness: saves applying while the event count stays flat means the OS file // watcher is dead and the reconcile poll is carrying the watch. EditorGUILayout.LabelField("Watcher Events", $"{EditorCodeReloadWatcher.FsEventCount}{Ago(EditorCodeReloadWatcher.LastFsEventUtc)}"); EditorGUILayout.LabelField("Last Apply", EditorCodeReloadWatcher.LastApplyFile is string f ? $"{f}{Ago(EditorCodeReloadWatcher.LastApplyUtc)}" : "none yet"); } } } private static string Ago(System.DateTime? utc) { if (utc == null) return ""; var s = (System.DateTime.UtcNow - utc.Value).TotalSeconds; return s < 1 ? " · just now" : s < 120 ? $" · {s:0}s ago" : s < 7200 ? $" · {s / 60:0}m ago" : $" · {s / 3600:0}h ago"; } } }