Class: Potluck::Service

Inherits:
Object
  • Object
show all
Defined in:
lib/potluck/service.rb

Overview

A Ruby interface for configuring, controlling, and interacting with external processes. Serves as a parent class for service-specific child classes.

Constant Summary collapse

SERVICE_PREFIX =
'potluck.npickens.'
LAUNCHCTL_ERROR_REGEX =
/^-|\t[^0]\t/.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(logger: nil, manage: self.class.launchctl?) ⇒ Service

Creates a new instance.

  • logger - Logger instance to use for outputting info and error messages (optional). Output will be sent to stdout and stderr if none is supplied.

  • manage - True if the service runs locally and should be managed by this process (default: true if launchctl is available and false otherwise).



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/potluck/service.rb', line 27

def initialize(logger: nil, manage: self.class.launchctl?)
  @logger = logger
  @manage = !!manage
  @manage_with_launchctl = false

  if manage.kind_of?(Hash)
    @status_command = manage[:status]
    @status_error_regex = manage[:status_error_regex]
    @start_command = manage[:start]
    @stop_command = manage[:stop]
  elsif manage
    @manage_with_launchctl = true
    self.class.ensure_launchctl!
  end
end

Class Method Details

.ensure_launchctl!Object

Checks if launchctl is available and raises an error if not.



224
225
226
# File 'lib/potluck/service.rb', line 224

def self.ensure_launchctl!
  launchctl? || raise(ServiceError, "Cannot manage #{pretty_name}: launchctl not found")
end

.launchctl?Boolean

Returns true if launchctl is available.

Returns:

  • (Boolean)


217
218
219
# File 'lib/potluck/service.rb', line 217

def self.launchctl?
  defined?(@@launchctl) ? @@launchctl : (@@launchctl = `which launchctl 2>&1` && $? == 0)
end

.launchctl_nameObject

Name for the launchctl service.



173
174
175
# File 'lib/potluck/service.rb', line 173

def self.launchctl_name
  "#{SERVICE_PREFIX}#{service_name}"
end

.plist(content = '') ⇒ Object

Content of the launchctl plist file.



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/potluck/service.rb', line 187

def self.plist(content = '')
  <<~EOS
    <?xml version="1.0" encoding="UTF-8"?>
    #{'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.'\
      '0.dtd">'}
    <plist version="1.0">
    <dict>
      <key>Label</key>
      <string>#{launchctl_name}</string>
      <key>RunAtLoad</key>
      <true/>
      <key>KeepAlive</key>
      <false/>
      #{content.gsub(/^/, '  ').strip}
    </dict>
    </plist>
  EOS
end

.plist_pathObject

Path to the launchctl plist file of the service.



180
181
182
# File 'lib/potluck/service.rb', line 180

def self.plist_path
  File.join(DIR, "#{launchctl_name}.plist")
end

.pretty_nameObject

Human-friendly name of the service.



159
160
161
# File 'lib/potluck/service.rb', line 159

def self.pretty_name
  @pretty_name ||= self.to_s.split('::').last
end

.service_nameObject

Computer-friendly name of the service.



166
167
168
# File 'lib/potluck/service.rb', line 166

def self.service_name
  @service_name ||= pretty_name.downcase
end

.write_plistObject

Writes the service’s launchctl plist file to disk.



209
210
211
212
# File 'lib/potluck/service.rb', line 209

def self.write_plist
  FileUtils.mkdir_p(File.dirname(plist_path))
  File.write(plist_path, plist)
end

Instance Method Details

#log(message, error = false) ⇒ Object

Logs a message using the logger or stdout/stderr if no logger is configured.

  • message - Message to log.

  • error - True if the message is an error (default: false).



148
149
150
151
152
153
154
# File 'lib/potluck/service.rb', line 148

def log(message, error = false)
  if @logger
    error ? @logger.error(message) : @logger.info(message)
  else
    error ? $stderr.puts(message) : $stdout.puts(message)
  end
end

#manage?Boolean

Returns true if the service is managed.

Returns:

  • (Boolean)


46
47
48
# File 'lib/potluck/service.rb', line 46

def manage?
  @manage
end

#manage_with_launchctl?Boolean

Returns true if the service is managed via launchctl.

Returns:

  • (Boolean)


53
54
55
# File 'lib/potluck/service.rb', line 53

def manage_with_launchctl?
  @manage_with_launchctl
end

#restartObject

Restarts the service if it’s managed by calling stop and then start.



116
117
118
119
120
121
# File 'lib/potluck/service.rb', line 116

def restart
  return unless manage?

  stop
  start
end

#run(command, capture_stderr: true) ⇒ Object

Runs a command with the default shell. Raises an error if the command exits with a non-zero status.

  • command - Command to run.

  • capture_stderr - True if stderr should be redirected to stdout; otherwise stderr output will not be logged (default: true).



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/potluck/service.rb', line 130

def run(command, capture_stderr: true)
  output = `#{command}#{' 2>&1' if capture_stderr}`
  status = $?

  if status != 0
    output.split("\n").each { |line| log(line, :error) }
    raise(ServiceError, "Command exited with status #{status.exitstatus}: #{command}")
  else
    output
  end
end

#startObject

Starts the service if it’s managed and is not active.

Raises:



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/potluck/service.rb', line 81

def start
  return unless manage?

  case status
  when :error then stop
  when :active then return
  end

  self.class.write_plist if manage_with_launchctl?
  run(start_command)
  wait { status == :inactive }

  raise(ServiceError, "Could not start #{self.class.pretty_name}") if status != :active

  log("#{self.class.pretty_name} started")
end

#statusObject

Returns the status of the service:

  • :active if the service is managed and running.

  • :inactive if the service is not managed or is not running.

  • :error if the service is managed and is in an error state.



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/potluck/service.rb', line 64

def status
  return :inactive unless manage?

  output = `#{status_command}`

  if $? != 0
    :inactive
  elsif status_error_regex && output[status_error_regex]
    :error
  else
    :active
  end
end

#stopObject

Stops the service if it’s managed and is active or in an error state.

Raises:



101
102
103
104
105
106
107
108
109
110
111
# File 'lib/potluck/service.rb', line 101

def stop
  return unless manage? && status != :inactive

  self.class.write_plist if manage_with_launchctl?
  run(stop_command)
  wait { status != :inactive }

  raise(ServiceError, "Could not stop #{self.class.pretty_name}") if status != :inactive

  log("#{self.class.pretty_name} stopped")
end