Getting Mono Process Info from a Mono App

Since a large amount of the work I tend to do is for embedded devices, and since I don’t like to have to visit deployed devices to restart an app whenever it may crash, a pretty common pattern I use is to create a watchdog application that periodically checks to see if the actual application is running and start it if it’s not.  It a bit more complex than that because I typically have support for intentional shutdowns and I like to log all restarts for diagnostics, but the general premise is pretty simple.  The app should always be running.  If it’s not, start it again.

In Mono (under Linux anyway) that task turns out to be a bit of a challenge.  Process.GetProcesses doesn’t work because rather than giving the actuall Application name like the .NET Framework does under Windows, Mono simply returns a Process with a ProcessName of “mono-sgen” for all of the Mono apps running.  I can’t differentiate between the Watchdog app, the target app and any other app that may or may not be running.

I ended up creating a new class I called LinuxProcess (for lack of a better name).  Ideally it would be a Process derivative, or even rolled back into the Mono source, but for now it’s stand-alone and feature-limited to what I needed for a Watchdog.  Full source is below (sorry about the length, but I prefer this over a zip, and it’s searchable and indexable).

using System;
using System.IO;
using System.Linq;

using Output = System.Console;
using System.Collections.Generic;

namespace System.Diagnostics
{
	public class LinuxProcess
	{
		private Process m_process;

        private LinuxProcess(string fileName)
        {
            m_process = Process.Start(fileName);
        }

		private LinuxProcess(int pid)
		{
			Id = pid;
			m_process = Process.GetProcessById(pid);

			if (m_process == null)
			{
				Output.WriteLine("GetProcessById returned null");
			}
		}

        public static LinuxProcess Start(string fileName)
        {
            return new LinuxProcess(fileName);
        }

        public bool HasExited
        {
            get { return m_process.HasExited; }
        }

		public void Kill()
		{
			m_process.Kill();
		}

		public static LinuxProcess[] GetProcessesByName(string processName)
		{
			return GetProcesses().Where(p => p.ProcessName == processName).ToArray();
		}

		public static LinuxProcess[] GetProcesses()
		{
			var list = new List<LinuxProcess>();

			foreach (var path in Directory.GetDirectories("/proc")) 
			{
				var d = Path.GetFileName(path);
				int pid;

				if (!int.TryParse(d, out pid))
				{
					continue;
				}
					
				// stat
				var stat = GetStat(pid);
				if (stat == null) continue;

				var proc = new LinuxProcess(stat.PID);
				proc.ProcessState = stat.State;

				// look for mono-specific processes
				if (stat.FileName == "(mono)")
				{
					// TODO: handle command-line args to the Mono app
					var cmdline = GetCommandLine(stat.PID);

					// cmdline[0] == path to mono
					// cmdline[1] == mono app
					// cmdline[1+n] == mono app args
					proc.ProcessName = Path.GetFileName(cmdline[1]);
				}
				else
				{
					// trim out the parens
					proc.ProcessName = stat.FileName.Trim(new char[] { '(', ')' });
				}

				list.Add(proc);
			}

			return list.ToArray();
		}

		private static Stat GetStat(int pid)
		{
			try
			{
				var statDir = string.Format("/proc/{0}/stat", pid);
				if (!File.Exists(statDir))
					return null;

				var proc = new LinuxProcess(pid);

				using (var reader = File.OpenText(statDir))
				{
					var line = reader.ReadToEnd();
					return new Stat(line);
				}
			}
			catch (Exception ex)
			{
				Output.WriteLine("Stat Exception: " + ex.Message);
				return null;
			}
		}

		private static string[] GetCommandLine(int pid)
		{
			// The command line arguments appear in this file as a set of null-separated strings, with a further null byte after the last string. 
			using (var reader = File.OpenText(string.Format("/proc/{0}/cmdline", pid)))
			{
				string contents = reader.ReadToEnd();
				var args = contents.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
				return args;
			}
		}

		public int Id { get; private set; }
		public string ProcessName { get; private set; }

		public ProcessState ProcessState { get; private set; }
	}

	internal class Stat
	{
		internal Stat(string procLine)
		{
			try
			{
				var items = procLine.Split(new char[] { ' ' }, StringSplitOptions.None);

				PID = Convert.ToInt16(items[0]);
				FileName = items[1];

				switch (items[2][0])
				{
					case 'R':
						State = ProcessState.Running;
						break;
					case 'S':
						State = ProcessState.InterruptableWait;
						break;
					case 'D':
						State = ProcessState.UninterruptableDiskWait;
						break;
					case 'Z':
						State = ProcessState.Zombie;
						break;
					case 'T':
						State = ProcessState.Traced;
						break;
					case 'W':
						State = ProcessState.Paging;
						break;
				}
			}
			catch (Exception ex)
			{
				Output.WriteLine("Stat parse exception: " + ex.Message);
			}
		}

		public int PID { get; private set; }
		public string FileName { get; private set; }
		public ProcessState State { get; private set; }
	}

	public enum ProcessState
	{
		Running, // R
		InterruptableWait, // S
		UninterruptableDiskWait, // D
		Zombie, // Z
		Traced, // T
		Paging // W
	}
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s