using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Text; using System.Threading; [DataContract] internal sealed class OwnedLaunch { [DataMember(Name = "executable", IsRequired = true)] public string Executable { get; set; } [DataMember(Name = "args", IsRequired = true)] public string[] Arguments { get; set; } [DataMember(Name = "cwd", IsRequired = true)] public string WorkingDirectory { get; set; } } [DataContract] internal sealed class Installation { [DataMember(Name = "version", IsRequired = true)] public int Version { get; set; } [DataMember(Name = "nodePath", IsRequired = true)] public string NodePath { get; set; } [DataMember(Name = "agentPath", IsRequired = true)] public string AgentPath { get; set; } [DataMember(Name = "workingDirectory", IsRequired = true)] public string WorkingDirectory { get; set; } [DataMember(Name = "configPath", IsRequired = true)] public string ConfigPath { get; set; } [DataMember(Name = "envFile", EmitDefaultValue = false)] public string EnvFile { get; set; } } [DataContract] internal sealed class RuntimeState { [DataMember(Name = "hostPid")] public int HostPid { get; set; } [DataMember(Name = "agentPid")] public uint AgentPid { get; set; } [DataMember(Name = "startedAtUtc")] public string StartedAtUtc { get; set; } } internal static class Program { private const uint CREATE_SUSPENDED = 0x00000004; private const uint CREATE_NO_WINDOW = 0x08000000; private const uint JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000; private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; private const int JobObjectExtendedLimitInformation = 9; private const uint INFINITE = 0xFFFFFFFF; private const uint WAIT_OBJECT_0 = 0; private const uint RESUME_FAILED = 0xFFFFFFFF; [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public long PerProcessUserTimeLimit; public long PerJobUserTimeLimit; public uint LimitFlags; public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public uint ActiveProcessLimit; public UIntPtr Affinity; public uint PriorityClass; public uint SchedulingClass; } [StructLayout(LayoutKind.Sequential)] private struct IO_COUNTERS { public ulong ReadOperationCount; public ulong WriteOperationCount; public ulong OtherOperationCount; public ulong ReadTransferCount; public ulong WriteTransferCount; public ulong OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct STARTUPINFO { public uint cb; public string lpReserved; public string lpDesktop; public string lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public ushort wShowWindow; public ushort cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject( IntPtr hJob, int jobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CreateProcess( string lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint ResumeThread(IntPtr hThread); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); [DllImport("kernel32.dll")] private static extern IntPtr GetStdHandle(int which); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags); [STAThread] private static int Main(string[] args) { string installationPath = null; try { if (args.Length == 1 && args[0] == "--owned-child") return RunOwnedChild(); installationPath = ParseInstallationPath(args); return Run(installationPath); } catch (Exception error) { if (args.Length == 1 && args[0] == "--owned-child") return 1; WriteError(installationPath, error); return 1; } } // A private stdin lease ties this job to its creator, even if the Agent // is forcibly terminated by Task Scheduler. No PID files grant authority. private static int RunOwnedChild() { StreamReader input = new StreamReader(Console.OpenStandardInput(), Encoding.UTF8); StringBuilder line = new StringBuilder(); int c; while ((c = input.Read()) != -1 && c != '\n') { if (line.Length >= 65536) throw new InvalidDataException("Launch frame too large"); line.Append((char)c); } OwnedLaunch launch; using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(line.ToString()))) launch = (OwnedLaunch)new DataContractJsonSerializer(typeof(OwnedLaunch)).ReadObject(stream); RequireFile(launch.Executable, "Owned executable"); if (!Path.IsPathRooted(launch.Executable) || !Directory.Exists(launch.WorkingDirectory)) throw new InvalidDataException("Invalid launch paths"); IntPtr job = IntPtr.Zero; PROCESS_INFORMATION process = new PROCESS_INFORMATION(); bool resumed = false; try { job = CreateJobObject(IntPtr.Zero, null); if (job == IntPtr.Zero) ThrowLastError("CreateJobObject"); ConfigureJob(job, false); STARTUPINFO startup = new STARTUPINFO(); startup.cb = (uint)Marshal.SizeOf(typeof(STARTUPINFO)); startup.dwFlags = 0x100; // STARTF_USESTDHANDLES; stdin belongs only to the lease. startup.hStdOutput = GetStdHandle(-11); startup.hStdError = GetStdHandle(-12); SetHandleInformation(startup.hStdOutput, 1, 1); SetHandleInformation(startup.hStdError, 1, 1); if (!CreateProcess(launch.Executable, BuildCommandLine(launch.Executable, launch.Arguments), IntPtr.Zero, IntPtr.Zero, true, CREATE_SUSPENDED | CREATE_NO_WINDOW, IntPtr.Zero, launch.WorkingDirectory, ref startup, out process)) ThrowLastError("CreateProcess"); if (!AssignProcessToJobObject(job, process.hProcess)) ThrowLastError("AssignProcessToJobObject"); if (ResumeThread(process.hThread) == RESUME_FAILED) ThrowLastError("ResumeThread"); resumed = true; Thread lease = new Thread(delegate() { try { while (input.Read() != -1) { } } catch { } Environment.Exit(0); // Closing our only job handle kills every descendant. }); lease.IsBackground = true; lease.Start(); if (WaitForSingleObject(process.hProcess, INFINITE) != WAIT_OBJECT_0) ThrowLastError("WaitForSingleObject"); uint code; if (!GetExitCodeProcess(process.hProcess, out code)) ThrowLastError("GetExitCodeProcess"); return unchecked((int)code); } finally { if (!resumed && process.hProcess != IntPtr.Zero) TerminateProcess(process.hProcess, 1); if (job != IntPtr.Zero) CloseHandle(job); if (process.hThread != IntPtr.Zero) CloseHandle(process.hThread); if (process.hProcess != IntPtr.Zero) CloseHandle(process.hProcess); } } private static int Run(string installationPath) { Installation installation = ReadJson(installationPath); ValidateInstallation(installation); string stateDirectory = Path.GetDirectoryName(installationPath); string runtimePath = Path.Combine(stateDirectory, "runtime.json"); string errorPath = Path.Combine(stateDirectory, "last-error.txt"); TryDelete(errorPath); TryDelete(runtimePath); IntPtr job = IntPtr.Zero; PROCESS_INFORMATION process = new PROCESS_INFORMATION(); bool completed = false; try { job = CreateJobObject(IntPtr.Zero, null); if (job == IntPtr.Zero) ThrowLastError("CreateJobObject"); ConfigureJob(job); string[] arguments = BuildAgentArguments(installation); STARTUPINFO startup = new STARTUPINFO(); startup.cb = (uint)Marshal.SizeOf(typeof(STARTUPINFO)); StringBuilder commandLine = BuildCommandLine(installation.NodePath, arguments); if (!CreateProcess( installation.NodePath, commandLine, IntPtr.Zero, IntPtr.Zero, false, CREATE_SUSPENDED | CREATE_NO_WINDOW, IntPtr.Zero, installation.WorkingDirectory, ref startup, out process)) { ThrowLastError("CreateProcess"); } if (!AssignProcessToJobObject(job, process.hProcess)) { ThrowLastError("AssignProcessToJobObject"); } WriteJsonAtomically(runtimePath, new RuntimeState { HostPid = Process.GetCurrentProcess().Id, AgentPid = process.dwProcessId, StartedAtUtc = DateTime.UtcNow.ToString("o") }); if (ResumeThread(process.hThread) == RESUME_FAILED) { ThrowLastError("ResumeThread"); } uint waitResult = WaitForSingleObject(process.hProcess, INFINITE); if (waitResult != WAIT_OBJECT_0) { ThrowLastError("WaitForSingleObject"); } uint exitCode; if (!GetExitCodeProcess(process.hProcess, out exitCode)) { ThrowLastError("GetExitCodeProcess"); } completed = true; return unchecked((int)exitCode); } finally { TryDelete(runtimePath); if (!completed && process.hProcess != IntPtr.Zero) { TerminateProcess(process.hProcess, 1); } if (process.hThread != IntPtr.Zero) CloseHandle(process.hThread); if (process.hProcess != IntPtr.Zero) CloseHandle(process.hProcess); if (job != IntPtr.Zero) CloseHandle(job); } } private static string ParseInstallationPath(string[] args) { if (args.Length != 2 || args[0] != "--installation" || String.IsNullOrWhiteSpace(args[1])) { throw new ArgumentException("Usage: palmtty-autostart-host.exe --installation "); } return Path.GetFullPath(args[1]); } private static void ValidateInstallation(Installation installation) { if (installation == null || installation.Version != 1) { throw new InvalidDataException("Unsupported PalmTTY autostart installation manifest"); } RequireFile(installation.NodePath, "Node executable"); RequireFile(installation.AgentPath, "PalmTTY Agent"); RequireFile(installation.ConfigPath, "PalmTTY config"); if (!String.IsNullOrWhiteSpace(installation.EnvFile)) { RequireFile(installation.EnvFile, "Environment file"); } if (String.IsNullOrWhiteSpace(installation.WorkingDirectory) || !Directory.Exists(installation.WorkingDirectory)) { throw new DirectoryNotFoundException("Working directory does not exist: " + installation.WorkingDirectory); } } private static void RequireFile(string filePath, string label) { if (String.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) { throw new FileNotFoundException(label + " does not exist", filePath); } } private static string[] BuildAgentArguments(Installation installation) { if (String.IsNullOrWhiteSpace(installation.EnvFile)) { return new[] { installation.AgentPath, "--config", installation.ConfigPath }; } return new[] { installation.AgentPath, "--config", installation.ConfigPath, "--env-file", installation.EnvFile }; } private static T ReadJson(string filePath) { DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T)); using (FileStream stream = File.OpenRead(filePath)) { return (T)serializer.ReadObject(stream); } } private static void WriteJsonAtomically(string filePath, T value) { string tempPath = filePath + "." + Guid.NewGuid().ToString("N") + ".tmp"; DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T)); try { using (FileStream stream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { serializer.WriteObject(stream, value); stream.Flush(true); } if (File.Exists(filePath)) File.Delete(filePath); File.Move(tempPath, filePath); } finally { TryDelete(tempPath); } } private static StringBuilder BuildCommandLine(string executable, string[] arguments) { StringBuilder result = new StringBuilder(); result.Append(QuoteArgument(executable)); foreach (string argument in arguments) { result.Append(' '); result.Append(QuoteArgument(argument)); } return result; } private static string QuoteArgument(string value) { if (value.Length == 0) return "\"\""; bool needsQuotes = false; foreach (char character in value) { if (Char.IsWhiteSpace(character) || character == '"') { needsQuotes = true; break; } } if (!needsQuotes) return value; StringBuilder result = new StringBuilder(); result.Append('"'); int backslashes = 0; foreach (char character in value) { if (character == '\\') { backslashes += 1; continue; } if (character == '"') { result.Append('\\', backslashes * 2 + 1); result.Append('"'); backslashes = 0; continue; } result.Append('\\', backslashes); result.Append(character); backslashes = 0; } result.Append('\\', backslashes * 2); result.Append('"'); return result.ToString(); } private static void ConfigureJob(IntPtr job, bool allowBreakaway = true) { JOBOBJECT_EXTENDED_LIMIT_INFORMATION information = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); information.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | (allowBreakaway ? JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK : 0); int size = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); IntPtr buffer = Marshal.AllocHGlobal(size); try { Marshal.StructureToPtr(information, buffer, false); if (!SetInformationJobObject( job, JobObjectExtendedLimitInformation, buffer, (uint)size)) { ThrowLastError("SetInformationJobObject"); } } finally { Marshal.FreeHGlobal(buffer); } } private static void ThrowLastError(string operation) { throw new Win32Exception(Marshal.GetLastWin32Error(), operation); } private static void WriteError(string installationPath, Exception error) { try { string directory = !String.IsNullOrWhiteSpace(installationPath) ? Path.GetDirectoryName(Path.GetFullPath(installationPath)) : AppDomain.CurrentDomain.BaseDirectory; Directory.CreateDirectory(directory); File.WriteAllText( Path.Combine(directory, "last-error.txt"), DateTime.UtcNow.ToString("o") + Environment.NewLine + error.ToString(), new UTF8Encoding(false) ); } catch { // A GUI-subsystem launcher has no console. Error persistence is best effort. } } private static void TryDelete(string filePath) { try { if (!String.IsNullOrWhiteSpace(filePath) && File.Exists(filePath)) File.Delete(filePath); } catch { // Best-effort cleanup. } } }