daemons 1.1.9 → 1.4.1

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -1
  3. data/README.md +207 -0
  4. data/Releases +85 -24
  5. data/examples/call/call.rb +13 -16
  6. data/examples/call/call_monitor.rb +13 -17
  7. data/examples/daemonize/daemonize.rb +4 -8
  8. data/examples/run/ctrl_crash.rb +0 -1
  9. data/examples/run/ctrl_custom_logfiles.rb +18 -0
  10. data/examples/run/ctrl_exec.rb +0 -1
  11. data/examples/run/ctrl_exit.rb +0 -1
  12. data/examples/run/ctrl_keep_pid_files.rb +1 -3
  13. data/examples/run/ctrl_monitor.rb +0 -1
  14. data/examples/run/ctrl_monitor_multiple.rb +17 -0
  15. data/examples/run/ctrl_monitor_nocrash.rb +15 -0
  16. data/examples/run/ctrl_multiple.rb +0 -1
  17. data/examples/run/ctrl_ontop.rb +0 -1
  18. data/examples/run/ctrl_optionparser.rb +5 -7
  19. data/examples/run/ctrl_proc.rb +8 -9
  20. data/examples/run/ctrl_proc_multiple.rb +4 -6
  21. data/examples/run/ctrl_proc_rand.rb +2 -4
  22. data/examples/run/ctrl_proc_simple.rb +0 -1
  23. data/examples/run/myserver.rb +0 -1
  24. data/examples/run/myserver_crashing.rb +5 -5
  25. data/examples/run/myserver_exiting.rb +2 -2
  26. data/examples/run/myserver_hanging.rb +4 -5
  27. data/examples/run/myserver_slowstop.rb +5 -6
  28. data/lib/daemons/application.rb +235 -229
  29. data/lib/daemons/application_group.rb +115 -100
  30. data/lib/daemons/change_privilege.rb +2 -4
  31. data/lib/daemons/cmdline.rb +75 -62
  32. data/lib/daemons/controller.rb +36 -54
  33. data/lib/daemons/daemonize.rb +74 -75
  34. data/lib/daemons/etc_extension.rb +3 -4
  35. data/lib/daemons/exceptions.rb +11 -13
  36. data/lib/daemons/monitor.rb +57 -77
  37. data/lib/daemons/pid.rb +26 -56
  38. data/lib/daemons/pidfile.rb +49 -44
  39. data/lib/daemons/pidmem.rb +5 -9
  40. data/lib/daemons/reporter.rb +54 -0
  41. data/lib/daemons/syslogio.rb +240 -0
  42. data/lib/daemons/version.rb +3 -0
  43. data/lib/daemons.rb +87 -77
  44. metadata +111 -46
  45. data/README +0 -214
  46. data/Rakefile +0 -90
  47. data/TODO +0 -2
  48. data/setup.rb +0 -1360
@@ -2,80 +2,110 @@ require 'daemons/pidfile'
2
2
  require 'daemons/pidmem'
3
3
  require 'daemons/change_privilege'
4
4
  require 'daemons/daemonize'
5
+ require 'daemons/exceptions'
6
+ require 'daemons/reporter'
5
7
 
6
8
  require 'timeout'
7
9
 
8
-
9
10
  module Daemons
10
-
11
11
  class Application
12
-
13
12
  attr_accessor :app_argv
14
13
  attr_accessor :controller_argv
15
-
14
+
16
15
  # the Pid instance belonging to this application
17
16
  attr_reader :pid
18
-
17
+
19
18
  # the ApplicationGroup the application belongs to
20
19
  attr_reader :group
21
-
20
+
22
21
  # my private options
23
22
  attr_reader :options
24
-
25
-
23
+
26
24
  SIGNAL = (RUBY_PLATFORM =~ /win32/ ? 'KILL' : 'TERM')
27
-
28
-
25
+
29
26
  def initialize(group, add_options = {}, pid = nil)
30
27
  @group = group
31
28
  @options = group.options.dup
32
29
  @options.update(add_options)
33
-
30
+
31
+ ['dir', 'log_dir', 'logfilename', 'output_logfilename'].each do |k|
32
+ @options[k] = File.expand_path(@options[k]) if @options.key?(k)
33
+ end
34
+
34
35
  @dir_mode = @dir = @script = nil
