puma 6.0.0 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

data/lib/puma/request.rb CHANGED
@@ -14,15 +14,18 @@ module Puma
14
14
  #
15
15
  module Request # :nodoc:
16
16
 
17
- # determines whether to write body to io_buffer first, or straight to socket
18
- # also fixes max size of chunked body read when bosy is an IO.
19
- BODY_LEN_MAX = 1_024 * 256
17
+ # Single element array body: smaller bodies are written to io_buffer first,
18
+ # then a single write from io_buffer. Larger sizes are written separately.
19
+ # Also fixes max size of chunked file body read.
20
+ BODY_LEN_MAX = 1_024 * 256
20
21
 
21
- # size divide for using copy_stream on body
22
+ # File body: smaller bodies are combined with io_buffer, then written to
23
+ # socket. Larger bodies are written separately using `copy_stream`
22
24
  IO_BODY_MAX = 1_024 * 64
23
25
 
24
- # max size for io_buffer, force write when exceeded
25
- IO_BUFFER_LEN_MAX = 1_024 * 1_024 * 4
26
+ # Array body: elements are collected in io_buffer. When io_buffer's size
27
+ # exceeds value, they are written to the socket.
28
+ IO_BUFFER_LEN_MAX = 1_024 * 512
26
29
 
27
30
  SOCKET_WRITE_ERR_MSG = "Socket timeout writing data"
28
31
 
@@ -41,16 +44,22 @@ module Puma
41
44
  #
42
45
  # Finally, it'll return +true+ on keep-alive connections.
43
46
  # @param client [Puma::Client]
44
- # @param io_buffer [Puma::IOBuffer]
45
47
  # @param requests [Integer]
46
48
  # @return [Boolean,:async]
47
49
  #
48
- def handle_request(client, io_buffer, requests)
50
+ def handle_request(client, requests)
49
51
  env = client.env
52
+ io_buffer = client.io_buffer
50
53
  socket = client.io # io may be a MiniSSL::Socket
54
+ app_body = nil
55
+
51
56
 
52
57
  return false if closed_socket?(socket)
53
58
 
59
+ if client.http_content_length_limit_exceeded
60
+ return prepare_response(413, {}, ["Payload Too Large"], requests, client)
61
+ end
62
+
54
63
  normalize_env env, client
55
64
 
56
65
  env[PUMA_SOCKET] = socket
@@ -85,14 +94,19 @@ module Puma
85
94
 
86
95
  begin
87
96
  if SUPPORTED_HTTP_METHODS.include?(env[REQUEST_METHOD])
88
- status, headers, res_body = @thread_pool.with_force_shutdown do
97
+ status, headers, app_body = @thread_pool.with_force_shutdown do
89
98
  @app.call(env)
90
99
  end
91
100
  else
92
101
  @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
93
- status, headers, res_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
102
+ status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
94
103
  end
95
104
 
105
+ # app_body needs to always be closed, hold value in case lowlevel_error
106
+ # is called
107
+ res_body = app_body
108
+
109
+ # full hijack, app called env['rack.hijack']
96
110
  return :async if client.hijacked
97
111
 
98
112
  status = status.to_i
@@ -114,114 +128,136 @@ module Puma
114
128
 
115
129
  status, headers, res_body = lowlevel_error(e, env, 500)
116
130
  end
117
- prepare_response(status, headers, res_body, io_buffer, requests, client)
131
+ prepare_response(status, headers, res_body, requests, client)
132
+ ensure
133
+ io_buffer.reset
134
+ uncork_socket client.io
135
+ app_body.close if app_body.respond_to? :close
136
+ client.tempfile&.unlink
137
+ after_reply = env[RACK_AFTER_REPLY] || []
138
+ begin
139
+ after_reply.each { |o| o.call }
140
+ rescue StandardError => e
141
+ @log_writer.debug_error e
142
+ end unless after_reply.empty?
118
143
  end
119
144
 
120
145
  # Assembles the headers and prepares the body for actually sending the
121
- # response via #fast_write_response.
146
+ # response via `#fast_write_response`.
122
147
  #
123
148
  # @param status [Integer] the status returned by the Rack application
124
149
  # @param headers [Hash] the headers returned by the Rack application
