/// <summary>
        /// Writes information about a single service to the database.
        /// </summary>
        /// <param name="obj">A ServiceObject to write.</param>
        public void Write(ServiceObject obj)
        {
            _numCollected++;

            var cmd = new SqliteCommand(INSERT_SQL, DatabaseManager.Connection, DatabaseManager.Transaction);

            cmd.Parameters.AddWithValue("@run_id", this.runId);
            cmd.Parameters.AddWithValue("@row_key", obj.GetUniqueHash());
            cmd.Parameters.AddWithValue("@service_name", obj.ServiceName);
            cmd.Parameters.AddWithValue("@display_name", obj.DisplayName);
            cmd.Parameters.AddWithValue("@start_type", obj.StartType);
            cmd.Parameters.AddWithValue("@current_state", obj.CurrentState);
            cmd.Parameters.AddWithValue("@serialized", JsonConvert.SerializeObject(obj));

            cmd.ExecuteNonQuery();
        }
        /// <summary>
        /// Executes the ServiceCollector (main entrypoint).
        /// </summary>
        public override void Execute()
        {
            Start();

            if (!this.CanRunOnPlatform())
            {
                Log.Information("ServiceCollector cannot run on this platform.");
                return;
            }

            Truncate(runId);

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // This gathers official "services" on Windows, but perhaps neglects other startup items
                foreach (ServiceController service in ServiceController.GetServices())
                {
                    if (this.filter != null && !this.filter(service))
                    {
                        Log.Information("Service [{0}] did not pass filter, ignoring.", service.ToString());
                        continue;
                    }

                    var obj = new ServiceObject()
                    {
                        DisplayName  = service.DisplayName,
                        ServiceName  = service.ServiceName,
                        StartType    = service.StartType.ToString(),
                        CurrentState = service.Status.ToString()
                    };

                    this.Write(obj);
                }
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                var runner = new ExternalCommandRunner();

                // Get the user processes
                // run "launchtl dumpstate" for the super detailed view
                // However, dumpstate is difficult to parse
                var result = runner.RunExternalCommand("launchctl", "list");
                Dictionary <string, ServiceObject> outDict = new Dictionary <string, ServiceObject>();
                foreach (var _line in result.Split('\n'))
                {
                    // Lines are formatted like this:
                    // PID   Exit  Name
                    //1015    0   com.apple.appstoreagent
                    var _fields = _line.Split('\t');
                    if (_fields.Length < 3 || _fields[0].Contains("PID"))
                    {
                        continue;
                    }
                    var obj = new ServiceObject()
                    {
                        DisplayName = _fields[2],
                        ServiceName = _fields[2],
                        StartType   = "Unknown",
                        // If we have a current PID then it is running.
                        CurrentState = (_fields[0].Equals("-"))?"Stopped":"Running"
                    };
                    if (!outDict.ContainsKey(obj.GetUniqueHash()))
                    {
                        this.Write(obj);
                        outDict.Add(obj.GetUniqueHash(), obj);
                    }
                }

                // Then get the system processes
                result = runner.RunExternalCommand("sudo", "launchctl list");

                foreach (var _line in result.Split('\n'))
                {
                    // Lines are formatted like this, with single tab separation:
                    //  PID     Exit    Name
                    //  1015    0       com.apple.appstoreagent
                    var _fields = _line.Split('\t');
                    if (_fields.Length < 3 || _fields[0].Contains("PID"))
                    {
                        continue;
                    }
                    var obj = new ServiceObject()
                    {
                        DisplayName = _fields[2],
                        ServiceName = _fields[2],
                        StartType   = "Unknown",
                        // If we have a current PID then it is running.
                        CurrentState = (_fields[0].Equals("-")) ? "Stopped" : "Running"
                    };

                    if (!outDict.ContainsKey(obj.GetUniqueHash()))
                    {
                        this.Write(obj);
                        outDict.Add(obj.GetUniqueHash(), obj);
                    }
                }
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                var runner = new ExternalCommandRunner();

                var result = runner.RunExternalCommand("systemctl", "list-units --type service");

                //Split lines and remove header
                var lines = result.Split('\n');
                lines.ToList().RemoveAt(0);

                foreach (var _line in lines)
                {
                    var _fields = _line.Split('\t');

                    if (_fields.Count() == 5)
                    {
                        var obj = new ServiceObject()
                        {
                            DisplayName  = _fields[4],
                            ServiceName  = _fields[0],
                            StartType    = "Unknown",
                            CurrentState = _fields[3],
                        };

                        Write(obj);
                    }
                }

                result = runner.RunExternalCommand("ls", "/etc/init.d/ -l");

                lines = result.Split('\n');
                String pattern = @".*\s(.*)";

                foreach (var _line in lines)
                {
                    Match           match       = Regex.Match(_line, pattern);
                    GroupCollection groups      = match.Groups;
                    var             serviceName = groups[1].ToString();

                    var obj = new ServiceObject()
                    {
                        DisplayName  = serviceName,
                        ServiceName  = serviceName,
                        StartType    = "Unknown",
                        CurrentState = "Unknown"
                    };

                    Write(obj);
                }

                // without systemd (maybe just CentOS)
                // chkconfig --list

                // BSD
                // service -l
                // this provides very minor amount of info
            }

            Stop();
        }