35
-
36
+
36
37
  @force_kill_waittime = @options[:force_kill_waittime] || 20
37
-
38
+
39
+ @signals_and_waits = parse_signals_and_waits(@options[:signals_and_waits])
40
+
41
+ @show_status_callback = method(:default_show_status)
42
+
43
+ @report = Reporter.new(@options)
44
+
38
45
  unless @pid = pid
39
46
  if @options[:no_pidfiles]
40
47
  @pid = PidMem.new
41
48
  elsif dir = pidfile_dir
42
- @pid = PidFile.new(dir, @group.app_name, @group.multiple)
49
+ @pid = PidFile.new(dir, @group.app_name, @group.multiple, @options[:pid_delimiter])
43
50
  else
44
51
  @pid = PidMem.new
45
52
  end
46
53
  end
47
54
  end
48
-
55
+
56
+ def show_status_callback=(function)
57
+ @show_status_callback =
58
+ if function.respond_to?(:call)
59
+ function
60
+ else
61
+ method(function)
62
+ end
63
+ end
64
+
49
65
  def change_privilege
50
66
  user = options[:user]
51
67
  group = options[:group]
52
- CurrentProcess.change_privilege(user, group) if user
68
+ if user
69
+ @report.changing_process_privilege(user, group)
70
+ CurrentProcess.change_privilege(user, group)
71
+ end
53
72
  end
54
-
73
+
55
74
  def script
56
- @script || @group.script
75
+ @script or group.script
57
76
  end
58
-
77
+
59
78
  def pidfile_dir
60
- Pid.dir(@dir_mode || @group.dir_mode, @dir || @group.dir, @script || @group.script)
79
+ Pid.dir dir_mode, dir, script
61
80
  end
62
-
81
+
63
82
  def logdir
64
- logdir = options[:log_dir]
65
- unless logdir
66
- logdir = options[:dir_mode] == :system ? '/var/log' : pidfile_dir
67
- end
68
- logdir
83
+ options[:log_dir] or
84
+ options[:dir_mode] == :system ? '/var/log' : pidfile_dir
69
85
  end
70
-
86
+
87
+ def output_logfilename
88
+ options[:output_logfilename] or "#{@group.app_name}.output"
89
+ end
90
+
71
91
  def output_logfile
72
- (options[:log_output] && logdir) ? File.join(logdir, @group.app_name + '.output') : nil
92
+ if log_output_syslog?
93
+ 'SYSLOG'
94
+ elsif log_output?
95
+ File.join logdir, output_logfilename
96
+ end
97
+ end
98
+
99
+ def logfilename
100
+ options[:logfilename] or "#{@group.app_name}.log"
73
101
  end
74
-
102
+
75
103
  def logfile
76
- logdir ? File.join(logdir, @group.app_name + '.log') : nil
104
+ if logdir
105
+ File.join logdir, logfilename
106
+ end
77
107
  end
78
-
108
+
79
109
  # this function is only used to daemonize the currently running process (Daemons.daemonize)
80
110
  def start_none
81
111
  unless options[:ontop]
@@ -83,163 +113,158 @@ module Daemons
83
113
  else
84
114
  Daemonize.simulate(output_logfile)
85
115
  end
86
-
116
+
87
117
  @pid.pid = Process.pid
88
-
89
-
118
+
90
119
  # We need this to remove the pid-file if the applications exits by itself.
91
- # Note that <tt>at_text</tt> will only be run if the applications exits by calling
120
+ # Note that <tt>at_text</tt> will only be run if the applications exits by calling
92
121
  # <tt>exit</tt>, and not if it calls <tt>exit!</tt> (so please don't call <tt>exit!</tt>
93
122
  # in your application!
94
123
  #
95
- at_exit {
124
+ at_exit do
96
125
  begin; @pid.cleanup; rescue ::Exception; end
97
-
126
+
98
127
  # If the option <tt>:backtrace</tt> is used and the application did exit by itself
99
128
  # create a exception log.
100
- if options[:backtrace] and not options[:ontop] and not $daemons_sigterm
101
- begin; exception_log(); rescue ::Exception; end
129
+ if options[:backtrace] && !options[:ontop] && !$daemons_sigterm
130
+ begin; exception_log; rescue ::Exception; end
102
131
  end
103
-
104
- }
105
-
106
- # This part is needed to remove the pid-file if the application is killed by
132
+
133
+ end
134
+
135
+ # This part is needed to remove the pid-file if the application is killed by
107
136
  # daemons or manually by the user.
