sockd 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d25ce828b55583f286558554e0fa7606403bdce4
4
- data.tar.gz: c2d8410fe27052b5632652d89eb6076c3246e261
3
+ metadata.gz: 6eac67e5bf5c221e1246054c67fe114d0e1ce125
4
+ data.tar.gz: cd0fca78d6544e1b4badca98c291313bb2c3955f
5
5
  SHA512:
6
- metadata.gz: b030f1bd71ad93b5bb32ba1de2fd2d23e095a9c0e73b260bc8558e7c700f1b14508407750162082b17a55c06045d052e4a2dece1afd4b02c104053c019dfd148
7
- data.tar.gz: 58ce09e8c91b0fc8d269315cf29d6aeed1e896cc03e13e41df6cfbc63f3466b52621ee80d62d7051de6e4bbdcf4b4767a32bb269e51f5ec6644f4e04c3428003
6
+ metadata.gz: 4dee986312d3761ef7854df1a6f6937ddf3d3a9a086969ea056ec396334d4fb4f4c7e32bcbc3f94c174e019da22d83cc5e32a0232a40dd21797abfeaa9b2652b
7
+ data.tar.gz: 92af3f8162ba59946f266c01198c3b4fd08814a4fbba27d1b465989a57b3f1d3f80fbfa19ddfe4f4d6ffd15f4734d07e1916b6ab172f21d315b551e2f21a8bf0
data/lib/sockd/runner.rb CHANGED
@@ -1,22 +1,20 @@
1
- require "logger"
2
1
  require "socket"
3
2
  require "timeout"
4
3
  require "fileutils"
5
- require "sockd/errors"
6
4
 
7
5
  module Sockd
8
6
  class Runner
9
7
 
8
+ class ServiceError < RuntimeError; end
9
+
10
10
  attr_reader :options, :name
11
11
 
12
12
  class << self
13
- def define(*args, &block)
14
- self.new(*args, &block)
15
- end
13
+ alias define new
16
14
  end
17
15
 
