Editor/SoloTestLauncher.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Editor;

namespace TeamCreate;

/// <summary>Opens the disposable copy in a second editor window. It only ever launches a folder that <see cref="SoloCopy"/> made.</summary>
public static class SoloTestLauncher
{
    private static string ExecutablePath()
    {
        try
        {
            string self = Process.GetCurrentProcess().MainModule?.FileName;
            return !string.IsNullOrEmpty(self) && File.Exists(self) ? self : null;
        }
        catch (Exception)
        {
            return null;
        }
    }

    /// <summary>
    /// Memory the system can hand out right now: physical memory not in use, including the standby cache. Read from the runtime's own view of system
    /// memory load, so it needs no native call. Null when the runtime cannot say.
    /// </summary>
    public static long? AvailableMemoryBytes()
    {
        try
        {
            GCMemoryInfo info = GC.GetGCMemoryInfo();
            if (info.TotalAvailableMemoryBytes <= 0 || info.MemoryLoadBytes <= 0)
            {
                return null;
            }
            return Math.Max(0, info.TotalAvailableMemoryBytes - info.MemoryLoadBytes);
        }
        catch (Exception)
        {
            return null;
        }
    }

    public const string HeartbeatFile = "solo-alive.txt";

    /// <summary>
    /// True when the copy's own editor has reported in recently. The copy writes its process id and the time every few seconds
    /// (<see cref="Beat"/>); a stale file, or an id that is no longer a running process, means the window is closed.
    /// </summary>
    public static bool IsRunning(string copyRoot)
    {
        try
        {
            string path = Path.Combine(copyRoot, ".collaboration", HeartbeatFile);
            if (!File.Exists(path))
            {
                return false;
            }
            string[] lines = File.ReadAllLines(path);
            if (lines.Length < 2 || !int.TryParse(lines[0], out int pid) || !DateTime.TryParse(lines[1], null, System.Globalization.DateTimeStyles.RoundtripKind, out DateTime at))
            {
                return false;
            }
            if ((DateTime.UtcNow - at).TotalSeconds > 20)
            {
                return false;
            }
            using (Process process = Process.GetProcessById(pid))
            {
                return !process.HasExited;
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private static DateTime nextBeat;

    /// <summary>Called from the copy's frame tick.</summary>
    public static void Beat(string root)
    {
        if (DateTime.UtcNow < nextBeat)
        {
            return;
        }
        nextBeat = DateTime.UtcNow.AddSeconds(5);
        try
        {
            File.WriteAllLines(Path.Combine(root, ".collaboration", HeartbeatFile), new[] { Environment.ProcessId.ToString(), DateTime.UtcNow.ToString("o") });
        }
        catch (Exception)
        {
        }
    }

    /// <summary>Returns null on success, or a sentence saying what went wrong.</summary>
    public static string Launch(string copyRoot)
    {
        if (!TestHarnessRules.IsDisposableCopy(copyRoot, SoloCopy.HasMarker(copyRoot)))
        {
            return "That folder is not a solo test copy, so it was not opened.";
        }
        string exe = ExecutablePath();
        if (exe == null)
        {
            return "The editor's program file could not be found, so the copy could not be opened. Open " + copyRoot + " from the s&box launcher instead; the test starts by itself.";
        }
        string project = Directory.EnumerateFiles(copyRoot, "*.sbproj", SearchOption.TopDirectoryOnly).FirstOrDefault();
        if (project == null)
        {
            return "The copy has no project file.";
        }
        try
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = exe,
                Arguments = "-project \"" + project + "\" -novid",
                UseShellExecute = false
            });
            return null;
        }
        catch (Exception ex)
        {
            return "The solo test window could not be opened: " + ex.Message + " Open " + copyRoot + " from the s&box launcher instead; the test starts by itself.";
        }
    }
}