108
137
  # Note that the applications is not supposed to overwrite the signal handler for
109
138
  # 'TERM'.
110
139
  #
111
- trap(SIGNAL) {
140
+ trap(SIGNAL) do
112
141
  begin; @pid.cleanup; rescue ::Exception; end
113
142
  $daemons_sigterm = true
114
-
143
+
115
144
  if options[:hard_exit]
116
145
  exit!
117
146
  else
118
147
  exit
119
148
  end
120
- }
149
+ end
121
150
  end
122
-
151
+
123
152
  def start_exec
124
153
  if options[:backtrace]
125
- puts "option :backtrace is not supported with :mode => :exec, ignoring"
154
+ @report.backtrace_not_supported
126
155
  end
127
-
156
+
128
157
  unless options[:ontop]
129
158
  Daemonize.daemonize(output_logfile, @group.app_name)
130
159
  else
131
160
  Daemonize.simulate(output_logfile)
132
161
  end
133
-
162
+
134
163
  # note that we cannot remove the pid file if we run in :ontop mode (i.e. 'ruby ctrl_exec.rb run')
135
164
  @pid.pid = Process.pid
136
-
137
- ENV['DAEMONS_ARGV'] = @controller_argv.join(' ')
138
- # haven't tested yet if this is really passed to the exec'd process...
139
-
140
- started()
141
- Kernel.exec(script(), *(@app_argv || []))
165
+
166
+ ENV['DAEMONS_ARGV'] = @controller_argv.join(' ')
167
+
168
+ started
169
+ Kernel.exec(script, *(@app_argv || []))
142
170
  end
143
-
171
+
144
172
  def start_load
145
173
  unless options[:ontop]
146
174
  Daemonize.daemonize(output_logfile, @group.app_name)
147
175
  else
148
176
  Daemonize.simulate(output_logfile)
149
177
  end
150
-
178
+
151
179
  @pid.pid = Process.pid
152
-
153
-
180
+
154
181
  # We need this to remove the pid-file if the applications exits by itself.
155
- # Note that <tt>at_exit</tt> will only be run if the applications exits by calling
182
+ # Note that <tt>at_exit</tt> will only be run if the applications exits by calling
156
183
  # <tt>exit</tt>, and not if it calls <tt>exit!</tt> (so please don't call <tt>exit!</tt>
157
184
  # in your application!
158
185
  #
159
- at_exit {
186
+ at_exit do
160
187
  begin; @pid.cleanup; rescue ::Exception; end
161
-
188
+
162
189
  # If the option <tt>:backtrace</tt> is used and the application did exit by itself
163
190
  # create a exception log.
164
- if options[:backtrace] and not options[:ontop] and not $daemons_sigterm
165
- begin; exception_log(); rescue ::Exception; end
191
+ if options[:backtrace] && !options[:ontop] && !$daemons_sigterm
192
+ begin; exception_log; rescue ::Exception; end
166
193
  end
167
-
168
- }
169
-
170
- # This part is needed to remove the pid-file if the application is killed by
194
+
195
+ end
196
+
197
+ # This part is needed to remove the pid-file if the application is killed by
171
198
  # daemons or manually by the user.
172
199
  # Note that the applications is not supposed to overwrite the signal handler for
173
200
  # 'TERM'.
174
201
  #
175
202
  $daemons_stop_proc = options[:stop_proc]
176
- trap(SIGNAL) {
203
+ trap(SIGNAL) do
177
204
  begin
178
- if $daemons_stop_proc
179
- $daemons_stop_proc.call
180
- end
205
+ if $daemons_stop_proc
206
+ $daemons_stop_proc.call
207
+ end
181
208
  rescue ::Exception
182
209
  end
183
-
210
+
184
211
  begin; @pid.cleanup; rescue ::Exception; end
185
212
  $daemons_sigterm = true
186
-
213
+
187
214
  if options[:hard_exit]
188
215
  exit!
189
216
  else
190
217
  exit
191
218
  end
192
- }
193
-
219
+ end
220
+
194
221
  # Now we really start the script...