125
- # @param app_body [Array] the body returned by the Rack application or
126
- # a call to `lowlevel_error`
127
- # @param io_buffer [Puma::IOBuffer] modified in place
150
+ # @param res_body [Array] the body returned by the Rack application or
151
+ # a call to `Server#lowlevel_error`
128
152
  # @param requests [Integer] number of inline requests handled
129
153
  # @param client [Puma::Client]
130
- # @return [Boolean,:async]
131
- def prepare_response(status, headers, app_body, io_buffer, requests, client)
154
+ # @return [Boolean,:async] keep-alive status or `:async`
155
+ def prepare_response(status, headers, res_body, requests, client)
132
156
  env = client.env
133
- socket = client.io
134
-
135
- after_reply = env[RACK_AFTER_REPLY] || []
157
+ socket = client.io
158
+ io_buffer = client.io_buffer
136
159
 
137
160
  return false if closed_socket?(socket)
138
161
 
139
- resp_info = str_headers(env, status, headers, app_body, io_buffer, requests, client)
140
-
141
- # below converts app_body into body, dependent on app_body's characteristics, and
142
- # resp_info[:content_length] will be set if it can be determined
143
- if !resp_info[:content_length] && !resp_info[:transfer_encoding] && status != 204
144
- if app_body.respond_to?(:to_ary)
145
- length = 0
146
- if array_body = app_body.to_ary
147
- body = array_body.map { |part| length += part.bytesize; part }
148
- elsif app_body.is_a?(::File) && app_body.respond_to?(:size)
149
- length = app_body.size
150
- elsif app_body.respond_to?(:each)
151
- body = []
152
- app_body.each { |part| length += part.bytesize; body << part }
162
+ # Close the connection after a reasonable number of inline requests
163
+ # if the server is at capacity and the listener has a new connection ready.
164
+ # This allows Puma to service connections fairly when the number
165
+ # of concurrent connections exceeds the size of the threadpool.
166
+ force_keep_alive = requests < @max_fast_inline ||
167
+ @thread_pool.busy_threads < @max_threads ||
168
+ !client.listener.to_io.wait_readable(0)
169
+
170
+ resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
171
+
172
+ close_body = false
173
+ response_hijack = nil
174
+ content_length = resp_info[:content_length]
175
+ keep_alive = resp_info[:keep_alive]
176
+
177
+ if res_body.respond_to?(:each) && !resp_info[:response_hijack]
178
+ # below converts app_body into body, dependent on app_body's characteristics, and
179
+ # content_length will be set if it can be determined
180
+ if !content_length && !resp_info[:transfer_encoding] && status != 204
181
+ if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
182
+ array_body.is_a?(Array)
183
+ body = array_body.compact
184
+ content_length = body.sum(&:bytesize)
185
+ elsif res_body.is_a?(File) && res_body.respond_to?(:size)
186
+ body = res_body
187
+ content_length = body.size
188
+ elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
189
+ body = File.open fn, 'rb'
190
+ content_length = body.size
191
+ close_body = true
192
+ else
193
+ body = res_body
153
194
  end
154
- resp_info[:content_length] = length
155
- elsif app_body.is_a?(File) && app_body.respond_to?(:size)
156
- resp_info[:content_length] = app_body.size
157
- body = app_body
158
- elsif app_body.respond_to?(:to_path) && app_body.respond_to?(:each) &&
159
- File.readable?(fn = app_body.to_path)
195
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
196
+ File.readable?(fn = res_body.to_path)
160
197
  body = File.open fn, 'rb'
161
- resp_info[:content_length] = body.size
198
+ content_length = body.size
199
+ close_body = true
200
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
201
+ res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
202
+ # Sprockets::Asset
203
+ content_length = res_body.bytesize unless content_length
204
+ if (body_str = res_body.to_hash[:source])
205
+ body = [body_str]
206
+ else # avoid each and use a File object
207
+ body = File.open fn, 'rb'
208
+ close_body = true
209
+ end
162
210
  else
163
- body = app_body
211
+ body = res_body
164
212
  end
165
- elsif !app_body.is_a?(::File) && app_body.respond_to?(:to_path) && app_body.respond_to?(:each) &&
166
- File.readable?(fn = app_body.to_path)
167
- body = File.open fn, 'rb'
168
- resp_info[:content_length] = body.size
169
213
  else
