aspera-cli 4.10.0 → 4.12.0
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +19 -0
- data/CHANGELOG.md +528 -0
- data/CONTRIBUTING.md +143 -0
- data/README.md +977 -589
- data/bin/ascli +4 -4
- data/bin/asession +12 -12
- data/docs/test_env.conf +29 -19
- data/examples/aoc.rb +6 -6
- data/examples/dascli +18 -16
- data/examples/faspex4.rb +15 -15
- data/examples/node.rb +12 -12
- data/examples/proxy.pac +2 -2
- data/examples/server.rb +12 -12
- data/lib/aspera/aoc.rb +344 -272
- data/lib/aspera/ascmd.rb +56 -54
- data/lib/aspera/ats_api.rb +4 -4
- data/lib/aspera/cli/basic_auth_plugin.rb +15 -12
- data/lib/aspera/cli/extended_value.rb +9 -9
- data/lib/aspera/cli/{formater.rb → formatter.rb} +69 -69
- data/lib/aspera/cli/listener/line_dump.rb +1 -1
- data/lib/aspera/cli/listener/logger.rb +1 -1
- data/lib/aspera/cli/listener/progress.rb +5 -6
- data/lib/aspera/cli/listener/progress_multi.rb +16 -21
- data/lib/aspera/cli/main.rb +72 -73
- data/lib/aspera/cli/manager.rb +112 -112
- data/lib/aspera/cli/plugin.rb +68 -48
- data/lib/aspera/cli/plugins/alee.rb +4 -4
- data/lib/aspera/cli/plugins/aoc.rb +322 -720
- data/lib/aspera/cli/plugins/ats.rb +50 -52
- data/lib/aspera/cli/plugins/bss.rb +10 -10
- data/lib/aspera/cli/plugins/config.rb +514 -410
- data/lib/aspera/cli/plugins/console.rb +12 -12
- data/lib/aspera/cli/plugins/cos.rb +18 -20
- data/lib/aspera/cli/plugins/faspex.rb +134 -136
- data/lib/aspera/cli/plugins/faspex5.rb +235 -70
- data/lib/aspera/cli/plugins/node.rb +378 -309
- data/lib/aspera/cli/plugins/orchestrator.rb +52 -49
- data/lib/aspera/cli/plugins/preview.rb +129 -120
- data/lib/aspera/cli/plugins/server.rb +137 -83
- data/lib/aspera/cli/plugins/shares.rb +77 -52
- data/lib/aspera/cli/plugins/sync.rb +13 -33
- data/lib/aspera/cli/transfer_agent.rb +61 -61
- data/lib/aspera/cli/version.rb +2 -1
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +78 -74
- data/lib/aspera/cos_node.rb +31 -29
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/environment.rb +30 -28
- data/lib/aspera/fasp/agent_base.rb +17 -15
- data/lib/aspera/fasp/agent_connect.rb +34 -32
- data/lib/aspera/fasp/agent_direct.rb +70 -73
- data/lib/aspera/fasp/agent_httpgw.rb +79 -74
- data/lib/aspera/fasp/agent_node.rb +26 -26
- data/lib/aspera/fasp/agent_trsdk.rb +20 -20
- data/lib/aspera/fasp/error.rb +3 -2
- data/lib/aspera/fasp/error_info.rb +11 -8
- data/lib/aspera/fasp/installation.rb +80 -80
- data/lib/aspera/fasp/listener.rb +2 -2
- data/lib/aspera/fasp/parameters.rb +103 -92
- data/lib/aspera/fasp/parameters.yaml +313 -214
- data/lib/aspera/fasp/resume_policy.rb +10 -10
- data/lib/aspera/fasp/transfer_spec.rb +22 -2
- data/lib/aspera/fasp/uri.rb +7 -7
- data/lib/aspera/faspex_gw.rb +80 -159
- data/lib/aspera/faspex_postproc.rb +77 -0
- data/lib/aspera/hash_ext.rb +3 -3
- data/lib/aspera/id_generator.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +23 -28
- data/lib/aspera/keychain/macos_security.rb +21 -20
- data/lib/aspera/log.rb +13 -13
- data/lib/aspera/nagios.rb +24 -23
- data/lib/aspera/node.rb +217 -38
- data/lib/aspera/oauth.rb +78 -74
- data/lib/aspera/open_application.rb +19 -11
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +13 -13
- data/lib/aspera/preview/file_types.rb +8 -8
- data/lib/aspera/preview/generator.rb +67 -67
- data/lib/aspera/preview/utils.rb +27 -27
- data/lib/aspera/proxy_auto_config.js +63 -63
- data/lib/aspera/proxy_auto_config.rb +19 -19
- data/lib/aspera/rest.rb +65 -67
- data/lib/aspera/rest_call_error.rb +2 -1
- data/lib/aspera/rest_error_analyzer.rb +22 -21
- data/lib/aspera/rest_errors_aspera.rb +16 -16
- data/lib/aspera/secret_hider.rb +17 -14
- data/lib/aspera/ssh.rb +15 -14
- data/lib/aspera/sync.rb +177 -62
- data/lib/aspera/temp_file_manager.rb +2 -2
- data/lib/aspera/uri_reader.rb +4 -4
- data/lib/aspera/web_auth.rb +13 -64
- data/lib/aspera/web_server_simple.rb +76 -0
- data.tar.gz.sig +0 -0
- metadata +11 -6
- metadata.gz.sig +0 -0
data/lib/aspera/environment.rb
CHANGED
@@ -10,12 +10,16 @@ module Aspera
|
|
10
10
|
OS_X = :osx
|
11
11
|
OS_LINUX = :linux
|
12
12
|
OS_AIX = :aix
|
13
|
-
OS_LIST = [OS_WINDOWS,OS_X,OS_LINUX,OS_AIX].freeze
|
13
|
+
OS_LIST = [OS_WINDOWS, OS_X, OS_LINUX, OS_AIX].freeze
|
14
14
|
CPU_X86_64 = :x86_64
|
15
15
|
CPU_PPC64 = :ppc64
|
16
16
|
CPU_PPC64LE = :ppc64le
|
17
17
|
CPU_S390 = :s390
|
18
|
-
CPU_LIST = [CPU_X86_64,CPU_PPC64,CPU_PPC64LE,CPU_S390].freeze
|
18
|
+
CPU_LIST = [CPU_X86_64, CPU_PPC64, CPU_PPC64LE, CPU_S390].freeze
|
19
|
+
|
20
|
+
BITS_PER_BYTE = 8
|
21
|
+
MEBI = 1024 * 1024
|
22
|
+
BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
|
19
23
|
|
20
24
|
class << self
|
21
25
|
def ruby_version
|
@@ -24,9 +28,9 @@ module Aspera
|
|
24
28
|
|
25
29
|
def os
|
26
30
|
case RbConfig::CONFIG['host_os']
|
27
|
-
when /mswin
|
31
|
+
when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/
|
28
32
|
return OS_WINDOWS
|
29
|
-
when /darwin
|
33
|
+
when /darwin/, /mac os/
|
30
34
|
return OS_X
|
31
35
|
when /linux/
|
32
36
|
return OS_LINUX
|
@@ -39,9 +43,9 @@ module Aspera
|
|
39
43
|
|
40
44
|
def cpu
|
41
45
|
case RbConfig::CONFIG['host_cpu']
|
42
|
-
when /x86_64
|
46
|
+
when /x86_64/, /x64/
|
43
47
|
return CPU_X86_64
|
44
|
-
when /powerpc
|
48
|
+
when /powerpc/, /ppc64/
|
45
49
|
return CPU_PPC64LE if os.eql?(OS_LINUX)
|
46
50
|
return CPU_PPC64
|
47
51
|
when /s390/
|
@@ -65,9 +69,9 @@ module Aspera
|
|
65
69
|
# on Windows, the env var %USERPROFILE% provides the path to user's home more reliably than %HOMEDRIVE%%HOMEPATH%
|
66
70
|
# so, tell Ruby the right way
|
67
71
|
def fix_home
|
68
|
-
return unless os.eql?(OS_WINDOWS) && ENV.
|
72
|
+
return unless os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV['USERPROFILE'])
|
69
73
|
ENV['HOME'] = ENV['USERPROFILE']
|
70
|
-
Log.log.debug
|
74
|
+
Log.log.debug{"Windows: set home to USERPROFILE: #{ENV['HOME']}"}
|
71
75
|
end
|
72
76
|
|
73
77
|
def empty_binding
|
@@ -76,37 +80,35 @@ module Aspera
|
|
76
80
|
|
77
81
|
# secure execution of Ruby code
|
78
82
|
def secure_eval(code)
|
79
|
-
Kernel.send('lave'.reverse,code,empty_binding, __FILE__, __LINE__)
|
83
|
+
Kernel.send('lave'.reverse, code, empty_binding, __FILE__, __LINE__)
|
80
84
|
end
|
81
85
|
|
82
86
|
# value is provided in block
|
83
|
-
def write_file_restricted(path,force: false)
|
87
|
+
def write_file_restricted(path, force: false)
|
84
88
|
raise 'coding error, missing content block' unless block_given?
|
85
89
|
if force || !File.exist?(path)
|
86
90
|
File.unlink(path) rescue nil # Windows may give error
|
87
|
-
File.write(path,yield)
|
91
|
+
File.write(path, yield)
|
88
92
|
restrict_file_access(path)
|
89
93
|
end
|
90
94
|
return path
|
91
95
|
end
|
92
96
|
|
93
|
-
def restrict_file_access(path,mode: nil)
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
Log.log.debug("No restriction can be set for #{path}");
|
103
|
-
end
|
97
|
+
def restrict_file_access(path, mode: nil)
|
98
|
+
if mode.nil?
|
99
|
+
# or FileUtils ?
|
100
|
+
if File.file?(path)
|
101
|
+
mode = 0o600
|
102
|
+
elsif File.directory?(path)
|
103
|
+
mode = 0o700
|
104
|
+
else
|
105
|
+
Log.log.debug{"No restriction can be set for #{path}"}
|
104
106
|
end
|
105
|
-
File.chmod(mode,path) unless mode.nil?
|
106
|
-
rescue => e
|
107
|
-
Log.log.warn(e.message)
|
108
107
|
end
|
108
|
+
File.chmod(mode, path) unless mode.nil?
|
109
|
+
rescue => e
|
110
|
+
Log.log.warn(e.message)
|
109
111
|
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
112
|
+
end # self
|
113
|
+
end # Environment
|
114
|
+
end # Aspera
|
@@ -6,6 +6,7 @@ module Aspera
|
|
6
6
|
# sub classes shall implement start_transfer and shutdown
|
7
7
|
class AgentBase
|
8
8
|
# fields description for JSON generation
|
9
|
+
# spellchecker: disable
|
9
10
|
INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte Rate MinRate Port Priority RateCap MinRateCap TCPPort CreatePolicy TimePolicy
|
10
11
|
DatagramSize XoptFlags VLinkVersion PeerVLinkVersion DSPipelineDepth PeerDSPipelineDepth ReadBlockSize WriteBlockSize
|
11
12
|
ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes TransferBytes PMTU Elapsedusec ArgScansAttempted
|
@@ -13,7 +14,8 @@ module Aspera
|
|
13
14
|
BOOLEAN_FIELDS = %w[Encryption Remote RateLock MinRateLock PolicyLock FilesEncrypt FilesDecrypt VLinkLocalEnabled VLinkRemoteEnabled
|
14
15
|
MoveRange Keepalive TestLogin UseProxy Precalc RTTAutocorrect].freeze
|
15
16
|
EXPECTED_METHODS = %i[text struct enhanced].freeze
|
16
|
-
private_constant :INTEGER_FIELDS
|
17
|
+
private_constant :INTEGER_FIELDS, :BOOLEAN_FIELDS, :EXPECTED_METHODS
|
18
|
+
# spellchecker: enable
|
17
19
|
|
18
20
|
class << self
|
19
21
|
# This checks the validity of the value returned by wait_for_transfers_completion
|
@@ -28,13 +30,13 @@ module Aspera
|
|
28
30
|
|
29
31
|
# translates legacy event into enhanced (JSON) event
|
30
32
|
def enhanced_event_format(event)
|
31
|
-
return event.keys.each_with_object({}) do |e,h|
|
33
|
+
return event.keys.each_with_object({}) do |e, h|
|
32
34
|
# capital_to_snake_case
|
33
|
-
new_name = e
|
34
|
-
gsub(/([a-z\d])([A-Z])/,'\1_\2')
|
35
|
-
gsub(/([a-z\d])(usec)$/,'\1_\2')
|
36
|
-
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
37
|
-
downcase
|
35
|
+
new_name = e
|
36
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
37
|
+
.gsub(/([a-z\d])(usec)$/, '\1_\2')
|
38
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
39
|
+
.downcase
|
38
40
|
value = event[e]
|
39
41
|
value = value.to_i if INTEGER_FIELDS.include?(e)
|
40
42
|
value = value.eql?('Yes') if BOOLEAN_FIELDS.include?(e)
|
@@ -46,7 +48,7 @@ module Aspera
|
|
46
48
|
@listeners = []
|
47
49
|
end
|
48
50
|
|
49
|
-
def notify_listeners(current_event_text,current_event_data)
|
51
|
+
def notify_listeners(current_event_text, current_event_data)
|
50
52
|
Log.log.debug('send event to listeners')
|
51
53
|
enhanced_event = nil
|
52
54
|
@listeners.each do |listener|
|
@@ -59,16 +61,16 @@ module Aspera
|
|
59
61
|
end
|
60
62
|
end # notify_listeners
|
61
63
|
|
62
|
-
def notify_begin(id,size)
|
63
|
-
notify_listeners('emulated',{LISTENER_SESSION_ID_B => id,'Type' => 'NOTIFICATION','PreTransferBytes' => size})
|
64
|
+
def notify_begin(id, size)
|
65
|
+
notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'NOTIFICATION', 'PreTransferBytes' => size})
|
64
66
|
end
|
65
67
|
|
66
|
-
def notify_progress(id,size)
|
67
|
-
notify_listeners('emulated',{LISTENER_SESSION_ID_B => id,'Type' => 'STATS','Bytescont' => size})
|
68
|
+
def notify_progress(id, size)
|
69
|
+
notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'STATS', 'Bytescont' => size})
|
68
70
|
end
|
69
71
|
|
70
72
|
def notify_end(id)
|
71
|
-
notify_listeners('emulated',{LISTENER_SESSION_ID_B => id,'Type' => 'DONE'})
|
73
|
+
notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'DONE'})
|
72
74
|
end
|
73
75
|
|
74
76
|
public
|
@@ -78,13 +80,13 @@ module Aspera
|
|
78
80
|
|
79
81
|
# listener receives events
|
80
82
|
def add_listener(listener)
|
81
|
-
raise "expect one of #{EXPECTED_METHODS}" if EXPECTED_METHODS.inject(0){|m,e|m
|
83
|
+
raise "expect one of #{EXPECTED_METHODS}" if EXPECTED_METHODS.inject(0){|m, e|m + (listener.respond_to?("event_#{e}") ? 1 : 0)}.eql?(0)
|
82
84
|
@listeners.push(listener)
|
83
85
|
self
|
84
86
|
end
|
85
87
|
|
86
88
|
# the following methods must be implemented by subclass:
|
87
|
-
# start_transfer(transfer_spec,
|
89
|
+
# start_transfer(transfer_spec, token_regenerator: nil) : start transfer
|
88
90
|
# wait_for_transfers_completion : wait for termination of all transfers, @return list of : :success or error message
|
89
91
|
# optional: shutdown
|
90
92
|
end
|
@@ -8,52 +8,54 @@ require 'tty-spinner'
|
|
8
8
|
|
9
9
|
module Aspera
|
10
10
|
module Fasp
|
11
|
-
class AgentConnect < AgentBase
|
12
|
-
|
13
|
-
SLEEP_SEC_BETWEEN_RETRY =
|
14
|
-
private_constant :
|
11
|
+
class AgentConnect < Aspera::Fasp::AgentBase
|
12
|
+
CONNECT_START_URIS = ['fasp://initialize', 'fasp://initialize', 'aspera-drive://initialize', 'https://test-connect.ibmaspera.com/']
|
13
|
+
SLEEP_SEC_BETWEEN_RETRY = 3
|
14
|
+
private_constant :CONNECT_START_URIS, :SLEEP_SEC_BETWEEN_RETRY
|
15
15
|
def initialize(_options)
|
16
16
|
super()
|
17
17
|
@connect_settings = {
|
18
18
|
'app_id' => SecureRandom.uuid
|
19
19
|
}
|
20
20
|
raise 'Using connect requires a graphical environment' if !OpenApplication.default_gui_mode.eql?(:graphical)
|
21
|
-
|
21
|
+
method_index = 0
|
22
22
|
begin
|
23
23
|
connect_url = Installation.instance.connect_uri
|
24
|
-
Log.log.debug
|
25
|
-
@connect_api = Rest.new({base_url: "#{connect_url}/v5/connect",headers: {'Origin' => Rest.user_agent}}) # could use v6 also now
|
26
|
-
|
27
|
-
Log.
|
24
|
+
Log.log.debug{"found: #{connect_url}"}
|
25
|
+
@connect_api = Rest.new({base_url: "#{connect_url}/v5/connect", headers: {'Origin' => Rest.user_agent}}) # could use v6 also now
|
26
|
+
connect_info = @connect_api.read('info/version')[:data]
|
27
|
+
Log.log.info('Connect was reached') if method_index > 0
|
28
|
+
Log.dump(:connect_version, connect_info)
|
28
29
|
rescue StandardError => e # Errno::ECONNREFUSED
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
start_url = CONNECT_START_URIS[method_index]
|
31
|
+
method_index += 1
|
32
|
+
raise StandardError, "Unable to start connect #{method_index} times" if start_url.nil?
|
33
|
+
Log.log.warn{"Aspera Connect is not started (#{e}). Trying to start it ##{method_index}..."}
|
34
|
+
if !OpenApplication.uri_graphical(start_url)
|
33
35
|
OpenApplication.uri_graphical('https://downloads.asperasoft.com/connect2/')
|
34
|
-
raise StandardError,'Connect is not installed'
|
36
|
+
raise StandardError, 'Connect is not installed'
|
35
37
|
end
|
36
38
|
sleep(SLEEP_SEC_BETWEEN_RETRY)
|
37
39
|
retry
|
38
40
|
end
|
39
41
|
end
|
40
42
|
|
41
|
-
def start_transfer(transfer_spec,
|
43
|
+
def start_transfer(transfer_spec, token_regenerator: nil)
|
42
44
|
if transfer_spec['direction'] == 'send'
|
43
|
-
Log.log.warn
|
45
|
+
Log.log.warn{"Connect requires upload selection using GUI, ignoring #{transfer_spec['paths']}".red}
|
44
46
|
transfer_spec.delete('paths')
|
45
|
-
|
47
|
+
selection = @connect_api.create('windows/select-open-file-dialog/', {
|
46
48
|
'aspera_connect_settings' => @connect_settings,
|
47
49
|
'title' => 'Select Files',
|
48
50
|
'suggestedName' => '',
|
49
51
|
'allowMultipleSelection' => true,
|
50
52
|
'allowedFileTypes' => ''})[:data]
|
51
|
-
transfer_spec['paths'] =
|
53
|
+
transfer_spec['paths'] = selection['dataTransfer']['files'].map { |i| {'source' => i['name']}}
|
52
54
|
end
|
53
55
|
@request_id = SecureRandom.uuid
|
54
56
|
# if there is a token, we ask connect client to use well known ssh private keys
|
55
57
|
# instead of asking password
|
56
|
-
transfer_spec['authentication'] = 'token' if transfer_spec.
|
58
|
+
transfer_spec['authentication'] = 'token' if transfer_spec.key?('token')
|
57
59
|
connect_transfer_args = {
|
58
60
|
'aspera_connect_settings' => @connect_settings.merge({
|
59
61
|
'request_id' => @request_id,
|
@@ -63,7 +65,7 @@ module Aspera
|
|
63
65
|
'transfer_spec' => transfer_spec
|
64
66
|
}]}
|
65
67
|
# asynchronous anyway
|
66
|
-
res = @connect_api.create('transfers/start',connect_transfer_args)[:data]
|
68
|
+
res = @connect_api.create('transfers/start', connect_transfer_args)[:data]
|
67
69
|
@xfer_id = res['transfer_specs'].first['transfer_spec']['tags']['aspera']['xfer_id']
|
68
70
|
end
|
69
71
|
|
@@ -73,39 +75,39 @@ module Aspera
|
|
73
75
|
spinner = nil
|
74
76
|
begin
|
75
77
|
loop do
|
76
|
-
tr_info = @connect_api.create("transfers/info/#{@xfer_id}",connect_activity_args)[:data]
|
78
|
+
tr_info = @connect_api.create("transfers/info/#{@xfer_id}", connect_activity_args)[:data]
|
77
79
|
if tr_info['transfer_info'].is_a?(Hash)
|
78
|
-
|
79
|
-
if
|
80
|
+
transfer = tr_info['transfer_info']
|
81
|
+
if transfer.nil?
|
80
82
|
Log.log.warn('no session in Connect')
|
81
83
|
break
|
82
84
|
end
|
83
85
|
# TODO: get session id
|
84
|
-
case
|
86
|
+
case transfer['status']
|
85
87
|
when 'completed'
|
86
88
|
notify_end(@connect_settings['app_id'])
|
87
89
|
break
|
88
|
-
when 'initiating','queued'
|
90
|
+
when 'initiating', 'queued'
|
89
91
|
if spinner.nil?
|
90
92
|
spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
|
91
93
|
spinner.start
|
92
94
|
end
|
93
|
-
spinner.update(title:
|
95
|
+
spinner.update(title: transfer['status'])
|
94
96
|
spinner.spin
|
95
97
|
when 'running'
|
96
|
-
#puts "running: sessions:#{
|
97
|
-
if !started && (
|
98
|
+
# puts "running: sessions:#{transfer['sessions'].length}, #{transfer['sessions'].map{|i| i['bytes_transferred']}.join(',')}"
|
99
|
+
if !started && (transfer['bytes_expected'] != 0)
|
98
100
|
spinner&.success
|
99
|
-
notify_begin(@connect_settings['app_id'],
|
101
|
+
notify_begin(@connect_settings['app_id'], transfer['bytes_expected'])
|
100
102
|
started = true
|
101
103
|
else
|
102
|
-
notify_progress(@connect_settings['app_id'],
|
104
|
+
notify_progress(@connect_settings['app_id'], transfer['bytes_written'])
|
103
105
|
end
|
104
106
|
when 'failed'
|
105
107
|
spinner&.error
|
106
|
-
raise Fasp::Error,
|
108
|
+
raise Fasp::Error, transfer['error_desc']
|
107
109
|
else
|
108
|
-
raise Fasp::Error, "unknown status: #{
|
110
|
+
raise Fasp::Error, "unknown status: #{transfer['status']}: #{transfer['error_desc']}"
|
109
111
|
end
|
110
112
|
end
|
111
113
|
sleep(1)
|