195
222
  $DAEMONS_ARGV = @controller_argv
196
223
  ENV['DAEMONS_ARGV'] = @controller_argv.join(' ')
197
-
224
+
198
225
  ARGV.clear
199
226
  ARGV.concat @app_argv if @app_argv
200
-
201
- started()
202
- # TODO: begin - rescue - end around this and exception logging
203
- load script()
227
+
228
+ started
229
+ # TODO: exception logging
230
+ load script
204
231
  end
205
-
232
+
206
233
  def start_proc
207
234
  return unless p = options[:proc]
208
-
209
- myproc = proc do
210
-
211
- @pid.pid = Process.pid
212
-
235
+
236
+ myproc = proc do
237
+
213
238
  # We need this to remove the pid-file if the applications exits by itself.
214
- # Note that <tt>at_text</tt> will only be run if the applications exits by calling
239
+ # Note that <tt>at_text</tt> will only be run if the applications exits by calling
215
240
  # <tt>exit</tt>, and not if it calls <tt>exit!</tt> (so please don't call <tt>exit!</tt>
216
241
  # in your application!
217
242
  #
218
- at_exit {
243
+ at_exit do
219
244
  begin; @pid.cleanup; rescue ::Exception; end
220
245
 
221
246
  # If the option <tt>:backtrace</tt> is used and the application did exit by itself
222
247
  # create a exception log.
223
- if options[:backtrace] and not options[:ontop] and not $daemons_sigterm
224
- begin; exception_log(); rescue ::Exception; end
248
+ if options[:backtrace] && !options[:ontop] && !$daemons_sigterm
249
+ begin; exception_log; rescue ::Exception; end
225
250
  end
226
251
 
227
- }
252
+ end
228
253
 
229
- # This part is needed to remove the pid-file if the application is killed by
254
+ # This part is needed to remove the pid-file if the application is killed by
230
255
  # daemons or manually by the user.
231
256
  # Note that the applications is not supposed to overwrite the signal handler for
232
257
  # 'TERM'.
233
258
  #
234
259
  $daemons_stop_proc = options[:stop_proc]
235
- trap(SIGNAL) {
260
+ trap(SIGNAL) do
236
261
  begin
237
- if $daemons_stop_proc
238
- $daemons_stop_proc.call
239
- end
262
+ if $daemons_stop_proc
263
+ $daemons_stop_proc.call
264
+ end
240
265
  rescue ::Exception
241
266
  end
242
-
267
+
243
268
  begin; @pid.cleanup; rescue ::Exception; end
244
269
  $daemons_sigterm = true
245
270
 
@@ -248,42 +273,28 @@ module Daemons
248
273
  else
249
274
  exit
250
275
  end
251
- }
252
-
253
- started()
254
-
255
- p.call()
276
+ end
277
+ p.call
256
278
  end
257
-
279
+
258
280
  unless options[:ontop]
259
- Daemonize.call_as_daemon(myproc, output_logfile, @group.app_name)
260
-
281
+ @pid.pid = Daemonize.call_as_daemon(myproc, output_logfile, @group.app_name)
282
+
261
283
  else
262
284
  Daemonize.simulate(output_logfile)
263
-
285
+
264
286
  myproc.call
265
-
266
- # why did we use this??
267
- # Thread.new(&options[:proc])
268
-
269
- # why did we use the code below??
270
- # unless pid = Process.fork
271
- # @pid.pid = pid
272
- # Daemonize.simulate(logfile)
273
- # options[:proc].call
274
- # exit
275
- # else
276
- # Process.detach(@pid.pid)
277
- # end
278
287
  end
279
-
288
+ started
280
289
  end
281
-
282
-
283
- def start
290
+
291
+ def start(restart = false)
284
292
  change_privilege
285
- @group.create_monitor(@group.applications[0] || self) unless options[:ontop] # we don't monitor applications in the foreground
286
-
293
+
294
+ unless restart
295
+ @group.create_monitor(self) unless options[:ontop] # we don't monitor applications in the foreground
296
+ end
297
+
287
298
  case options[:mode]
288
299
  when :none
289
300
  # this is only used to daemonize the currently running process
@@ -298,32 +309,14 @@ module Daemons
298
309
  start_load