170
- body = app_body
214
+ # partial hijack, from Rack spec:
215
+ # Servers must ignore the body part of the response tuple when the
216
+ # rack.hijack response header is present.
217
+ response_hijack = resp_info[:response_hijack] || res_body
171
218
  end
172
219
 
173
220
  line_ending = LINE_END
174
221
 
175
- content_length = resp_info[:content_length]
176
- keep_alive = resp_info[:keep_alive]
177
-
178
- if app_body && !app_body.respond_to?(:each)
179
- response_hijack = app_body
180
- else
181
- response_hijack = resp_info[:response_hijack]
182
- end
183
-
184
222
  cork_socket socket
185
223
 
186
224
  if resp_info[:no_body]
187
- if content_length and status != 204
225
+ # 101 (Switching Protocols) doesn't return here or have content_length,
226
+ # it should be using `response_hijack`
227
+ unless status == 101
228
+ if content_length && status != 204
229
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
230
+ end
231
+
232
+ io_buffer << LINE_END
233
+ fast_write_str socket, io_buffer.read_and_reset
234
+ socket.flush
235
+ return keep_alive
236
+ end
237
+ else
238
+ if content_length
188
239
  io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
240
+ chunked = false
241
+ elsif !response_hijack && resp_info[:allow_chunked]
242
+ io_buffer << TRANSFER_ENCODING_CHUNKED
243
+ chunked = true
189
244
  end
190
-
191
- io_buffer << LINE_END
192
- fast_write_str socket, io_buffer.to_s
193
- return keep_alive
194
- end
195
- if content_length
196
- io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
197
- chunked = false
198
- elsif !response_hijack and resp_info[:allow_chunked]
199
- io_buffer << TRANSFER_ENCODING_CHUNKED
200
- chunked = true
201
245
  end
202
246
 
203
247
  io_buffer << line_ending
204
248
 
249
+ # partial hijack, we write headers, then hand the socket to the app via
250
+ # response_hijack.call
205
251
  if response_hijack
206
- fast_write_str socket, io_buffer.to_s
252
+ fast_write_str socket, io_buffer.read_and_reset
253
+ uncork_socket socket
207
254
  response_hijack.call socket
208
255
  return :async
209
256
  end
210
257
 
211
258
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
259
+ body.close if close_body
212
260
  keep_alive
213
- ensure
214
- io_buffer.reset
215
- resp_info = nil
216
- uncork_socket socket
217
- app_body.close if app_body.respond_to? :close
218
- client.tempfile&.unlink
219
-
220
- begin
221
- after_reply.each { |o| o.call }
222
- rescue StandardError => e
223
- @log_writer.debug_error e
224
- end unless after_reply.empty?
225
261
  end
226
262
 
227
263
  # @param env [Hash] see Puma::Client#env, from request
@@ -237,10 +273,10 @@ module Puma
237
273
 
238
274
  # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
239
275
  # and body segments (called by `fast_write_response`).
240
- # Writes a string to an io (normally `Client#io`) using `write_nonblock`.
276
+ # Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
241
277
  # Large strings may not be written in one pass, especially if `io` is a
242
278
  # `MiniSSL::Socket`.
243
- # @param io [#write_nonblock] the io to write to
279
+ # @param socket [#write_nonblock] the request/response socket
244
280
  # @param str [String] the string written to the io
245
281
  # @raise [ConnectionError]
246
282
  #
@@ -249,7 +285,7 @@ module Puma
249
285
  byte_size = str.bytesize
250
286
  while n < byte_size
251
287
  begin
252
- n += socket.syswrite(n.zero? ? str : str.byteslice(n..-1))
288
+ n += socket.write_nonblock(n.zero? ? str : str.byteslice(n..-1))
253
289
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
254
290
  unless socket.wait_writable WRITE_TIMEOUT
255
291
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
@@ -267,39 +303,41 @@ module Puma
267
303
  # @param socket [#write] the response socket
268
304
  # @param body [Enumerable, File] the body object
269
305
  # @param io_buffer [Puma::IOBuffer] contains headers
