sockd 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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