299
310
  end
300
311
  end
301
-
312
+
302
313
  def started
303
314
  if pid = @pid.pid
304
- puts "#{self.group.app_name}: process with pid #{pid} started."
305
- STDOUT.flush
315
+ @report.process_started(group.app_name, pid)
306
316
  end
307
317
  end
308
-
309
-
310
- # def run
311
- # if @group.controller.options[:exec]
312
- # run_via_exec()
313
- # else
314
- # run_via_load()
315
- # end
316
- # end
317
- #
318
- # def run_via_exec
319
- #
320
- # end
321
- #
322
- # def run_via_load
323
- #
324
- # end
325
-
326
- def reload
318
+
319
+ def reload
327
320
  if @pid.pid == 0
328
321
  zap
329
322
  start
@@ -342,122 +335,112 @@ module Daemons
342
335
  # one cannot catch exceptions that are thrown in threads other than the main
343
336
  # thread.
344
337
  #
345
- # This function searches for all exceptions in memory and outputs them to STDERR
338
+ # This function searches for all exceptions in memory and outputs them to $stderr
346
339
  # (if it is connected) and to a log file in the pid-file directory.
347
340
  #
348
341
  def exception_log
349
342
  return unless logfile
350
-
343
+
351
344
  require 'logger'
352
-
345
+
353
346
  l_file = Logger.new(logfile)
354
-
347
+
355
348
  # the code below finds the last exception
356
349
  e = nil
357
-
358
- ObjectSpace.each_object {|o|
350
+
351
+ ObjectSpace.each_object do |o|
359
352
  if ::Exception === o
360
353
  e = o
361
354
  end
362
- }
363
-
364
- l_file.info "*** below you find the most recent exception thrown, this will be likely (but not certainly) the exception that made the application exit abnormally ***"
355
+ end
356
+
357
+ l_file.info '*** below you find the most recent exception thrown, this will be likely (but not certainly) the exception that made the application exit abnormally ***'
365
358
  l_file.error e
366
-
367
- l_file.info "*** below you find all exception objects found in memory, some of them may have been thrown in your application, others may just be in memory because they are standard exceptions ***"
368
-
359
+
360
+ l_file.info '*** below you find all exception objects found in memory, some of them may have been thrown in your application, others may just be in memory because they are standard exceptions ***'
361
+
369
362
  # this code logs every exception found in memory
370
- ObjectSpace.each_object {|o|
363
+ ObjectSpace.each_object do |o|
371
364
  if ::Exception === o
372
365
  l_file.error o
373
366
  end
374
- }
375
-
367
+ end
368
+
376
369
  l_file.close
377
370
  end
378
-
379
-
371
+
380
372
  def stop(no_wait = false)
381
- if not running?
382
- self.zap
373
+ unless running?
374
+ zap
383
375
  return
384
376
  end
385
-
377
+
378
+ # confusing: pid is also a attribute_reader
386
379
  pid = @pid.pid
387
-
380
+
388
381
  # Catch errors when trying to kill a process that doesn't
389
382
  # exist. This happens when the process quits and hasn't been
390
383
  # restarted by the monitor yet. By catching the error, we allow the
391
384
  # pid file clean-up to occur.
392
385
  begin
393
- Process.kill(SIGNAL, pid)
386
+ wait_and_retry_kill_harder(pid, @signals_and_waits, no_wait)
394
387
  rescue Errno::ESRCH => e
395
- puts "#{e} #{pid}"
396
- puts "deleting pid-file."
388
+ @report.output_message("#{e} #{pid}")
389
+ @report.output_message('deleting pid-file.')
397
390
  end
398
-
399
- if not no_wait
400
- if @force_kill_waittime > 0
401
- puts "#{self.group.app_name}: trying to stop process with pid #{pid}..."
402
- STDOUT.flush
403
-
404
- begin
405
- Timeout::timeout(@force_kill_waittime) {
406
- while Pid.running?(pid)
407
- sleep(0.2)
408
- end
409
- }
410
- rescue Timeout::Error
411
- puts "#{self.group.app_name}: process with pid #{pid} won't stop, we forcefully kill it..."
412
- STDOUT.flush
413
-
414
- begin
415
- Process.kill('KILL', pid)
416
- rescue Errno::ESRCH
417
- end
418
-
419
- begin
420
- Timeout::timeout(20) {
421
- while Pid.running?(pid)
422
- sleep(1)
423
- end
424
- }
425
- rescue Timeout::Error
426
- puts "#{self.group.app_name}: unable to forcefully kill process with pid #{pid}."
427
- STDOUT.flush
428
- end
429
- end
430
- end
431
-
432
-
433
- end
434
-
391
+
435
392
  sleep(0.1)