270
- # @param chunk [Boolean]
306
+ # @param chunked [Boolean]
307
+ # @paramn content_length [Integer
271
308
  # @raise [ConnectionError]
272
309
  #
273
310
  def fast_write_response(socket, body, io_buffer, chunked, content_length)
274
- if body.is_a?(::File) || body.respond_to?(:read) || body.respond_to?(:readpartial)
311
+ if body.is_a?(::File) && body.respond_to?(:read)
275
312
  if chunked # would this ever happen?
276
- while part = body.read(BODY_LEN_MAX)
277
- io_buffer.append part.bytesize.to_s(16), LINE_END, part, LINE_END
313
+ while chunk = body.read(BODY_LEN_MAX)
314
+ io_buffer.append chunk.bytesize.to_s(16), LINE_END, chunk, LINE_END
278
315
  end
279
- io_buffer << CLOSE_CHUNKED
280
- fast_write_str socket, io_buffer.to_s
316
+ fast_write_str socket, CLOSE_CHUNKED
281
317
  else
282
318
  if content_length <= IO_BODY_MAX
283
- io_buffer.write body.sysread(content_length)
284
- fast_write_str socket, io_buffer.to_s
319
+ io_buffer.write body.read(content_length)
320
+ fast_write_str socket, io_buffer.read_and_reset
285
321
  else
286
- fast_write_str socket, io_buffer.to_s
322
+ fast_write_str socket, io_buffer.read_and_reset
287
323
  IO.copy_stream body, socket
288
324
  end
289
325
  end
290
- body.close
291
326
  elsif body.is_a?(::Array) && body.length == 1
292
- body_first = body.first
293
- if body_first.is_a?(::String) && body_first.bytesize >= BODY_LEN_MAX
327
+ body_first = nil
328
+ # using body_first = body.first causes issues?
329
+ body.each { |str| body_first ||= str }
330
+
331
+ if body_first.is_a?(::String) && body_first.bytesize < BODY_LEN_MAX
332
+ # smaller body, write to io_buffer first
333
+ io_buffer.write body_first
334
+ fast_write_str socket, io_buffer.read_and_reset
335
+ else
294
336
  # large body, write both header & body to socket
295
- fast_write_str socket, io_buffer.to_s
337
+ fast_write_str socket, io_buffer.read_and_reset
296
338
  fast_write_str socket, body_first
297
- else
298
- # smaller body, write to stream first
299
- io_buffer.write body_first
300
- fast_write_str socket, io_buffer.to_s
301
339
  end
302
- else
340
+ elsif body.is_a?(::Array)
303
341
  # for array bodies, flush io_buffer to socket when size is greater than
304
342
  # IO_BUFFER_LEN_MAX
305
343
  if chunked
@@ -307,8 +345,7 @@ module Puma
307
345
  next if (byte_size = part.bytesize).zero?
308
346
  io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
309
347
  if io_buffer.length > IO_BUFFER_LEN_MAX
310
- fast_write_str socket, io_buffer.to_s
311
- io_buffer.reset
348
+ fast_write_str socket, io_buffer.read_and_reset
312
349
  end
313
350
  end
314
351
  io_buffer.write CLOSE_CHUNKED
@@ -317,13 +354,37 @@ module Puma
317
354
  next if part.bytesize.zero?
318
355
  io_buffer.write part
319
356
  if io_buffer.length > IO_BUFFER_LEN_MAX
320
- fast_write_str socket, io_buffer.to_s
321
- io_buffer.reset
357
+ fast_write_str socket, io_buffer.read_and_reset
322
358
  end
323
359
  end
324
360
  end
325
- fast_write_str(socket, io_buffer.to_s) unless io_buffer.length.zero?
361
+ # may write last body part for non-chunked, also headers if array is empty
362
+ fast_write_str(socket, io_buffer.read_and_reset) unless io_buffer.length.zero?
363
+ else
364
+ # for enum bodies
365
+ if chunked
366
+ empty_body = true
367
+ body.each do |part|
368
+ next if part.nil? || (byte_size = part.bytesize).zero?
369
+ empty_body = false
370
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
371
+ fast_write_str socket, io_buffer.read_and_reset
372
+ end
373
+ if empty_body
374
+ io_buffer << CLOSE_CHUNKED
375
+ fast_write_str socket, io_buffer.read_and_reset
376
+ else
377
+ fast_write_str socket, CLOSE_CHUNKED
378
+ end
379
+ else
380
+ fast_write_str socket, io_buffer.read_and_reset
381
+ body.each do |part|
382
+ next if part.bytesize.zero?
383
+ fast_write_str socket, part
384
+ end
385
+ end
326
386
  end
