a4tools 1.2.7
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 +7 -0
- data/.bundle/install.log +38 -0
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -0
- data/a4tools.gemspec +38 -0
- data/bin/deploy_latest_clients +32 -0
- data/bin/devsite_config_server +48 -0
- data/bin/netshell +23 -0
- data/bin/update_server +101 -0
- data/bin/usher +54 -0
- data/lib/a4tools.rb +61 -0
- data/lib/a4tools/version.rb +3 -0
- data/lib/acres_client.rb +376 -0
- data/lib/clients/caching_client.rb +151 -0
- data/lib/clients/deployment_client.rb +53 -0
- data/lib/clients/kai_config_client.rb +39 -0
- data/lib/clients/usher_client.rb +72 -0
- data/lib/clients/usher_mgmt_client.rb +201 -0
- data/lib/event_manager.rb +24 -0
- data/lib/events.json +1 -0
- data/lib/net_shell/builtin_command.rb +312 -0
- data/lib/net_shell/builtin_commands/build.rb +251 -0
- data/lib/net_shell/builtin_commands/cd.rb +12 -0
- data/lib/net_shell/builtin_commands/connect.rb +122 -0
- data/lib/net_shell/builtin_commands/deploy.rb +280 -0
- data/lib/net_shell/builtin_commands/disconnect.rb +15 -0
- data/lib/net_shell/builtin_commands/excerpt.rb +97 -0
- data/lib/net_shell/builtin_commands/exit.rb +7 -0
- data/lib/net_shell/builtin_commands/get.rb +38 -0
- data/lib/net_shell/builtin_commands/help.rb +40 -0
- data/lib/net_shell/builtin_commands/host.rb +126 -0
- data/lib/net_shell/builtin_commands/inject.rb +42 -0
- data/lib/net_shell/builtin_commands/jsoncache.rb +80 -0
- data/lib/net_shell/builtin_commands/kai_event.rb +151 -0
- data/lib/net_shell/builtin_commands/persist.rb +24 -0
- data/lib/net_shell/builtin_commands/pwd.rb +6 -0
- data/lib/net_shell/builtin_commands/recap.rb +188 -0
- data/lib/net_shell/builtin_commands/references.rb +63 -0
- data/lib/net_shell/builtin_commands/select.rb +36 -0
- data/lib/net_shell/builtin_commands/send.rb +74 -0
- data/lib/net_shell/builtin_commands/set.rb +29 -0
- data/lib/net_shell/builtin_commands/show.rb +183 -0
- data/lib/net_shell/builtin_commands/site.rb +122 -0
- data/lib/net_shell/builtin_commands/ssh.rb +62 -0
- data/lib/net_shell/builtin_commands/talk.rb +90 -0
- data/lib/net_shell/builtin_commands/translate.rb +45 -0
- data/lib/net_shell/builtin_commands/unset.rb +14 -0
- data/lib/net_shell/builtin_commands/usher.rb +55 -0
- data/lib/net_shell/builtin_commands/usher_device.rb +39 -0
- data/lib/net_shell/builtin_commands/usher_site.rb +245 -0
- data/lib/net_shell/builtin_commands/usherm_connect.rb +21 -0
- data/lib/net_shell/colors.rb +149 -0
- data/lib/net_shell/command.rb +97 -0
- data/lib/net_shell/io.rb +132 -0
- data/lib/net_shell/net_shell.rb +396 -0
- data/lib/net_shell/prompt.rb +335 -0
- data/lib/object_builder/definitions/app_info_for_script.rb +83 -0
- data/lib/object_builder/definitions/connection_request.rb +28 -0
- data/lib/object_builder/definitions/device_info_for_system.rb +37 -0
- data/lib/object_builder/object_builder.rb +145 -0
- data/lib/talk.json +1 -0
- data/lib/talk_consumer.rb +235 -0
- metadata +279 -0
data/lib/acres_client.rb
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'uri'
|
3
|
+
require 'net/https'
|
4
|
+
require 'faye/websocket'
|
5
|
+
require 'eventmachine'
|
6
|
+
require 'thread'
|
7
|
+
|
8
|
+
|
9
|
+
module A4Tools
|
10
|
+
module EventGenerator
|
11
|
+
def on(key, &callback)
|
12
|
+
@callbacks ||= {}
|
13
|
+
@callbacks[key] ||= []
|
14
|
+
@callbacks[key].push callback
|
15
|
+
end
|
16
|
+
|
17
|
+
def signal(key, value=nil)
|
18
|
+
return if @callbacks.nil?
|
19
|
+
[ key, :all ].each do |k|
|
20
|
+
next unless @callbacks.has_key? k
|
21
|
+
@callbacks[k].each { |callback| callback.call(key, value) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def passthrough(target)
|
26
|
+
target.on(:all) { |trigger, value| signal(trigger, value) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Transporter
|
31
|
+
include EventGenerator
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@callbacks = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def connect(timeout=5)
|
38
|
+
signal(:connect)
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def disconnect
|
43
|
+
signal(:disconnect)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class HTTPTransporter < Transporter
|
48
|
+
def initialize(uri)
|
49
|
+
super()
|
50
|
+
@uri = uri
|
51
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
52
|
+
@http.use_ssl = @uri.scheme == 'https'
|
53
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
54
|
+
end
|
55
|
+
|
56
|
+
def response_body(result)
|
57
|
+
return nil unless result.code.to_i >= 200 and result.code.to_i < 300
|
58
|
+
|
59
|
+
jsonrpc = symbolify JSON.parse(result.body)
|
60
|
+
return nil unless jsonrpc[:jsonrpc] == "2.0"
|
61
|
+
return nil unless jsonrpc[:error].nil?
|
62
|
+
return nil if jsonrpc[:result].nil?
|
63
|
+
|
64
|
+
return jsonrpc[:result][:body] unless jsonrpc[:result][:body].nil?
|
65
|
+
|
66
|
+
return jsonrpc[:result]
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_message(msg)
|
70
|
+
req = Net::HTTP::Post.new(@uri.to_s,
|
71
|
+
initheader = { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
|
72
|
+
)
|
73
|
+
req.body = msg
|
74
|
+
signal(:sent, msg)
|
75
|
+
response = @http.request(req)
|
76
|
+
signal(:message, response.body)
|
77
|
+
|
78
|
+
response
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class WebSocketTransporter < Transporter
|
83
|
+
attr_reader :ready
|
84
|
+
|
85
|
+
def initialize(uri)
|
86
|
+
super()
|
87
|
+
@uri = uri
|
88
|
+
@queue = []
|
89
|
+
@ready = false
|
90
|
+
@mutex = Mutex.new
|
91
|
+
@condition = ConditionVariable.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def connect(timeout=5)
|
95
|
+
return true if @ready
|
96
|
+
start_socket
|
97
|
+
@mutex.synchronize { @condition.wait(@mutex, timeout) }
|
98
|
+
@ready
|
99
|
+
end
|
100
|
+
|
101
|
+
def disconnect
|
102
|
+
@client.kill unless @client.nil?
|
103
|
+
@client = nil
|
104
|
+
@ws = nil
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
def start_socket
|
109
|
+
@client = Thread.new do
|
110
|
+
EM.run do
|
111
|
+
@ws = Faye::WebSocket::Client.new(@uri.to_s)
|
112
|
+
|
113
|
+
@ws.on :open do |event|
|
114
|
+
@mutex.synchronize do
|
115
|
+
@ready = true
|
116
|
+
@condition.signal
|
117
|
+
end
|
118
|
+
|
119
|
+
signal(:connect)
|
120
|
+
empty_queue
|
121
|
+
end
|
122
|
+
|
123
|
+
@ws.on :error do |event|
|
124
|
+
@mutex.synchronize { @condition.signal }
|
125
|
+
end
|
126
|
+
|
127
|
+
@ws.on :message do |event|
|
128
|
+
signal(:message, event.data)
|
129
|
+
end
|
130
|
+
|
131
|
+
@ws.on :close do |event|
|
132
|
+
@ready = false
|
133
|
+
@ws = nil
|
134
|
+
signal(:disconnect)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def empty_queue
|
141
|
+
@queue.each { |msg| send_message(msg) }
|
142
|
+
@queue = []
|
143
|
+
end
|
144
|
+
|
145
|
+
def send_message(msg)
|
146
|
+
unless @ready then
|
147
|
+
@queue.push msg
|
148
|
+
return
|
149
|
+
end
|
150
|
+
|
151
|
+
signal(:sent, msg)
|
152
|
+
@ws.send(msg)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class AcresClient
|
157
|
+
include EventGenerator
|
158
|
+
|
159
|
+
attr_reader :uri, :token, :server_info, :history, :start_time, :connect_time, :connected, :ready
|
160
|
+
attr_accessor :username, :password, :version
|
161
|
+
|
162
|
+
def initialize(destination, username=nil, password=nil)
|
163
|
+
if destination.is_a? String then
|
164
|
+
@uri = URI(destination)
|
165
|
+
else
|
166
|
+
@uri = URI(destination[:url] + destination[:ctx] + "/json?wrap")
|
167
|
+
end
|
168
|
+
|
169
|
+
@username = username
|
170
|
+
@password = password
|
171
|
+
@history = []
|
172
|
+
|
173
|
+
case @uri.scheme
|
174
|
+
when 'http', 'https'
|
175
|
+
@transport = HTTPTransporter.new(@uri)
|
176
|
+
when 'ws', 'wss'
|
177
|
+
@transport = WebSocketTransporter.new(@uri)
|
178
|
+
end
|
179
|
+
|
180
|
+
passthrough(@transport)
|
181
|
+
@transport.on(:message) { |trigger, message| snoop_token(message) }
|
182
|
+
@transport.on(:message) { |trigger, message| add_message_to_history(:server, message) }
|
183
|
+
@transport.on(:sent) { |trigger, message| add_message_to_history(:client, message) }
|
184
|
+
@transport.on(:connect) { |trigger, message| @ready = true }
|
185
|
+
@transport.on(:disconnect) do |trigger, message|
|
186
|
+
@ready = false
|
187
|
+
@start_time = nil
|
188
|
+
end
|
189
|
+
|
190
|
+
@connected = false
|
191
|
+
@connect_time = nil
|
192
|
+
@start_time = nil
|
193
|
+
@authenticated = false
|
194
|
+
@token = nil
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_message_to_history(sender, message, timestamp=nil)
|
198
|
+
timestamp ||= Time.now
|
199
|
+
@start_time ||= timestamp
|
200
|
+
history.push({ time: timestamp, sender: sender, message: (symbolify(JSON.parse(message)) rescue nil), raw:message })
|
201
|
+
end
|
202
|
+
|
203
|
+
def snoop_token(message)
|
204
|
+
begin
|
205
|
+
response = symbolify JSON.parse(message)
|
206
|
+
result = response[:result]
|
207
|
+
return if attempt_snoop(result)
|
208
|
+
attempt_snoop(result[:body]) unless result.nil? or result[:body].nil?
|
209
|
+
rescue
|
210
|
+
""
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def attempt_snoop(result)
|
215
|
+
if not(result.nil?) and result.has_key? :tok and result.has_key? :serverInfo then
|
216
|
+
# this is a connection token
|
217
|
+
@connected = true
|
218
|
+
@token = result[:tok] if result.has_key? :tok
|
219
|
+
@server_info = result[:serverInfo] if result.has_key? :serverInfo
|
220
|
+
signal(:connect)
|
221
|
+
true
|
222
|
+
else
|
223
|
+
false
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def transport_connect
|
228
|
+
@transport.connect
|
229
|
+
end
|
230
|
+
|
231
|
+
def disconnect
|
232
|
+
@transport.disconnect
|
233
|
+
end
|
234
|
+
|
235
|
+
def connect
|
236
|
+
return nil unless transport_connect
|
237
|
+
|
238
|
+
request = {
|
239
|
+
appInfo: app_info,
|
240
|
+
deviceInfo: device_info
|
241
|
+
}
|
242
|
+
|
243
|
+
result = send_message(wrapped_message("connect", "com.acres4.common.info.ConnectionRequest", request))
|
244
|
+
response = response_body(result)
|
245
|
+
return nil unless response
|
246
|
+
|
247
|
+
response
|
248
|
+
end
|
249
|
+
|
250
|
+
def connect_if_needed
|
251
|
+
return true if @connected
|
252
|
+
return true unless connect.nil?
|
253
|
+
return false
|
254
|
+
end
|
255
|
+
|
256
|
+
def authenticate(username=nil, password=nil)
|
257
|
+
return nil unless connect_if_needed
|
258
|
+
|
259
|
+
request = {
|
260
|
+
tok: @token,
|
261
|
+
username: username || @username,
|
262
|
+
password: make_password(password || @password),
|
263
|
+
automaticReconnect: false
|
264
|
+
}
|
265
|
+
|
266
|
+
result = send_message(wrapped_message("login", "com.acres4.common.info.ClientLoginRequest", request))
|
267
|
+
response = response_body(result)
|
268
|
+
@authenticated = not(response.nil?)
|
269
|
+
response
|
270
|
+
end
|
271
|
+
|
272
|
+
def authenticate_if_needed(username=nil, password=nil)
|
273
|
+
return true if @authenticated
|
274
|
+
return true unless authenticate(username, password).nil?
|
275
|
+
return false
|
276
|
+
end
|
277
|
+
|
278
|
+
def make_password(password)
|
279
|
+
{
|
280
|
+
algorithm:"CLEARTEXT",
|
281
|
+
contents:password,
|
282
|
+
parameters:[]
|
283
|
+
}
|
284
|
+
end
|
285
|
+
|
286
|
+
def next_msg_id
|
287
|
+
@msg_id = @msg_id.nil? ? 0 : @msg_id + 1
|
288
|
+
end
|
289
|
+
|
290
|
+
def wrapped_message(method, cls, body)
|
291
|
+
cls ||= talk.guess_class(body)
|
292
|
+
body[:__class] = cls.to_s;
|
293
|
+
body[:tok] = @token unless @token.nil?
|
294
|
+
|
295
|
+
wrapper = {
|
296
|
+
className:cls,
|
297
|
+
body:body,
|
298
|
+
namedObjectEncodingType:0,
|
299
|
+
serializedEncoding:nil,
|
300
|
+
__class:"com.acres4.common.info.NamedObjectWrapper"
|
301
|
+
}
|
302
|
+
|
303
|
+
jsonrpc_message(method, nil, "com.acres4.common.info.NamedObjectWrapper", wrapper)
|
304
|
+
end
|
305
|
+
|
306
|
+
def inject_token(message, force=false, type=nil)
|
307
|
+
return nil if message.nil?
|
308
|
+
return message unless force or message[:tok].nil?
|
309
|
+
type ||= talk.guess_class(message)
|
310
|
+
unless type.nil? then
|
311
|
+
cls = talk.class_named(type)
|
312
|
+
has_tok = (cls[:field].select do |f|
|
313
|
+
is_token_class = talk.name_matches?("com.acres4.common.info.ConnectionToken", f[:type].first)
|
314
|
+
f[:name] == "tok" and is_token_class
|
315
|
+
end).length >= 1
|
316
|
+
|
317
|
+
message[:tok] = @token if has_tok
|
318
|
+
end
|
319
|
+
|
320
|
+
message
|
321
|
+
end
|
322
|
+
|
323
|
+
def jsonrpc_message(method, interface=nil, cls=nil, body={})
|
324
|
+
body[:__class] = (cls || talk.guess_class(body)) unless body.nil?
|
325
|
+
|
326
|
+
json = {
|
327
|
+
__class:"com.acres4.common.info.JSONRPCRequest",
|
328
|
+
method:method,
|
329
|
+
params:body,
|
330
|
+
id:next_msg_id,
|
331
|
+
jsonrpc:"2.0"
|
332
|
+
}
|
333
|
+
|
334
|
+
json[:intf] = interface unless interface.nil? # don't even include it if we didn't specify it
|
335
|
+
json
|
336
|
+
end
|
337
|
+
|
338
|
+
def empty_query(method, interface=nil)
|
339
|
+
jsonrpc_message(method, interface)
|
340
|
+
end
|
341
|
+
|
342
|
+
def send_message(message)
|
343
|
+
message = message.to_json unless message.is_a? String
|
344
|
+
@transport.send_message(message)
|
345
|
+
end
|
346
|
+
|
347
|
+
def response_body(result)
|
348
|
+
return nil unless result.code.to_i >= 200 and result.code.to_i < 300
|
349
|
+
|
350
|
+
jsonrpc = JSON.parse(result.body)
|
351
|
+
return nil unless jsonrpc["jsonrpc"] == "2.0"
|
352
|
+
return nil unless jsonrpc["error"].nil?
|
353
|
+
return nil if jsonrpc["result"].nil?
|
354
|
+
|
355
|
+
return symbolify jsonrpc["result"]["body"] unless jsonrpc["result"]["body"].nil?
|
356
|
+
|
357
|
+
return symbolify jsonrpc["result"]
|
358
|
+
end
|
359
|
+
|
360
|
+
def app_info
|
361
|
+
info = ObjectBuilder[:app_info_for_script].value
|
362
|
+
info[:currentTalkVersion] = @version unless @version.nil?
|
363
|
+
info
|
364
|
+
end
|
365
|
+
|
366
|
+
def device_info
|
367
|
+
ObjectBuilder[:device_info_for_system].value
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
require 'clients/caching_client.rb'
|
373
|
+
require 'clients/deployment_client.rb'
|
374
|
+
require 'clients/kai_config_client.rb'
|
375
|
+
require 'clients/usher_client.rb'
|
376
|
+
require 'clients/usher_mgmt_client.rb'
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module A4Tools
|
2
|
+
class AuthenticationError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class UpdateError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class CachingClient < AcresClient
|
9
|
+
attr_accessor :timeout
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :cache_methods, :query_methods
|
13
|
+
def cache(name, params={}, &blk)
|
14
|
+
@cache_methods ||= {}
|
15
|
+
@cache_methods[name] = {
|
16
|
+
params:params,
|
17
|
+
block:blk
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def query(name, params={}, &blk)
|
22
|
+
@query_methods ||= {}
|
23
|
+
@query_methods[name] = {
|
24
|
+
params:params,
|
25
|
+
block:blk
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(destination, username=nil, password=nil, timeout=300)
|
31
|
+
super(destination, username, password)
|
32
|
+
@timeout = timeout
|
33
|
+
@query_cache = {}
|
34
|
+
@cache = {}
|
35
|
+
@update_thread = nil
|
36
|
+
@query_methods = self.class.query_methods.clone rescue {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_update_request
|
40
|
+
new_cache = {}
|
41
|
+
return {} if self.class.cache_methods.nil?
|
42
|
+
self.class.cache_methods.each do |method, defn|
|
43
|
+
if defn[:params][:authenticate] then
|
44
|
+
raise(AuthenticationError, "Unable to authenticate") unless authenticate_if_needed
|
45
|
+
end
|
46
|
+
|
47
|
+
request = defn[:block].nil? ? nil : defn[:block].call
|
48
|
+
new_cache[method] = update_from_method(method, request)
|
49
|
+
end
|
50
|
+
new_cache
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_from_method(method, request, type=nil)
|
54
|
+
type ||= talk.expand_name(talk.method_named(method.to_s)[0][:request])
|
55
|
+
inject_token(request, false, type)
|
56
|
+
body = request.nil? ? empty_query(method) : wrapped_message(method.to_s, type, request)
|
57
|
+
result = send_message(body)
|
58
|
+
begin
|
59
|
+
parsed = symbolify(JSON.parse(result.body))
|
60
|
+
raise(UpdateError, "#{method}: #{parsed[:error][:code]} #{parsed[:error][:message]}") unless parsed[:error].nil?
|
61
|
+
raise(UpdateError, "#{method}: Server did not include a response field") if parsed[:result].nil?
|
62
|
+
toplevel = parsed[:result]
|
63
|
+
if toplevel.has_key?(:body) and toplevel.has_key?(:className) then
|
64
|
+
toplevel = toplevel[:body]
|
65
|
+
toplevel[:__class] = parsed[:result][:className] # strip namedobjectwrapper
|
66
|
+
end
|
67
|
+
toplevel
|
68
|
+
rescue JSON::ParserError
|
69
|
+
truncated = result.body.length > 100 ? result.body[0..99] + "..." : result.body
|
70
|
+
raise(UpdateError, "#{method}: Server returned non-JSON response: (#{result.body.length} bytes) '#{truncated}'")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def [](key)
|
75
|
+
cache(key)
|
76
|
+
end
|
77
|
+
|
78
|
+
def cache(key, sync=false)
|
79
|
+
if sync then
|
80
|
+
refresh unless fresh? # force caller to wait until we've refreshed
|
81
|
+
else
|
82
|
+
refresh_async unless fresh? # kick off an async update if we're stale, but still return what we have
|
83
|
+
end
|
84
|
+
|
85
|
+
@cache[key]
|
86
|
+
end
|
87
|
+
|
88
|
+
def query(method, *args)
|
89
|
+
defn = @query_methods[method]
|
90
|
+
raise UpdateError, "#{method}: Unsupported method" if defn.nil?
|
91
|
+
|
92
|
+
if defn[:params][:authenticate] then
|
93
|
+
raise(AuthenticationError, "Unable to authenticate as #{username}") unless authenticate_if_needed
|
94
|
+
end
|
95
|
+
|
96
|
+
@query_cache[method] ||= {}
|
97
|
+
item = @query_cache[method] || {}
|
98
|
+
return item[:cache] unless timestamp_expired?(item[:update])
|
99
|
+
|
100
|
+
request = defn[:block].nil? ? nil : defn[:block].call(*args)
|
101
|
+
result = update_from_method(method, request)
|
102
|
+
item[:update] = Time.now
|
103
|
+
item[:cache] = result
|
104
|
+
@query_cache[method] = item
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
# Force a cache update next time someone makes a request
|
109
|
+
def dirty
|
110
|
+
@cache_update_ts = nil
|
111
|
+
@query_methods.each { |method, defn| defn[:update] = nil }
|
112
|
+
end
|
113
|
+
|
114
|
+
def ensure_fresh
|
115
|
+
refresh unless fresh?
|
116
|
+
end
|
117
|
+
|
118
|
+
def fresh?
|
119
|
+
not timestamp_expired?(@cache_update_ts)
|
120
|
+
end
|
121
|
+
|
122
|
+
def timestamp_expired?(ts)
|
123
|
+
ts.nil? or (Time.now - ts) > @timeout
|
124
|
+
end
|
125
|
+
|
126
|
+
def refresh_async
|
127
|
+
# Create a new thread to get the update cache, unless one is already in flight
|
128
|
+
if @update_thread.nil? then
|
129
|
+
begin
|
130
|
+
Thread.new { refresh }
|
131
|
+
rescue AuthenticationError, UpdateError => exc
|
132
|
+
signal(:error, exc.message)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def refresh
|
139
|
+
unless @update_thread.nil? then
|
140
|
+
# We already have an update thread in flight; block on that
|
141
|
+
@update_thread.join unless @update_thread == Thread.current
|
142
|
+
return @cache
|
143
|
+
end
|
144
|
+
|
145
|
+
@update_thread = Thread.current
|
146
|
+
@cache = send_update_request
|
147
|
+
@cache_update_ts = Time.now
|
148
|
+
@update_thread = nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|