436
393
  unless Pid.running?(pid)
437
394
  # We try to remove the pid-files by ourselves, in case the application
438
395
  # didn't clean it up.
439
- begin; @pid.cleanup; rescue ::Exception; end
440
-
441
- puts "#{self.group.app_name}: process with pid #{pid} successfully stopped."
442
- STDOUT.flush
396
+ zap!
397
+
398
+ @report.stopped_process(group.app_name, pid)
443
399
  end
444
-
445
400
  end
446
-
401
+
402
+ # @param Hash remaing_signals
403
+ # @param Boolean no_wait Send first Signal and return
404
+ def wait_and_retry_kill_harder(pid, remaining_signals, no_wait = false)
405
+ sig_wait = remaining_signals.shift
406
+ sig = sig_wait[:sig]
407
+ wait = sig_wait[:wait]
408
+ Process.kill(sig, pid)
409
+ return if no_wait || !wait.positive?
410
+
411
+ @report.stopping_process(group.app_name, pid, sig, wait)
412
+
413
+ begin
414
+ Timeout.timeout(wait, TimeoutError) do
415
+ sleep(0.2) while Pid.running?(pid)
416
+ end
417
+ rescue TimeoutError
418
+ if remaining_signals.any?
419
+ wait_and_retry_kill_harder(pid, remaining_signals)
420
+ else
421
+ @report.cannot_stop_process(group.app_name, pid)
422
+ end
423
+ end
424
+ end
425
+
447
426
  def zap
448
- @pid.cleanup
427
+ @pid.zap
449
428
  end
450
-
429
+
451
430
  def zap!
452
- begin; @pid.cleanup; rescue ::Exception; end
431
+ begin; @pid.zap; rescue ::Exception; end
453
432
  end
454
-
433
+
455
434
  def show_status
456
- running = self.running?
457
-
458
- puts "#{self.group.app_name}: #{running ? '' : 'not '}running#{(running and @pid.exist?) ? ' [pid ' + @pid.pid.to_s + ']' : ''}#{(@pid.exist? and not running) ? ' (but pid-file exists: ' + @pid.pid.to_s + ')' : ''}"
435
+ @show_status_callback.call(self)
436
+ end
437
+
438
+ def default_show_status(daemon = self)
439
+ running = daemon.running?
440
+
441
+ @report.status(group.app_name, running, daemon.pid.exist?, daemon.pid.pid.to_s)
459
442
  end
460
-
443
+
461
444
  # This function implements a (probably too simle) method to detect
462
445
  # whether the program with the pid found in the pid-file is still running.
463
446
  # It just searches for the pid in the output of <tt>ps ax</tt>, which
@@ -466,12 +449,35 @@ module Daemons
466
449
  # system.
467
450
  #
468
451
  def running?
469
- if @pid.exist?
470
- return Pid.running?(@pid.pid)
452
+ @pid.exist? and Pid.running? @pid.pid
453
+ end
454
+
455
+ private
456
+
457
+ def log_output?
458
+ options[:log_output] && logdir
459
+ end
460
+
461
+ def log_output_syslog?
462
+ options[:log_output_syslog]
463
+ end
464
+
465
+ def dir_mode
466
+ @dir_mode or group.dir_mode
467
+ end
468
+
469
+ def dir
470
+ @dir or group.dir
471
+ end
472
+
473
+ def parse_signals_and_waits(argv)
474
+ unless argv
475
+ return [
476
+ { sig: 'TERM', wait: @force_kill_waittime },
477
+ { sig: 'KILL', wait: 20 }
478
+ ]
471
479
  end
472
-
473
- return false
480
+ argv.split('|').collect{ |part| splitted = part.split(':'); {sig: splitted[0], wait: splitted[1].to_i}}
474
481
  end
475
482
  end
476
-
477
483
  end