387
+ socket.flush
327
388
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
328
389
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
329
390
  rescue Errno::EPIPE, SystemCallError, IOError
@@ -436,7 +497,7 @@ module Puma
436
497
  to_add = nil
437
498
 
438
499
  env.each do |k,v|
439
- if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
500
+ if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING"
440
501
  if to_delete
441
502
  to_delete << k
442
503
  else
@@ -496,12 +557,13 @@ module Puma
496
557
  # @param content_length [Integer,nil] content length if it can be determined from the
497
558
  # response body
498
559
  # @param io_buffer [Puma::IOBuffer] modified inn place
499
- # @param requests [Integer] number of inline requests handled
500
- # @param client [Puma::Client]
560
+ # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
561
+ # status and `@max_fast_inline`
501
562
  # @return [Hash] resp_info
502
563
  # @version 5.0.3
503
564
  #
504
- def str_headers(env, status, headers, res_body, io_buffer, requests, client)
565
+ def str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
566
+
505
567
  line_ending = LINE_END
506
568
  colon = COLON
507
569
 
@@ -544,13 +606,8 @@ module Puma
544
606
  # if running without request queueing
545
607
  resp_info[:keep_alive] &&= @queue_requests
546
608
 
547
- # Close the connection after a reasonable number of inline requests
548
- # if the server is at capacity and the listener has a new connection ready.
549
- # This allows Puma to service connections fairly when the number
550
- # of concurrent connections exceeds the size of the threadpool.
551
- resp_info[:keep_alive] &&= requests < @max_fast_inline ||
552
- @thread_pool.busy_threads < @max_threads ||
553
- !client.listener.to_io.wait_readable(0)
609
+ # see prepare_response
610
+ resp_info[:keep_alive] &&= force_keep_alive
554
611
 
555
612
  resp_info[:response_hijack] = nil
556
613
 
@@ -560,7 +617,8 @@ module Puma
560
617
  case k.downcase
561
618
  when CONTENT_LENGTH2
562
619
  next if illegal_header_value?(vs)
563
- resp_info[:content_length] = vs
620
+ # nil.to_i is 0, nil&.to_i is nil
621
+ resp_info[:content_length] = vs&.to_i
564
622
  next
565
623
  when TRANSFER_ENCODING
566
624
  resp_info[:allow_chunked] = false
data/lib/puma/runner.rb CHANGED
@@ -182,6 +182,10 @@ module Puma
182
182
  end
183
183
  end
184
184
 
185
+ def utc_iso8601(val)
186
+ "#{val.utc.strftime '%FT%T'}Z"
187
+ end
188
+
185
189
  def stats
186
190
  {
187
191
  versions: {
@@ -194,5 +198,12 @@ module Puma
194
198
  }
195
199
  }
196
200
  end
201
+
202
+ # this method call should always be guarded by `@log_writer.debug?`
203
+ def debug_loaded_extensions(str)
204
+ @log_writer.debug "────────────────────────────────── #{str}"
205
+ re_ext = /\.#{RbConfig::CONFIG['DLEXT']}\z/i
206
+ $LOADED_FEATURES.grep(re_ext).each { |f| @log_writer.debug(" #{f}") }
207
+ end
197
208
  end
