rest-ftp-daemon 0.72b → 0.85.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.ruby-version +1 -0
- data/Gemfile.lock +6 -4
- data/README.md +58 -93
- data/bin/rest-ftp-daemon +96 -24
- data/config.ru +12 -9
- data/lib/rest-ftp-daemon.rb +2 -1
- data/lib/rest-ftp-daemon/api/defaults.rb +1 -10
- data/lib/rest-ftp-daemon/api/jobs.rb +20 -18
- data/lib/rest-ftp-daemon/api/root.rb +37 -25
- data/lib/rest-ftp-daemon/common.rb +2 -37
- data/lib/rest-ftp-daemon/config.rb +14 -19
- data/lib/rest-ftp-daemon/constants.rb +21 -0
- data/lib/rest-ftp-daemon/exceptions.rb +16 -29
- data/lib/rest-ftp-daemon/helpers.rb +55 -0
- data/lib/rest-ftp-daemon/job.rb +274 -150
- data/lib/rest-ftp-daemon/job_queue.rb +112 -17
- data/lib/rest-ftp-daemon/logger.rb +29 -5
- data/lib/rest-ftp-daemon/notification.rb +33 -48
- data/lib/rest-ftp-daemon/static/css/bootstrap.css +4490 -0
- data/lib/rest-ftp-daemon/static/css/{bootstrap.min.css → bootstrap.min.old1.css} +1 -1
- data/lib/rest-ftp-daemon/uri.rb +7 -1
- data/lib/rest-ftp-daemon/views/dashboard.haml +10 -10
- data/lib/rest-ftp-daemon/views/dashboard_jobs.haml +20 -26
- data/lib/rest-ftp-daemon/views/dashboard_tokens.haml +1 -1
- data/lib/rest-ftp-daemon/views/dashboard_workers.haml +45 -0
- data/lib/rest-ftp-daemon/worker_pool.rb +66 -29
- data/rest-ftp-daemon.gemspec +9 -8
- data/rest-ftp-daemon.yml.sample +15 -4
- metadata +48 -30
data/lib/rest-ftp-daemon/job.rb
CHANGED
@@ -3,31 +3,37 @@
|
|
3
3
|
require 'uri'
|
4
4
|
require 'net/ftp'
|
5
5
|
require 'double_bag_ftps'
|
6
|
+
require 'timeout'
|
6
7
|
|
7
8
|
module RestFtpDaemon
|
8
9
|
class Job < RestFtpDaemon::Common
|
10
|
+
attr_accessor :wid
|
9
11
|
|
10
12
|
def initialize(id, params={})
|
11
13
|
# Call super
|
12
|
-
super()
|
14
|
+
# super()
|
15
|
+
info "Job.initialize"
|
13
16
|
|
14
|
-
#
|
15
|
-
|
17
|
+
# Generate new Job.id
|
18
|
+
# $queue.counter_add :transferred, source_size
|
19
|
+
|
20
|
+
# Logger
|
21
|
+
@logger = RestFtpDaemon::Logger.new(:workers, "JOB #{id}")
|
22
|
+
|
23
|
+
# Protect with a mutex
|
24
|
+
@mutex = Mutex.new
|
16
25
|
|
17
26
|
# Init context
|
27
|
+
@params = params
|
18
28
|
set :id, id
|
19
29
|
set :started_at, Time.now
|
20
|
-
|
30
|
+
status :created
|
21
31
|
|
22
32
|
# Send first notification
|
33
|
+
info "Job.initialize/notify"
|
23
34
|
notify "rftpd.queued"
|
24
35
|
end
|
25
36
|
|
26
|
-
def progname
|
27
|
-
job_id = get(:id)
|
28
|
-
"JOB #{job_id}"
|
29
|
-
end
|
30
|
-
|
31
37
|
def id
|
32
38
|
get :id
|
33
39
|
end
|
@@ -40,60 +46,99 @@ module RestFtpDaemon
|
|
40
46
|
end
|
41
47
|
|
42
48
|
def process
|
43
|
-
#
|
44
|
-
|
45
|
-
set :status, :starting
|
46
|
-
set :error, 0
|
49
|
+
# Update job's status
|
50
|
+
set :error, nil
|
47
51
|
|
52
|
+
# Prepare job
|
48
53
|
begin
|
49
|
-
|
54
|
+
info "Job.process/prepare"
|
55
|
+
status :preparing
|
50
56
|
prepare
|
51
57
|
|
52
|
-
|
58
|
+
rescue RestFtpDaemon::JobMissingAttribute => exception
|
59
|
+
return oops "rftpd.started", exception, :job_missing_attribute
|
60
|
+
|
61
|
+
rescue RestFtpDaemon::JobSourceNotFound => exception
|
62
|
+
return oops "rftpd.started", exception, :job_source_not_found
|
63
|
+
|
64
|
+
rescue RestFtpDaemon::RestFtpDaemonException => exception
|
65
|
+
return oops "rftpd.started", exception, :job_prepare_failed
|
66
|
+
|
67
|
+
rescue Exception => exception
|
68
|
+
return oops "rftpd.started", exception, :job_prepare_unhandled, true
|
69
|
+
|
70
|
+
rescue exception
|
71
|
+
return oops "rftpd.started", exception, :WOUHOU, true
|
72
|
+
|
73
|
+
else
|
74
|
+
# Update job's status
|
75
|
+
info "Job.process/prepare ok"
|
76
|
+
status :prepared
|
77
|
+
info "Job.process/prepare status updated"
|
78
|
+
|
79
|
+
# Notify rftpd.start
|
80
|
+
info "Job.process/prepare notify started"
|
81
|
+
notify "rftpd.started", 0
|
82
|
+
info "Job.process/prepare notified started"
|
83
|
+
end
|
84
|
+
|
85
|
+
info "Job.process prepare>transfer"
|
86
|
+
|
87
|
+
# Process job
|
88
|
+
begin
|
89
|
+
info "Job.process/transfer"
|
90
|
+
status :starting
|
53
91
|
transfer
|
54
92
|
|
93
|
+
rescue Timeout::Error => exception
|
94
|
+
return oops "rftpd.ended", exception, :job_timeout_error
|
95
|
+
|
55
96
|
rescue Net::FTPPermError => exception
|
56
|
-
|
57
|
-
|
58
|
-
|
97
|
+
return oops "rftpd.ended", exception, :job_ftp_perm_error
|
98
|
+
|
99
|
+
rescue Errno::ECONNREFUSED => exception
|
100
|
+
return oops "rftpd.ended", exception, :job_connexion_refused
|
101
|
+
|
102
|
+
rescue Errno::EMFILE => exception
|
103
|
+
return oops "rftpd.ended", exception, :job_too_many_open_files
|
104
|
+
|
105
|
+
rescue RestFtpDaemon::JobTargetFileExists => exception
|
106
|
+
return oops "rftpd.ended", exception, :job_target_file_exists
|
59
107
|
|
60
|
-
rescue RestFtpDaemonException => exception
|
61
|
-
|
62
|
-
set :status, :failed
|
63
|
-
set :error, exception.class
|
108
|
+
rescue RestFtpDaemon::RestFtpDaemonException => exception
|
109
|
+
return oops "rftpd.ended", exception, :job_transfer_failed
|
64
110
|
|
65
111
|
rescue Exception => exception
|
66
|
-
|
67
|
-
set :status, :crashed
|
68
|
-
set :error, exception.class
|
112
|
+
return oops "rftpd.ended", exception, :job_transfer_unhandled, true
|
69
113
|
|
70
114
|
else
|
115
|
+
# Update job's status
|
71
116
|
info "Job.process finished"
|
72
|
-
|
117
|
+
status :finished
|
118
|
+
|
119
|
+
# Notify rftpd.ended
|
120
|
+
notify "rftpd.ended", 0
|
73
121
|
end
|
74
122
|
|
75
123
|
end
|
76
124
|
|
77
125
|
def describe
|
78
|
-
# Update realtime info
|
79
|
-
#w = wandering_time
|
80
|
-
#set :wandering, w.round(2) unless w.nil?
|
81
|
-
|
82
126
|
# Update realtime info
|
83
127
|
u = up_time
|
84
128
|
set :uptime, u.round(2) unless u.nil?
|
85
129
|
|
86
|
-
# Return the whole structure
|
130
|
+
# Return the whole structure FIXME
|
87
131
|
@params
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
@status = text
|
132
|
+
# @mutex.synchronize do
|
133
|
+
# out = @params.clone
|
134
|
+
# end
|
92
135
|
end
|
93
136
|
|
94
137
|
def get attribute
|
95
|
-
|
96
|
-
|
138
|
+
@mutex.synchronize do
|
139
|
+
@params || {}
|
140
|
+
@params[attribute]
|
141
|
+
end
|
97
142
|
end
|
98
143
|
|
99
144
|
protected
|
@@ -116,15 +161,17 @@ module RestFtpDaemon
|
|
116
161
|
@wander_for.to_f - (Time.now - @wander_started)
|
117
162
|
end
|
118
163
|
|
119
|
-
# def exception_handler(actor, reason)
|
120
|
-
# set :status, :crashed
|
121
|
-
# set :error, reason
|
122
|
-
# end
|
123
|
-
|
124
164
|
def set attribute, value
|
125
|
-
|
126
|
-
|
127
|
-
|
165
|
+
@mutex.synchronize do
|
166
|
+
@params || {}
|
167
|
+
# return unless @params.is_a? Enumerable
|
168
|
+
@params[:updated_at] = Time.now
|
169
|
+
@params[attribute] = value
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def status status
|
174
|
+
set :status, status
|
128
175
|
end
|
129
176
|
|
130
177
|
def expand_path path
|
@@ -138,169 +185,246 @@ module RestFtpDaemon
|
|
138
185
|
def replace_token path
|
139
186
|
# Ensure endpoints are not a nil value
|
140
187
|
return path unless Settings.endpoints.is_a? Enumerable
|
141
|
-
|
188
|
+
vectors = Settings.endpoints.clone
|
189
|
+
|
190
|
+
# Stack RANDOM into tokens
|
191
|
+
vectors['RANDOM'] = SecureRandom.hex(IDENT_RANDOM_LEN)
|
142
192
|
|
143
193
|
# Replace endpoints defined in config
|
144
|
-
|
145
|
-
|
194
|
+
newpath = path.clone
|
195
|
+
vectors.each do |from, to|
|
196
|
+
next if to.to_s.blank?
|
197
|
+
#info "Job.replace_token #{Helpers.tokenize(from)} > #{to}"
|
198
|
+
newpath.gsub! Helpers.tokenize(from), to
|
146
199
|
end
|
147
200
|
|
148
|
-
# Replace with the special RAND token
|
149
|
-
newpath.gsub! "[RANDOM]", SecureRandom.hex(8)
|
150
|
-
|
151
201
|
return newpath
|
152
202
|
end
|
153
203
|
|
154
204
|
def prepare
|
155
205
|
# Init
|
156
|
-
|
157
|
-
set :status, :preparing
|
206
|
+
status :preparing
|
158
207
|
@source_method = :file
|
159
208
|
@target_method = nil
|
160
209
|
@source_path = nil
|
161
210
|
@target_url = nil
|
162
211
|
|
163
212
|
# Check source
|
164
|
-
raise
|
165
|
-
|
166
|
-
@source_path = expand_path @params["source"]
|
213
|
+
raise RestFtpDaemon::JobMissingAttribute unless @params[:source]
|
214
|
+
@source_path = expand_path @params[:source]
|
167
215
|
set :source_path, @source_path
|
168
216
|
set :source_method, :file
|
169
217
|
|
170
218
|
# Check target
|
171
|
-
raise
|
172
|
-
@target_url = expand_url @params[
|
173
|
-
set :target_url, @target_url.
|
219
|
+
raise RestFtpDaemon::JobMissingAttribute unless @params[:target]
|
220
|
+
@target_url = expand_url @params[:target]
|
221
|
+
set :target_url, @target_url.to_s
|
174
222
|
|
175
223
|
if @target_url.kind_of? URI::FTP
|
176
224
|
@target_method = :ftp
|
225
|
+
elsif @target_url.kind_of? URI::FTPES
|
226
|
+
@target_method = :ftps
|
177
227
|
elsif @target_url.kind_of? URI::FTPS
|
178
228
|
@target_method = :ftps
|
179
229
|
end
|
180
230
|
set :target_method, @target_method
|
181
231
|
|
182
232
|
# Check compliance
|
183
|
-
raise JobTargetUnparseable if @target_url.nil?
|
184
|
-
raise JobTargetUnsupported if @target_method.nil?
|
185
|
-
raise JobSourceNotFound unless File.exists? @source_path
|
186
|
-
|
233
|
+
raise RestFtpDaemon::JobTargetUnparseable if @target_url.nil?
|
234
|
+
raise RestFtpDaemon::JobTargetUnsupported if @target_method.nil?
|
235
|
+
raise RestFtpDaemon::JobSourceNotFound unless File.exists? @source_path
|
187
236
|
end
|
188
237
|
|
189
|
-
def
|
238
|
+
def transfer
|
239
|
+
# Method assertions
|
240
|
+
info "Job.transfer checking_source"
|
241
|
+
status :checking_source
|
242
|
+
raise RestFtpDaemon::JobAssertionFailed unless @source_path && @target_url
|
243
|
+
|
190
244
|
# Init
|
191
|
-
set :status, :faking
|
192
245
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
246
|
+
target_name = File.basename @target_url.path
|
247
|
+
|
248
|
+
# Scheme-aware config
|
249
|
+
ftp_init
|
250
|
+
|
251
|
+
# Connect remote server, login and chdir
|
252
|
+
ftp_connect
|
253
|
+
|
254
|
+
# Check for target file presence
|
255
|
+
if get(:overwrite).nil? && (ftp_presence target_name)
|
256
|
+
@ftp.close
|
257
|
+
raise RestFtpDaemon::JobTargetFileExists
|
197
258
|
end
|
198
|
-
end
|
199
259
|
|
200
|
-
|
201
|
-
|
202
|
-
info "Job.transfer"
|
260
|
+
# Do transfer
|
261
|
+
ftp_transfer target_name
|
203
262
|
|
204
|
-
#
|
205
|
-
|
206
|
-
|
263
|
+
# Close FTP connexion
|
264
|
+
info "Job.transfer disconnecting"
|
265
|
+
status :disconnecting
|
266
|
+
@ftp.close
|
267
|
+
end
|
207
268
|
|
208
|
-
|
209
|
-
info "Job.transfer checking_source"
|
210
|
-
set :status, :checking_source
|
211
|
-
raise RestFtpDaemon::JobPrerequisitesNotMet unless @source_path
|
212
|
-
raise RestFtpDaemon::JobPrerequisitesNotMet unless @target_url
|
213
|
-
target_path = File.dirname @target_url.path
|
214
|
-
target_name = File.basename @target_url.path
|
269
|
+
private
|
215
270
|
|
216
|
-
|
217
|
-
|
218
|
-
|
271
|
+
def oops signal_name, exception, error_name = nil, include_backtrace = false
|
272
|
+
# Log this error
|
273
|
+
error_name = exception.class if error_name.nil?
|
274
|
+
info "Job.oops si[#{signal_name}] er[#{error_name.to_s}] ex[#{exception.class}]"
|
275
|
+
|
276
|
+
# Update job's internal status
|
277
|
+
set :status, :failed
|
278
|
+
set :error, error_name
|
279
|
+
set :error_exception, exception.class
|
280
|
+
|
281
|
+
# Build status stack
|
282
|
+
status = nil
|
283
|
+
if include_backtrace
|
284
|
+
set :error_backtrace, exception.backtrace
|
285
|
+
status = {
|
286
|
+
backtrace: exception.backtrace,
|
287
|
+
}
|
288
|
+
end
|
219
289
|
|
220
|
-
# Prepare
|
221
|
-
|
290
|
+
# Prepare notification if signal given
|
291
|
+
return unless signal_name
|
292
|
+
notify signal_name, error_name, status
|
293
|
+
end
|
294
|
+
|
295
|
+
def ftp_init
|
296
|
+
# Method assertions
|
297
|
+
info "Job.ftp_init"
|
298
|
+
status :ftp_init
|
299
|
+
raise RestFtpDaemon::JobAssertionFailed if @target_method.nil? || @target_url.nil?
|
222
300
|
|
223
|
-
# Scheme-aware config
|
224
301
|
case @target_method
|
225
302
|
when :ftp
|
226
|
-
info "Job.
|
227
|
-
ftp = Net::FTP.new
|
303
|
+
info "Job.ftp_init scheme: ftp"
|
304
|
+
@ftp = Net::FTP.new
|
228
305
|
when :ftps
|
229
|
-
info "Job.transfer scheme
|
230
|
-
ftp = DoubleBagFTPS.new
|
231
|
-
ftp.ssl_context = DoubleBagFTPS.create_ssl_context(:verify_mode => OpenSSL::SSL::VERIFY_NONE)
|
232
|
-
ftp.ftps_mode = DoubleBagFTPS::EXPLICIT
|
306
|
+
info "Job.transfer scheme: ftps"
|
307
|
+
@ftp = DoubleBagFTPS.new
|
308
|
+
@ftp.ssl_context = DoubleBagFTPS.create_ssl_context(:verify_mode => OpenSSL::SSL::VERIFY_NONE)
|
309
|
+
@ftp.ftps_mode = DoubleBagFTPS::EXPLICIT
|
233
310
|
else
|
234
|
-
info "Job.transfer scheme other
|
311
|
+
info "Job.transfer scheme: other [#{@target_url.scheme}]"
|
235
312
|
end
|
313
|
+
end
|
236
314
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
ftp.connect(@target_url.host)
|
241
|
-
ftp.passive = true
|
315
|
+
def ftp_connect
|
316
|
+
#status :ftp_connect
|
317
|
+
# connect_timeout_sec = (Settings.transfer.connect_timeout_sec rescue nil) || DEFAULT_CONNECT_TIMEOUT_SEC
|
242
318
|
|
243
|
-
#
|
244
|
-
info "Job.
|
245
|
-
|
246
|
-
|
247
|
-
rescue Exception => exception
|
248
|
-
info "Job.process login failed [#{exception.class}] #{u.inspect}"
|
249
|
-
set :status, :login_failed
|
250
|
-
set :error, exception.class
|
251
|
-
end
|
319
|
+
# Method assertions
|
320
|
+
info "Job.ftp_connect connect"
|
321
|
+
status :ftp_connect
|
322
|
+
raise RestFtpDaemon::JobAssertionFailed if @ftp.nil? || @target_url.nil?
|
252
323
|
|
253
|
-
|
254
|
-
|
255
|
-
set :status, :chdir
|
256
|
-
ftp.chdir(target_path)
|
324
|
+
ret = @ftp.connect(@target_url.host)
|
325
|
+
@ftp.passive = true
|
257
326
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
set :status, :remote_check
|
262
|
-
|
263
|
-
# Get file list, sometimes the response can be an empty value
|
264
|
-
results = ftp.list(target_name) rescue nil
|
265
|
-
|
266
|
-
# Result can be nil or a list of files
|
267
|
-
if results.nil? || results.count.zero?
|
268
|
-
info "Job.transfer remote_absent"
|
269
|
-
set :status, :remote_absent
|
270
|
-
else
|
271
|
-
info "Job.transfer remote_present"
|
272
|
-
set :status, :remote_present
|
273
|
-
ftp.close
|
274
|
-
notify "rftpd.ended", RestFtpDaemon::JobTargetFileExists
|
275
|
-
raise RestFtpDaemon::JobTargetFileExists
|
276
|
-
end
|
327
|
+
info "Job.ftp_connect login"
|
328
|
+
status :ftp_login
|
329
|
+
ret = @ftp.login @target_url.user, @target_url.password
|
277
330
|
|
278
|
-
|
331
|
+
info "Job.ftp_connect chdir"
|
332
|
+
status :ftp_chdir
|
333
|
+
path = File.dirname @target_url.path
|
334
|
+
ret = @ftp.chdir(path)
|
335
|
+
end
|
279
336
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
337
|
+
def ftp_presence target_name
|
338
|
+
# Method assertions
|
339
|
+
info "Job.ftp_presence"
|
340
|
+
status :ftp_presence
|
341
|
+
raise RestFtpDaemon::JobAssertionFailed if @ftp.nil? || @target_url.nil?
|
342
|
+
|
343
|
+
# Get file list, sometimes the response can be an empty value
|
344
|
+
results = @ftp.list(target_name) rescue nil
|
345
|
+
|
346
|
+
# Result can be nil or a list of files
|
347
|
+
return false if results.nil?
|
348
|
+
return results.count >0
|
349
|
+
end
|
350
|
+
|
351
|
+
def ftp_transfer target_name
|
352
|
+
# Method assertions
|
353
|
+
info "Job.ftp_transfer starting"
|
354
|
+
status :ftp_transfer
|
355
|
+
raise RestFtpDaemon::JobAssertionFailed if @ftp.nil? || @source_path.nil?
|
356
|
+
|
357
|
+
# Read source file size and parameters
|
358
|
+
source_size = File.size @source_path
|
359
|
+
set :transfer_size, source_size
|
360
|
+
update_every_kb = (Settings.transfer.update_every_kb rescue nil) || DEFAULT_UPDATE_EVERY_KB
|
361
|
+
notify_after_sec = Settings.transfer.notify_after_sec rescue nil
|
362
|
+
|
363
|
+
# Start transfer
|
364
|
+
transferred = 0
|
365
|
+
chunk_size = update_every_kb * 1024
|
366
|
+
t0 = tstart = Time.now
|
367
|
+
notified_at = Time.now
|
368
|
+
status :uploading
|
369
|
+
@ftp.putbinaryfile(@source_path, target_name, chunk_size) do |block|
|
286
370
|
# Update counters
|
287
371
|
transferred += block.bytesize
|
372
|
+
set :transfer_sent, transferred
|
373
|
+
|
374
|
+
# Update bitrate
|
375
|
+
dt = Time.now - t0
|
376
|
+
bitrate0 = (8 * chunk_size/dt).round(0)
|
377
|
+
set :transfer_bitrate, bitrate0
|
288
378
|
|
289
379
|
# Update job info
|
290
|
-
|
291
|
-
set :progress,
|
292
|
-
|
380
|
+
percent1 = (100.0 * transferred / source_size).round(1)
|
381
|
+
set :progress, percent1
|
382
|
+
|
383
|
+
# Log progress
|
384
|
+
status = []
|
385
|
+
status << "#{percent1} %"
|
386
|
+
status << (Helpers.format_bytes transferred, "B")
|
387
|
+
status << (Helpers.format_bytes source_size, "B")
|
388
|
+
status << (Helpers.format_bytes bitrate0, "bps")
|
389
|
+
info "Job.ftp_transfer" + status.map{|txt| ("%#{DEFAULT_LOGS_PROGNAME_TRIM.to_i}s" % txt)}.join("\t")
|
390
|
+
|
391
|
+
# Update time pointer
|
392
|
+
t0 = Time.now
|
393
|
+
|
394
|
+
# Notify if requested
|
395
|
+
unless notify_after_sec.nil? || (notified_at + notify_after_sec > Time.now)
|
396
|
+
status = {
|
397
|
+
progress: percent1,
|
398
|
+
transfer_sent: transferred,
|
399
|
+
transfer_size: source_size,
|
400
|
+
transfer_bitrate: bitrate0
|
401
|
+
}
|
402
|
+
notify "rftpd.progress", 0, status
|
403
|
+
notified_at = Time.now
|
404
|
+
end
|
405
|
+
|
293
406
|
end
|
294
407
|
|
295
|
-
#
|
296
|
-
|
297
|
-
set :
|
298
|
-
|
299
|
-
|
300
|
-
|
408
|
+
# Compute final bitrate
|
409
|
+
tbitrate0 = (8 * source_size.to_f / (Time.now - tstart)).round(0)
|
410
|
+
set :transfer_bitrate, tbitrate0
|
411
|
+
|
412
|
+
# Add total transferred to counter
|
413
|
+
$queue.counter_add :transferred, source_size
|
414
|
+
|
415
|
+
# Done
|
416
|
+
#set :progress, nil
|
417
|
+
info "Job.ftp_transfer finished"
|
301
418
|
end
|
302
419
|
|
303
|
-
|
420
|
+
def notify signal, error = 0, status = {}
|
421
|
+
RestFtpDaemon::Notification.new get(:notify), {
|
422
|
+
id: get(:id),
|
423
|
+
signal: signal,
|
424
|
+
error: error,
|
425
|
+
status: status,
|
426
|
+
}
|
427
|
+
end
|
304
428
|
|
305
429
|
end
|
306
430
|
end
|