18
- def initialize(name, options = {}, &block)
19
- @name = name
16
+ def initialize(name = nil, options = {}, &block)
17
+ @name = name || File.basename($0)
20
18
  @options = {
21
19
  :host => "127.0.0.1",
22
20
  :port => 0,
@@ -66,33 +64,24 @@ module Sockd
66
64
  # @runner.handle { |msg| if msg == 'foo' then return 'bar' ... }
67
65
  def handle(message = nil, socket = nil, &block)
68
66
  return self if block_given? && @handle = block
69
- @handle || (raise SockdError, "No message handler provided.")
67
+ raise ArgumentError, "no message handler provided" unless @handle
70
68
  @handle.call(message, socket)
71
69
  end
72
70
 
73
- # call one of start, stop, restart, or send
74
- def run(method, *args)
75
- if %w(start stop restart send).include?(method)
76
- begin
77
- self.public_send method.to_sym, *args
78
- rescue ArgumentError => e
79
- raise unless e.backtrace[1].include? "in `public_send"
80
- raise BadCommandError, "wrong number of arguments for command: #{method}"
81
- end
82
- else
83
- raise BadCommandError, "invalid command: #{method}"
84
- end
85
- end
86
-
87
71
  # start our service
88
72
  def start
89
73
  server do |server|
90
74
 
91
75
  if options[:daemonize]
92
76
  pid = daemon_running?
93
- raise ProcError, "#{name} process already running (#{pid})" if pid
77
+ raise ServiceError, "#{name} process already running (#{pid})" if pid
94
78
  puts "starting #{name} process..."
95
- return self unless daemonize
79
+ unless daemonize
80
+ unless send('ping', 10).chomp == 'pong'
81
+ raise ServiceError, "invalid ping response"
82
+ end
83
+ return self
84
+ end
96
85
  end
97
86
 
98
87
  drop_privileges options[:user], options[:group]
@@ -106,7 +95,8 @@ module Sockd
106
95
  exit 130
107
96
  end
108
97
 
109
- log "listening on " + server.local_address.inspect_sockaddr
98
+ log "listening on #{server.local_address.inspect_sockaddr}"
99
+
110
100
  while true
111
101
  sock = server.accept
112
102
  begin
@@ -139,7 +129,7 @@ module Sockd
139
129
  Process.kill('KILL', pid)
140
130
  puts "SIGKILL sent to #{name} (#{pid})"
141
131
  end
142
- raise ProcError.new("unable to stop #{name} process") if daemon_running?
132
+ raise ServiceError, "unable to stop #{name} process" if daemon_running?
143
133
  else
144
134
  warn "#{name} process not running"
145
135
  end
@@ -153,22 +143,16 @@ module Sockd
153
143
  end
154
144
 
155
145
  # send a message to a running service and return the response
156
- def send(*args)
157
- raise ArgumentError if args.empty?
158
- message = args.join(' ')
159
- response = nil
160
- begin
161
- client do |sock|
162
- sock.write message + "\r\n"
163
- response = sock.gets
164
- end
165
- rescue Errno::ECONNREFUSED, Errno::ENOENT
166
- unless daemon_running?
167
- abort "#{name} process not running"
168
- end
169
- abort "unable to establish connection"
146
+ def send(message, timeout = 30)
147
+ client do |sock|
148
+ sock.write "#{message}\r\n"
149
+ ready = IO.select([sock], nil, nil, timeout)
150
+ raise ServiceError, "timed out waiting for server response" unless ready
151
+ sock.recv(256)
170
152
  end
171
- puts response
153
+ rescue Errno::ECONNREFUSED, Errno::ENOENT
154
+ raise ServiceError, "#{name} process not running" unless daemon_running?
155
+ raise ServiceError, "unable to establish connection"
172
156
  end
173
157
 
174
158
  # output a timestamped log message
@@ -185,29 +169,23 @@ module Sockd
185
169
  UNIXServer.new(options[:socket])
186
170
  rescue Errno::EADDRINUSE
187
171
  begin
188
- Timeout.timeout(5) do
189
- UNIXSocket.open(options[:socket]) do |sock|
190
- sock.write "ping\r\n"
191
- if sock.gets.chomp == "pong"
192
- raise ProcError, "socket #{options[:socket]} already in use by another instance of #{name}"
193
- end
194
- end
195
- end
196
- raise ProcError, "socket #{options[:socket]} already in use by another process"
197
- rescue Errno::ECONNREFUSED, Timeout::Error
172
+ send('ping', 20)
173
+ rescue ServiceError
174
+ # socket stale, reopening
198
175
  File.delete(options[:socket])
199
176
  UNIXServer.new(options[:socket])
177
+ else
178
+ raise ServiceError, "socket #{options[:socket]} already in use by another process"
200
179
  end
201
180
  end.tap do
202
181
  # get user and group ids
203
- uid = Etc.getpwnam(options[:user]).uid if options[:user]
204
- gid = Etc.getgrnam(options[:group]).gid if options[:group]
205
- gid = Etc.getpwnam(options[:user]).gid if !gid && options[:user]
182
+ uid, gid = user_id(options[:user]) if options[:user]
183
+ gid = group_id(options[:group]) if options[:group]
206
184
  File.chown(uid, gid, options[:socket]) if uid || gid
207
185
 
208
186
  # ensure mode is octal if string provided
209
187
  options[:mode] = options[:mode].to_i(8) if options[:mode].is_a?(String)
210
- File.chmod(options[:mode], options[:socket])
188
+ File.chmod(options[:mode], options[:socket]) if options[:mode] != 0
211
189
  end
212
190
  else
213
191
  TCPServer.new(options[:host], options[:port])
@@ -219,7 +197,7 @@ module Sockd
219
197
  end
220
198
  rescue Errno::EACCES
221
199
  sock = options[:socket] || "#{options[:host]}:#{options[:port]}"
222
- raise ProcError, "unable to open socket: #{sock} (check permissions)"
200
+ raise ServiceError, "unable to open socket: #{sock} (check permissions)"
223
201
  end
224
202
 
225
203
  # return a UNIXSocket or TCPSocket instance depending on config
@@ -231,7 +209,7 @@ module Sockd
231
209
  end
232
210
  rescue Errno::EACCES
233
211
  sock = options[:socket] || "#{options[:host]}:#{options[:port]}"
234
- raise ProcError, "unable to open socket: #{sock} (check permissions)"
212
+ raise ServiceError, "unable to open socket: #{sock} (check permissions)"
235
213
  end
236
214
 
237
215
  # handle process termination signals
@@ -271,7 +249,7 @@ module Sockd
271
249
 
272
250
  Process.waitpid
273
251
  unless wait_until { daemon_running? }
274
- raise ProcError, "failed to start #{@name} service"
252
+ raise ServiceError, "failed to start #{name} service"
275
253
  end
276
254
  end
277
255
 
@@ -291,15 +269,13 @@ module Sockd
291
269
 
292
270
  # drop privileges to the specified user and group
293
271
  def drop_privileges(user, group)
294
- uid = Etc.getpwnam(user).uid if user
295
- gid = Etc.getgrnam(group).gid if group
296
- gid = Etc.getpwnam(user).gid if group.nil? && user
272
+ uid, gid = user_id(user) if user
273
+ gid = group_id(group) if group
297
274
 
298
275
  Process::Sys.setgid(gid) if gid
299
276
  Process::Sys.setuid(uid) if uid
300
- rescue ArgumentError, Errno::EPERM => e
301
- # user or group does not exist
302
- raise ProcError, "unable to drop privileges (#{e})"
277
+ rescue Errno::EPERM => e
278
+ raise ServiceError, "unable to drop privileges (#{e})"
303
279
  end
304
280
 
305
281
  # redirect our output as per configuration
@@ -329,7 +305,7 @@ module Sockd
329
305
  rescue Errno::EACCES, Errno::EISDIR
330
306
  end
331
307
  unless File.file?(path) && File.writable?(path)
332
- raise ProcError, "unable to open file: #{path} (check permissions)"
308
+ raise ServiceError, "unable to open file: #{path} (check permissions)"
333
309
  end
334
310
  path
335
311
  end
@@ -341,5 +317,18 @@ module Sockd
341
317
  end
342
318
  timer > 0
343
319
  end
320
+
321
+ def user_id(user)
322
+ user = Etc.getpwnam(user)
323
+ [user.uid, user.gid]
324
+ rescue ArgumentError
325
+ raise ServiceError, "unable to find user: #{user}"
326
+ end
327
+
328
+ def group_id(group)
329
+ Etc.getgrnam(group).gid
330
+ rescue ArgumentError
331
+ raise ServiceError, "unable to find group: #{user}"
332
+ end
344
333
  end
345
334
  end
data/lib/sockd/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sockd
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/sockd.rb CHANGED
@@ -1,32 +1,176 @@
1
- require "sockd/errors"
2
- require "sockd/optparse"
1
+ require "yaml"
2
+ require "optparse"
3
3
  require "sockd/runner"
4
4
  require "sockd/version"
5
5
 
6
6
  module Sockd
7
7
 
8
- # instantiate a new sockd service
9
- def self.define(name, options = {}, &block)
10
- Runner.define(name, options, &block)
11
- end
8
+ class ParseError < OptionParser::ParseError; end
9
+ class ConfigFileError < RuntimeError; end
12
10
 
13
- # instantiate command line option parser
14
- def self.optparse(name, defaults = {}, &block)
15
- OptionParser.new(name, defaults, &block)
16
- end
11
+ class << self
12
+
13
+ def define(name = nil, options = {}, &block)
14
+ Runner.define(name = nil, options, &block)
15
+ end
16
+
17
+ alias new define
18
+
19
+ def run(name = nil, options = {}, &block)
20
+ parse define(name, options, &block)
21
+ end
22
+
23
+ def parse(runner, argv = ARGV, &block)
24
+ raise ArgumentError, 'You must provide an instance of Sockd::Runner' unless runner.class <= Runner
25
+ options = {}
26
+ parser = optparser(runner.name, options, &block)
27
+ command, *message = parser.parse(argv)
28
+
29
+ if options[:config_save]
30
+ save_path = options[:config_path] || runner.options[:config_path]
31
+ raise ParseError, 'no config file path specified, unable to save' unless save_path
32
+ save_yaml options, save_path
33
+
34
+ puts "config saved to: #{path}"
35
+ exit
36
+ end
37
+
38
+ if options[:config_path]
39
+ read_yaml options, options[:config_path]
40
+ elsif runner.options[:config_path] && File.file?(runner.options[:config_path])
41
+ read_yaml options, runner.options[:config_path]
42
+ end
43
+
44
+ runner.options.merge! options
45
+
46
+ case command
47
+ when nil
48
+ runner.options[:daemonize] = false
49
+ runner.start
50
+ when 'start', 'stop', 'restart'
51
+ raise ParseError, "invalid arguments for #{command}" unless message.empty?
52
+ runner.public_send command.to_sym
53
+ else
54
+ message.unshift command unless command == 'send'
55
+ raise ParseError, 'no message provided' if message.empty?
56
+ puts runner.send message.join(' ')
57
+ end
58
+ rescue OptionParser::ParseError => e
59
+ puts "Error: #{e.message}"
60
+ puts parser
61
+ puts ''
62
+ exit 1
63
+ rescue ConfigFileError, Runner::ServiceError => e
64
+ puts "Error: #{e.message}"
65
+ exit 1
66
+ end
67
+
68
+ private
69
+
70
+ def optparser(name, options)
71
+ OptionParser.new do |opts|
72
+ opts.program_name = name
73
+ opts.summary_width = 25
74
+ opts.banner = <<-EOF.gsub /^[ ]{8}/, ''
75
+
76
+ Usage: #{name} [options] <command> [<message>]
77
+
78
+ Commands:
79
+ #{name} run server without forking
80
+ #{name} start start as a daemon
81
+ #{name} stop [-f] stop a running daemon
82
+ #{name} restart stop, then start the daemon
83
+ #{name} send <message> send a message to a running daemon
84
+ #{name} <message> send a message (send command implied)
85
+
86
+ Options:
87
+ EOF
88
+
89
+ # allow user to specify custom options
90
+ yield opts, options if block_given?
91
+
92
+ opts.on('-p', '--port PORT', String, 'Listen on TCP port PORT') do |port|
93
+ options[:port] = port
94
+ options[:socket] = nil
95
+ end
96
+
97
+ opts.on('-H', '--host HOST', String, 'Listen on HOST') do |host|
98
+ options[:host] = host
99
+ options[:socket] = nil
100
+ end
101
+
102
+ opts.on('-s', '--socket PATH', String,
103
+ 'Listen on Unix domain socket PATH (disables TCP support)') do |path|
104
+ options[:socket] = File.expand_path(path)
105
+ end
106
+
107
+ opts.on('-m', '--mode MODE', OptionParser::OctalInteger,
108
+ 'Set file permissions when using Unix socket') do |mode|
109
+ options[:mode] = mode
110
+ end
111
+
112
+ opts.on('-P', '--pid PATH', String, 'Where to write the PID file') do |path|
113
+ options[:pid_path] = File.expand_path(path)
114
+ end
115
+
116
+ opts.on('-l', '--log PATH', String, 'Where to write the log file') do |path|
117
+ options[:log_path] = File.expand_path(path)
118
+ end
119
+
120
+ opts.on('-u', '--user USER', String,
121
+ 'Assume the identity of USER when running as a daemon') do |user|
122
+ options[:user] = user
123
+ end
124
+
125
+ opts.on('-g', '--group GROUP', String,
126
+ 'Assume group GROUP when running as a daemon') do |group|
127
+ options[:group] = group
128
+ end
129
+
130
+ opts.on('-f', '--force',
131
+ 'Force kill if SIGTERM fails when running "stop" command') do
132
+ options[:force] = true
133
+ end
134
+
135
+ opts.separator ''
136
+ opts.separator 'Additional Options:'
137
+
138
+ opts.on_tail('--config PATH', String,
139
+ 'Load default parameters from YAML file at PATH') do |path|
140
+ options[:config_path] = File.expand_path(path)
141
+ end
142
+
143
+ opts.on_tail('--save', 'Save current parameters into a config file') do
144
+ options[:config_save] = true
145
+ end
146
+
147
+ opts.on_tail('-h', '--help', 'Display this usage information') do
148
+ puts opts
149
+ puts ''
150
+ exit
151
+ end
152
+ end
153
+ end
154
+
155
+ def read_yaml(options, path)
156
+ config = YAML.load_file(path).merge!(options)
157
+ options.replace(config)
158
+ rescue Errno::EACCES, Errno::EISDIR => e
159
+ raise ConfigFileError, "unable to read config (#{e.message})"
160
+ end
161
+
162
+ def save_yaml(options, path)
163
+ options[:mode] = sprintf('0%o', options[:mode]) if options[:mode]
164
+ options.delete(:config_path)
165
+ options.delete(:config_save)
17
166
 
18
- # instantiate and run a sockd service using command line arguments
19
- def self.run(name, options = {}, &block)
20
- runner = define(name, options, &block)
21
- parser = optparse(runner.name, runner.options)
22
- argv = parser.parse!
23
- runner.run(*argv)
24
- rescue OptionParserError, BadCommandError => e
25
- warn "Error: #{e.message}"
26
- warn "#{parser}\n"
27
- exit
28
- rescue SockdError => e
29
- warn "Error: #{e.message}"
30
- exit
167
+ FileUtils.mkdir_p(File.dirname(path), mode: 0755)
168
+ File.open(path, 'w') do |file|
169
+ file.write options.to_yaml
170
+ end
171
+ File.chmod(0644, path)
172
+ rescue Errno::EACCES, Errno::EISDIR => e
173
+ raise ConfigFileError, "unable to save config (#{e.message})"
174
+ end
31
175
  end
32
176
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sockd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Greiling
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-14 00:00:00.000000000 Z
11
+ date: 2015-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -53,8 +53,6 @@ files:
53
53
  - examples/listd.rb
54
54
  - examples/piglatin.rb
55
55
  - lib/sockd.rb
56
- - lib/sockd/errors.rb
57
- - lib/sockd/optparse.rb
58
56
  - lib/sockd/runner.rb
59
57
  - lib/sockd/version.rb
60
58
  - sockd.gemspec
data/lib/sockd/errors.rb DELETED
@@ -1,15 +0,0 @@
1
- module Sockd
2
-
3
- class SockdError < StandardError
4
- end
5
-
6
- class OptionParserError < SockdError
7
- end
8
-
9
- class BadCommandError < SockdError
10
- end
11
-
12
- class ProcError < SockdError
13
- end
14
-
15
- end
@@ -1,121 +0,0 @@
1
- require "fileutils"
2
- require "optparse"
3
- require "shellwords"
4
- require "sockd/errors"
5
-
6
- module Sockd
7
- class OptionParser
8
-
9
- attr_accessor :name, :options, :callback
10
-
11
- def initialize(name = nil, defaults = {}, &block)
12
- @name = name || File.basename($0)
13
- @options = defaults.replace({
14
- host: "127.0.0.1",
15
- port: 0,
16
- socket: false,
17
- mode: 0660,
18
- daemonize: true,
19
- pid_path: "/var/run/#{safe_name}.pid",
20
- log_path: false,
21
- force: false,
22
- user: false,
23
- group: false
24
- }.merge(defaults))
25
- @callback = block if block_given?
26
- end
27
-
28
- def safe_name
29
- name.gsub(/(^[0-9]*|[^0-9a-z])/i, '')
30
- end
31
-
32
- def parser
33
- @parser ||= ::OptionParser.new do |opts|
34
- opts.summary_width = 25
35
- opts.banner = <<-EOF.gsub /^[ ]{8}/, ''
36
- Usage: #{name} [options] <command> [<message>]
37
-
38
- Commands:
39
- #{name} run server without forking
40
- #{name} start start as a daemon
41
- #{name} stop [-f] stop a running daemon
42
- #{name} restart stop, then start the daemon
43
- #{name} send <message> send a message to a running daemon
44
- #{name} <message> send a message (send command implied)
45
-
46
- Options:
47
- EOF
48
-
49
- instance_exec(opts, &callback) if callback
50
-
51
- opts.on("-p", "--port PORT", String, "Listen on TCP port PORT (default: #{options[:port]})") do |x|
52
- options[:port] = x
53
- # prefer TCP connection if explicitly setting a port
54
- options[:socket] = nil
55
- end
56
-
57
- opts.on("-H", "--host HOST", String, "Listen on HOST (default: #{options[:host]})") do |x|
58
- options[:host] = x
59
- # prefer TCP connection if explicitly setting a host
60
- options[:socket] = nil
61
- end
62
-
63
- opts.on("-s", "--socket SOCKET", String, "Listen on Unix socket path (disables network support)", "(default: #{options[:socket]})") do |x|
64
- options[:socket] = File.expand_path(x)
65
- end
66
-
67
- opts.on("-m", "--mode MODE", String, "Set file permissions when using Unix socket", "(default: #{options[:mode]})") do |x|
68
- options[:mode] = x
69
- end
70
-
71
- opts.on("-P", "--pid FILE", String, "Where to write the PID file", "(default: #{options[:pid_path]})") do |x|
72
- options[:pid_path] = File.expand_path(x)
73
- end
74
-
75
- opts.on("-l", "--log FILE", String, "Where to write the log file", "(default: #{options[:log_path]})") do |x|
76
- options[:log_path] = File.expand_path(x)
77
- end
78
-
79
- opts.on("-u", "--user USER", String, "Assume the identity of USER when running as a daemon", "(default: #{options[:user]})") do |x|
80
- options[:user] = x
81
- end
82
-
83
- opts.on("-g", "--group GROUP", String, "Assume group GROUP when running as a daemon", "(default: #{options[:group]})") do |x|
84
- options[:group] = x
85
- end
86
-
87
- opts.on("-f", "--force", String, "Force kill if SIGTERM fails when running 'stop' command") do
88
- options[:force] = true
89
- end
90
-
91
- opts.separator "\n Additional Options:"
92
-
93
- opts.on_tail("-h", "--help", "Display this usage information.") do
94
- puts "\n#{opts}\n"
95
- exit
96
- end
97
- end
98
- end
99
-
100
- def parse!(argv = nil)
101
- argv ||= ARGV.dup
102
- argv = Shellwords.shellwords argv if argv.is_a? String
103
-
104
- parser.parse! argv
105
-
106
- if argv.empty?
107
- argv.push 'start'
108
- options[:daemonize] = false
109
- end
110
- argv.unshift 'send' unless %w(start stop restart send).include?(argv.first)
111
-
112
- argv
113
- rescue ::OptionParser::InvalidOption, ::OptionParser::MissingArgument => e
114
- raise OptionParserError.new e
115
- end
116
-
117
- def to_s
118
- parser.to_s
119
- end
120
- end
121
- end