198
209
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Puma
6
+ # The MIT License
7
+ #
8
+ # Copyright (c) 2017-2022 Agis Anastasopoulos
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ # this software and associated documentation files (the "Software"), to deal in
12
+ # the Software without restriction, including without limitation the rights to
13
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ # the Software, and to permit persons to whom the Software is furnished to do so,
15
+ # subject to the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be included in all
18
+ # copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ #
27
+ # This is a copy of https://github.com/agis/ruby-sdnotify as of commit cca575c
28
+ # The only changes made was "rehoming" it within the Puma module to avoid
29
+ # namespace collisions and applying standard's code formatting style.
30
+ #
31
+ # SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to
32
+ # notify systemd about state changes. Methods of this package are no-op on
33
+ # non-systemd systems (eg. Darwin).
34
+ #
35
+ # The API maps closely to the original implementation of sd_notify(3),
36
+ # therefore be sure to check the official man pages prior to using SdNotify.
37
+ #
38
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
39
+ module SdNotify
40
+ # Exception raised when there's an error writing to the notification socket
41
+ class NotifyError < RuntimeError; end
42
+
43
+ READY = "READY=1"
44
+ RELOADING = "RELOADING=1"
45
+ STOPPING = "STOPPING=1"
46
+ STATUS = "STATUS="
47
+ ERRNO = "ERRNO="
48
+ MAINPID = "MAINPID="
49
+ WATCHDOG = "WATCHDOG=1"
50
+ FDSTORE = "FDSTORE=1"
51
+
52
+ def self.ready(unset_env=false)
53
+ notify(READY, unset_env)
54
+ end
55
+
56
+ def self.reloading(unset_env=false)
57
+ notify(RELOADING, unset_env)
58
+ end
59
+
60
+ def self.stopping(unset_env=false)
61
+ notify(STOPPING, unset_env)
62
+ end
63
+
64
+ # @param status [String] a custom status string that describes the current
65
+ # state of the service
66
+ def self.status(status, unset_env=false)
67
+ notify("#{STATUS}#{status}", unset_env)
68
+ end
69
+
70
+ # @param errno [Integer]
71
+ def self.errno(errno, unset_env=false)
72
+ notify("#{ERRNO}#{errno}", unset_env)
73
+ end
74
+
75
+ # @param pid [Integer]
76
+ def self.mainpid(pid, unset_env=false)
77
+ notify("#{MAINPID}#{pid}", unset_env)
78
+ end
79
+
80
+ def self.watchdog(unset_env=false)
81
+ notify(WATCHDOG, unset_env)
82
+ end
83
+
84
+ def self.fdstore(unset_env=false)
85
+ notify(FDSTORE, unset_env)
86
+ end
87
+
88
+ # @param [Boolean] true if the service manager expects watchdog keep-alive
89
+ # notification messages to be sent from this process.
90
+ #
91
+ # If the $WATCHDOG_USEC environment variable is set,
92
+ # and the $WATCHDOG_PID variable is unset or set to the PID of the current
93
+ # process
94
+ #
95
+ # @note Unlike sd_watchdog_enabled(3), this method does not mutate the
96
+ # environment.
97
+ def self.watchdog?
98
+ wd_usec = ENV["WATCHDOG_USEC"]
99
+ wd_pid = ENV["WATCHDOG_PID"]
100
+
101
+ return false if !wd_usec
102
+
103
+ begin
104
+ wd_usec = Integer(wd_usec)
105
+ rescue
106
+ return false
107
+ end
108
+
109
+ return false if wd_usec <= 0
110
+ return true if !wd_pid || wd_pid == $$.to_s
111
+
112
+ false
113
+ end
114
+
115
+ # Notify systemd with the provided state, via the notification socket, if
116
+ # any.
117
+ #
118
+ # Generally this method will be used indirectly through the other methods
119
+ # of the library.
120
+ #
121
+ # @param state [String]
122
+ # @param unset_env [Boolean]
123
+ #
124
+ # @return [Fixnum, nil] the number of bytes written to the notification
125
+ # socket or nil if there was no socket to report to (eg. the program wasn't
126
+ # started by systemd)
127
+ #
128
+ # @raise [NotifyError] if there was an error communicating with the systemd
129
+ # socket
130
+ #
131
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
132
+ def self.notify(state, unset_env=false)
133
+ sock = ENV["NOTIFY_SOCKET"]
134
+
135
+ return nil if !sock
136
+
137
+ ENV.delete("NOTIFY_SOCKET") if unset_env
138
+
139
+ begin
140
+ Addrinfo.unix(sock, :DGRAM).connect do |s|
141
+ s.close_on_exec = true
142
+ s.write(state)
143
+ end
144
+ rescue StandardError => e
145
+ raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
146
+ end
147
+ end
148
+ end
149
+ end