aspera-cli 4.4.0 → 4.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1042 -787
- data/bin/ascli +1 -1
- data/bin/asession +3 -5
- data/docs/Makefile +4 -7
- data/docs/README.erb.md +988 -740
- data/examples/faspex4.rb +4 -6
- data/examples/transfer.rb +2 -2
- data/lib/aspera/aoc.rb +139 -118
- data/lib/aspera/cli/listener/progress_multi.rb +5 -5
- data/lib/aspera/cli/main.rb +64 -34
- data/lib/aspera/cli/manager.rb +19 -20
- data/lib/aspera/cli/plugin.rb +9 -1
- data/lib/aspera/cli/plugins/aoc.rb +156 -143
- data/lib/aspera/cli/plugins/ats.rb +11 -10
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +236 -112
- data/lib/aspera/cli/plugins/faspex.rb +29 -7
- data/lib/aspera/cli/plugins/faspex5.rb +21 -8
- data/lib/aspera/cli/plugins/node.rb +21 -9
- data/lib/aspera/cli/plugins/orchestrator.rb +5 -3
- data/lib/aspera/cli/plugins/preview.rb +2 -2
- data/lib/aspera/cli/plugins/server.rb +3 -3
- data/lib/aspera/cli/plugins/shares.rb +17 -0
- data/lib/aspera/cli/transfer_agent.rb +47 -85
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +4 -4
- data/lib/aspera/fasp/{manager.rb → agent_base.rb} +7 -6
- data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +46 -39
- data/lib/aspera/fasp/{local.rb → agent_direct.rb} +14 -17
- data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +4 -4
- data/lib/aspera/fasp/{node.rb → agent_node.rb} +25 -8
- data/lib/aspera/fasp/agent_trsdk.rb +106 -0
- data/lib/aspera/fasp/default.rb +17 -0
- data/lib/aspera/fasp/installation.rb +64 -48
- data/lib/aspera/fasp/parameters.rb +7 -3
- data/lib/aspera/faspex_gw.rb +6 -6
- data/lib/aspera/keychain/encrypted_hash.rb +120 -0
- data/lib/aspera/keychain/macos_security.rb +94 -0
- data/lib/aspera/log.rb +45 -32
- data/lib/aspera/node.rb +3 -6
- data/lib/aspera/rest.rb +65 -49
- metadata +68 -27
- data/lib/aspera/api_detector.rb +0 -60
- data/lib/aspera/secrets.rb +0 -20
data/examples/faspex4.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# find Faspex API here: https://developer.ibm.com/apis/catalog/?search=faspex
|
3
3
|
# this example makes use of class Aspera::Rest for REST calls, alternatively class RestClient of gem rest-client could be used
|
4
|
-
# this example makes use of class Aspera::Fasp::
|
4
|
+
# this example makes use of class Aspera::Fasp::AgentDirect for transfers, alternatively the official "Transfer SDK" could be used
|
5
5
|
# Aspera SDK can be downloaded with: `ascli conf ascp install` , it installs in $HOME/.aspera/ascli/sdk
|
6
6
|
require 'aspera/rest'
|
7
7
|
require 'aspera/log'
|
8
|
-
require 'aspera/fasp/
|
8
|
+
require 'aspera/fasp/agent_direct'
|
9
9
|
|
10
10
|
tmpdir=ENV['tmp']||Dir.tmpdir || '.'
|
11
11
|
|
@@ -59,10 +59,8 @@ pkg_created=api_v3.create('send',package_create_params)[:data]
|
|
59
59
|
transfer_spec=pkg_created['xfer_sessions'].first
|
60
60
|
# set paths of files to send
|
61
61
|
transfer_spec['paths']=[{'source'=>file_to_send}]
|
62
|
-
# get
|
63
|
-
transfer_client=Aspera::Fasp::
|
64
|
-
# disable ascp output on stdout (optional)
|
65
|
-
transfer_client.quiet=true
|
62
|
+
# get local agent (ascp), disable ascp output on stdout to not mix with JSON events
|
63
|
+
transfer_client=Aspera::Fasp::AgentDirect.new({quiet: true})
|
66
64
|
# start transfer (asynchronous)
|
67
65
|
job_id=transfer_client.start_transfer(transfer_spec)
|
68
66
|
# wait for all transfer completion (for the example)
|
data/examples/transfer.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# Example: transfer a file using one of the provided transfer agents
|
3
3
|
# location of ascp can be specified with env var "ascp"
|
4
4
|
# temp folder can be specified with env var "tmp"
|
5
|
-
require 'aspera/fasp/
|
5
|
+
require 'aspera/fasp/agent_direct'
|
6
6
|
require 'aspera/fasp/listener'
|
7
7
|
require 'aspera/fasp/installation'
|
8
8
|
require 'aspera/log'
|
@@ -39,7 +39,7 @@ Aspera::Fasp::Installation.instance.ascp_path=ENV['ascp'] if ENV.has_key?('ascp'
|
|
39
39
|
#
|
40
40
|
|
41
41
|
# get FASP Manager singleton based on above ascp location
|
42
|
-
fasp_manager=Aspera::Fasp::
|
42
|
+
fasp_manager=Aspera::Fasp::AgentDirect.new
|
43
43
|
|
44
44
|
# Note that it would also be possible to start transfers using other agents
|
45
45
|
#require 'aspera/fasp/connect'
|
data/lib/aspera/aoc.rb
CHANGED
@@ -2,7 +2,7 @@ require 'aspera/log'
|
|
2
2
|
require 'aspera/rest'
|
3
3
|
require 'aspera/hash_ext'
|
4
4
|
require 'aspera/data_repository'
|
5
|
-
require 'aspera/
|
5
|
+
require 'aspera/fasp/default'
|
6
6
|
require 'base64'
|
7
7
|
|
8
8
|
module Aspera
|
@@ -18,19 +18,20 @@ module Aspera
|
|
18
18
|
# to avoid infinite loop in pub link redirection
|
19
19
|
MAX_REDIRECT=10
|
20
20
|
CLIENT_APPS=['aspera.global-cli-client','aspera.drive']
|
21
|
+
# index offset in data repository of client app
|
21
22
|
DATA_REPO_INDEX_START = 4
|
23
|
+
# cookie prefix so that console can decode identity
|
24
|
+
COOKIE_PREFIX='aspera.aoc'
|
22
25
|
|
23
26
|
# path in URL of public links
|
24
|
-
|
27
|
+
PUBLIC_LINK_PATHS=['/packages/public/receive','/packages/public/send','/files/public']
|
25
28
|
JWT_AUDIENCE='https://api.asperafiles.com/api/v1/oauth2/token'
|
26
29
|
OAUTH_API_SUBPATH='api/v1/oauth2'
|
27
|
-
|
28
|
-
|
29
|
-
'ssh_port' => Node::SSH_PORT_DEFAULT,
|
30
|
-
'fasp_port' => Node::UDP_PORT_DEFAULT
|
31
|
-
}
|
30
|
+
# minimum fields for user info if retrieval fails
|
31
|
+
USER_INFO_FIELDS_MIN=['name','email','id','default_workspace_id','organization_id']
|
32
32
|
|
33
|
-
private_constant :PRODUCT_NAME,:PROD_DOMAIN,:MAX_REDIRECT,:CLIENT_APPS,:
|
33
|
+
private_constant :PRODUCT_NAME,:PROD_DOMAIN,:MAX_REDIRECT,:CLIENT_APPS,:PUBLIC_LINK_PATHS,:JWT_AUDIENCE,
|
34
|
+
:OAUTH_API_SUBPATH,:COOKIE_PREFIX,:USER_INFO_FIELDS_MIN
|
34
35
|
|
35
36
|
public
|
36
37
|
# various API scopes supported
|
@@ -45,84 +46,119 @@ module Aspera
|
|
45
46
|
FILES_APP='files'
|
46
47
|
PACKAGES_APP='packages'
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
49
|
+
# class static methods
|
50
|
+
class << self
|
51
51
|
# strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
|
52
|
-
|
53
|
-
|
52
|
+
def get_client_info(client_name=CLIENT_APPS.first)
|
53
|
+
client_index=CLIENT_APPS.index(client_name)
|
54
|
+
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
55
|
+
return client_name,Base64.urlsafe_encode64(DataRepository.instance.get_bin(DATA_REPO_INDEX_START+client_index))
|
56
|
+
end
|
54
57
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
58
|
+
# @param url of AoC instance
|
59
|
+
# @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
|
60
|
+
def parse_url(aoc_org_url)
|
61
|
+
uri=URI.parse(aoc_org_url.gsub(/\/+$/,''))
|
62
|
+
instance_fqdn=uri.host
|
63
|
+
Log.log.debug("instance_fqdn=#{instance_fqdn}")
|
64
|
+
raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
|
65
|
+
organization,instance_domain=instance_fqdn.split('.',2)
|
66
|
+
Log.log.debug("instance_domain=#{instance_domain}")
|
67
|
+
Log.log.debug("organization=#{organization}")
|
68
|
+
raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
|
69
|
+
return organization,instance_domain
|
70
|
+
end
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
72
|
+
# base API url depends on domain, which could be "qa.xxx"
|
73
|
+
def api_base_url(api_domain=PROD_DOMAIN)
|
74
|
+
return "https://api.#{api_domain}"
|
75
|
+
end
|
73
76
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
def metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
|
78
|
+
return Rest.new({
|
79
|
+
:base_url => "#{api_base_url(api_domain)}/metering/v1",
|
80
|
+
:headers => {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
|
81
|
+
})
|
82
|
+
end
|
80
83
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
84
|
+
# node API scopes
|
85
|
+
def node_scope(access_key,scope)
|
86
|
+
return 'node.'+access_key+':'+scope
|
87
|
+
end
|
85
88
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
+
def set_use_default_ports(val)
|
90
|
+
@@use_standard_ports=val
|
91
|
+
end
|
89
92
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
93
|
+
# check option "link"
|
94
|
+
# if present try to get token value (resolve redirection if short links used)
|
95
|
+
# then set options url/token/auth
|
96
|
+
def resolve_pub_link(rest_opts,public_link_url)
|
97
|
+
return if public_link_url.nil?
|
98
|
+
# set to token if available after redirection
|
99
|
+
url_param_token_pair=nil
|
100
|
+
redirect_count=0
|
101
|
+
loop do
|
102
|
+
uri=URI.parse(public_link_url)
|
103
|
+
if PUBLIC_LINK_PATHS.include?(uri.path)
|
104
|
+
url_param_token_pair=URI::decode_www_form(uri.query).select{|e|e.first.eql?('token')}.first
|
105
|
+
if url_param_token_pair.nil?
|
106
|
+
raise ArgumentError,"link option must be URL with 'token' parameter"
|
107
|
+
end
|
108
|
+
# ok we get it !
|
109
|
+
rest_opts[:org_url]='https://'+uri.host
|
110
|
+
rest_opts[:auth][:grant]=:url_token
|
111
|
+
rest_opts[:auth][:url_token]=url_param_token_pair.last
|
112
|
+
return
|
104
113
|
end
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
114
|
+
Log.log.debug("no expected format: #{public_link_url}")
|
115
|
+
raise "exceeded max redirection: #{MAX_REDIRECT}" if redirect_count > MAX_REDIRECT
|
116
|
+
r = Net::HTTP.get_response(uri)
|
117
|
+
if r.code.start_with?("3")
|
118
|
+
public_link_url = r['location']
|
119
|
+
raise "no location in redirection" if public_link_url.nil?
|
120
|
+
Log.log.debug("redirect to: #{public_link_url}")
|
121
|
+
else
|
122
|
+
# not a redirection
|
123
|
+
raise ArgumentError,'link option must be redirect or have token parameter'
|
124
|
+
end
|
125
|
+
end # loop
|
126
|
+
|
127
|
+
raise RuntimeError,'too many redirections'
|
128
|
+
end
|
129
|
+
|
130
|
+
# additional transfer spec (tags) for package information
|
131
|
+
def package_tags(package_info,operation)
|
132
|
+
return {'tags'=>{'aspera'=>{'files'=>{
|
133
|
+
'package_id' => package_info['id'],
|
134
|
+
'package_name' => package_info['name'],
|
135
|
+
'package_operation' => operation
|
136
|
+
}}}}
|
137
|
+
end
|
138
|
+
|
139
|
+
# add details to show in analytics
|
140
|
+
def analytics_ts(app,direction,ws_id,ws_name)
|
141
|
+
# translate transfer to operation
|
142
|
+
operation=case direction
|
143
|
+
when 'send'; 'upload'
|
144
|
+
when 'receive'; 'download'
|
145
|
+
else raise "ERROR: unexpected value: #{direction}"
|
121
146
|
end
|
122
|
-
end # loop
|
123
147
|
|
124
|
-
|
125
|
-
|
148
|
+
return {
|
149
|
+
'tags' => {
|
150
|
+
'aspera' => {
|
151
|
+
'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
|
152
|
+
'files' => {
|
153
|
+
'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
|
154
|
+
'workspace_name' => ws_name, # activity tracking
|
155
|
+
'workspace_id' => ws_id,
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
end
|
161
|
+
end # static methods
|
126
162
|
|
127
163
|
# @param :link,:url,:auth,:client_id,:client_secret,:scope,:redirect_uri,:private_key,:username,:subpath,:password (for pub link)
|
128
164
|
def initialize(opt)
|
@@ -130,6 +166,7 @@ module Aspera
|
|
130
166
|
# key: access key
|
131
167
|
# value: associated secret
|
132
168
|
@key_chain=nil
|
169
|
+
@user_info=nil
|
133
170
|
|
134
171
|
# init rest params
|
135
172
|
aoc_rest_p={:auth=>{:type =>:oauth2}}
|
@@ -197,50 +234,28 @@ module Aspera
|
|
197
234
|
|
198
235
|
def key_chain=(keychain)
|
199
236
|
raise "keychain already set" unless @key_chain.nil?
|
237
|
+
raise "keychain must have get_secret" unless keychain.respond_to?(:get_secret)
|
200
238
|
@key_chain=keychain
|
201
239
|
nil
|
202
240
|
end
|
203
241
|
|
204
|
-
#
|
205
|
-
def
|
206
|
-
|
207
|
-
'
|
208
|
-
'
|
209
|
-
|
210
|
-
}
|
211
|
-
end
|
212
|
-
|
213
|
-
# add details to show in analytics
|
214
|
-
def self.analytics_ts(app,direction,ws_id,ws_name)
|
215
|
-
# translate transfer to operation
|
216
|
-
operation=case direction
|
217
|
-
when 'send'; 'upload'
|
218
|
-
when 'receive'; 'download'
|
219
|
-
else raise "ERROR: unexpected value: #{direction}"
|
242
|
+
# cached user information
|
243
|
+
def user_info
|
244
|
+
if @user_info.nil?
|
245
|
+
# get our user's default information
|
246
|
+
@user_info=self.read('self')[:data] rescue nil
|
247
|
+
@user_info=USER_INFO_FIELDS_MIN.inject({}){|m,f|m[f]=nil;m} if @user_info.nil?
|
248
|
+
USER_INFO_FIELDS_MIN.each{|f|@user_info[f]='unknown' if @user_info[f].nil?}
|
220
249
|
end
|
221
|
-
|
222
|
-
return {
|
223
|
-
'tags' => {
|
224
|
-
'aspera' => {
|
225
|
-
'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
|
226
|
-
'files' => {
|
227
|
-
'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
|
228
|
-
'workspace_name' => ws_name, # activity tracking
|
229
|
-
'workspace_id' => ws_id,
|
230
|
-
}
|
231
|
-
}
|
232
|
-
}
|
233
|
-
}
|
250
|
+
return @user_info
|
234
251
|
end
|
235
252
|
|
236
253
|
# build ts addon for IBM Aspera Console (cookie)
|
237
|
-
def
|
238
|
-
|
239
|
-
elements.
|
240
|
-
|
241
|
-
return {
|
242
|
-
'cookie'=>elements.join(':')
|
243
|
-
}
|
254
|
+
def console_ts(app)
|
255
|
+
# we are sure that fields are not nil
|
256
|
+
elements=[app,user_info['name'],user_info['email']].map{|e|Base64.strict_encode64(e)}
|
257
|
+
elements.unshift(COOKIE_PREFIX)
|
258
|
+
return {'cookie'=>elements.join(':')}
|
244
259
|
end
|
245
260
|
|
246
261
|
# build "transfer info", 2 elements array with:
|
@@ -248,12 +263,12 @@ module Aspera
|
|
248
263
|
# - source and token regeneration method
|
249
264
|
def tr_spec(app,direction,node_file,ts_add)
|
250
265
|
# prepare the rest end point is used to generate the bearer token
|
251
|
-
|
266
|
+
token_generation_lambda=lambda {|do_refresh|self.oauth_token(scope: self.class.node_scope(node_file[:node_info]['access_key'],SCOPE_NODE_USER), refresh: do_refresh)}
|
252
267
|
# prepare transfer specification
|
253
268
|
# note xfer_id and xfer_retry are set by the transfer agent itself
|
254
269
|
transfer_spec={
|
255
270
|
'direction' => direction,
|
256
|
-
'token' =>
|
271
|
+
'token' => token_generation_lambda.call(false), # first time, use cache
|
257
272
|
'tags' => {
|
258
273
|
'aspera' => {
|
259
274
|
'app' => app,
|
@@ -270,8 +285,14 @@ module Aspera
|
|
270
285
|
}
|
271
286
|
# add remote host info
|
272
287
|
if @@use_standard_ports
|
273
|
-
|
288
|
+
# get default TCP/UDP ports and transfer user
|
289
|
+
transfer_spec.merge!(Fasp::Default::AK_TSPEC_BASE)
|
290
|
+
# by default: same address as node API
|
274
291
|
transfer_spec['remote_host']=node_file[:node_info]['host']
|
292
|
+
# 30 it's necessarily https scheme: webui does not allow anything else
|
293
|
+
if node_file[:node_info]['transfer_url'].is_a?(String) and !node_file[:node_info]['transfer_url'].empty?
|
294
|
+
transfer_spec['remote_host']=URI.parse(node_file[:node_info]['transfer_url']).host
|
295
|
+
end
|
275
296
|
else
|
276
297
|
# retrieve values from API
|
277
298
|
std_t_spec=get_node_api(node_file[:node_info],scope: SCOPE_NODE_USER).create('files/download_setup',{:transfer_requests => [ { :transfer_request => {:paths => [ {"source"=>'/'} ] } } ] } )[:data]['transfer_specs'].first['transfer_spec']
|
@@ -282,7 +303,7 @@ module Aspera
|
|
282
303
|
# additional information for transfer agent
|
283
304
|
source_and_token_generator={
|
284
305
|
:src => :node_gen4,
|
285
|
-
:regenerate_token =>
|
306
|
+
:regenerate_token => token_generation_lambda
|
286
307
|
}
|
287
308
|
return transfer_spec,source_and_token_generator
|
288
309
|
end
|
@@ -292,10 +313,10 @@ module Aspera
|
|
292
313
|
# no scope: requires secret
|
293
314
|
# if secret provided beforehand: use it
|
294
315
|
def get_node_api(node_info,options={})
|
295
|
-
raise "INTERNAL ERROR: method parameters: options must
|
316
|
+
raise "INTERNAL ERROR: method parameters: options must be Hash" unless options.is_a?(Hash)
|
296
317
|
options.keys.each {|k| raise "INTERNAL ERROR: not valid option: #{k}" unless [:scope,:use_secret].include?(k)}
|
297
318
|
# get optional secret unless :use_secret is false (default is true)
|
298
|
-
ak_secret=@key_chain.get_secret(node_info['access_key'],false) if !options.has_key?(:use_secret) or options[:use_secret]
|
319
|
+
ak_secret=@key_chain.get_secret(url: node_info['url'], username: node_info['access_key'], mandatory: false) if !@key_chain.nil? and ( !options.has_key?(:use_secret) or options[:use_secret] )
|
299
320
|
if ak_secret.nil? and !options.has_key?(:scope)
|
300
321
|
raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}"
|
301
322
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'aspera/fasp/listener'
|
2
|
-
require 'aspera/fasp/
|
2
|
+
require 'aspera/fasp/agent_base'
|
3
3
|
require 'ruby-progressbar'
|
4
4
|
|
5
5
|
module Aspera
|
@@ -43,13 +43,13 @@ module Aspera
|
|
43
43
|
:title => '',
|
44
44
|
:total => nil)
|
45
45
|
end
|
46
|
-
if !data.has_key?(Fasp::
|
47
|
-
Log.log.error("Internal error: no #{Fasp::
|
46
|
+
if !data.has_key?(Fasp::AgentBase::LISTENER_SESSION_ID_S)
|
47
|
+
Log.log.error("Internal error: no #{Fasp::AgentBase::LISTENER_SESSION_ID_S} in event: #{data}")
|
48
48
|
return
|
49
49
|
end
|
50
50
|
newtitle=@sessions.length < 2 ? '' : "multi=#{@sessions.length}"
|
51
51
|
@progress_bar.title=newtitle unless @progress_bar.title.eql?(newtitle)
|
52
|
-
session=@sessions[data[Fasp::
|
52
|
+
session=@sessions[data[Fasp::AgentBase::LISTENER_SESSION_ID_S]]||={
|
53
53
|
cumulative: 0,
|
54
54
|
job_size: 0,
|
55
55
|
current: 0
|
@@ -78,7 +78,7 @@ module Aspera
|
|
78
78
|
# stop event when one file is completed
|
79
79
|
session[:cumulative]=session[:cumulative]+data['size'].to_i
|
80
80
|
when 'DONE' # end of session
|
81
|
-
@sessions.delete(data[Fasp::
|
81
|
+
@sessions.delete(data[Fasp::AgentBase::LISTENER_SESSION_ID_S])
|
82
82
|
update_progress
|
83
83
|
update_total
|
84
84
|
else
|