blob: 420ef4d9b2ec151a30d5e7932af290498131cc4c [file] [log] [blame]
Hung-Te Linf2f78f72012-02-08 19:27:11 +08001#!/usr/bin/env python
2# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6
7'''
8This module provides both client and server side of a XML RPC based server which
9can be used to handle factory test states (status) and shared persistent data.
10'''
11
12
Jon Salz758e6cc2012-04-03 15:47:07 +080013import logging
Jon Salz258a40c2012-04-19 12:34:01 +080014import mimetypes
Jon Salz758e6cc2012-04-03 15:47:07 +080015import os
Jon Salz5eee01c2012-06-04 16:07:33 +080016import Queue
17import re
Jon Salz758e6cc2012-04-03 15:47:07 +080018import shelve
19import shutil
Jon Salz258a40c2012-04-19 12:34:01 +080020import SocketServer
Jon Salz758e6cc2012-04-03 15:47:07 +080021import sys
22import threading
Jon Salz258a40c2012-04-19 12:34:01 +080023import time
24
25from hashlib import sha1
Jon Salz5eee01c2012-06-04 16:07:33 +080026from uuid import uuid4
Hung-Te Linf2f78f72012-02-08 19:27:11 +080027
28import factory_common
Jon Salz258a40c2012-04-19 12:34:01 +080029
30from jsonrpclib import jsonclass
31from jsonrpclib import jsonrpc
32from jsonrpclib import SimpleJSONRPCServer
Jon Salz83591782012-06-26 11:09:58 +080033from cros.factory.test import factory
34from cros.factory.test.factory import TestState
35from cros.factory.goofy import system
36from cros.factory.test import unicode_to_string
Hung-Te Linf2f78f72012-02-08 19:27:11 +080037
Jon Salz49a7d152012-06-19 15:04:09 +080038
Hung-Te Linf2f78f72012-02-08 19:27:11 +080039DEFAULT_FACTORY_STATE_PORT = 0x0FAC
40DEFAULT_FACTORY_STATE_ADDRESS = 'localhost'
41DEFAULT_FACTORY_STATE_BIND_ADDRESS = 'localhost'
Jon Salz8796e362012-05-24 11:39:09 +080042DEFAULT_FACTORY_STATE_FILE_PATH = factory.get_state_root()
Hung-Te Linf2f78f72012-02-08 19:27:11 +080043
44
45def _synchronized(f):
46 '''
47 Decorates a function to grab a lock.
48 '''
49 def wrapped(self, *args, **kw):
50 with self._lock: # pylint: disable=W0212
51 return f(self, *args, **kw)
52 return wrapped
53
54
Jon Salz758e6cc2012-04-03 15:47:07 +080055def clear_state(state_file_path=None):
56 '''Clears test state (removes the state file path).
57
58 Args:
59 state_file_path: Path to state; uses the default path if None.
60 '''
61 state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
62 logging.warn('Clearing state file path %s' % state_file_path)
63 if os.path.exists(state_file_path):
64 shutil.rmtree(state_file_path)
65
66
Jon Salz258a40c2012-04-19 12:34:01 +080067class TestHistoryItem(object):
68 def __init__(self, path, state, log, trace=None):
69 self.path = path
70 self.state = state
71 self.log = log
72 self.trace = trace
73 self.time = time.time()
74
75
Jon Salzaeb4fd42012-06-05 15:08:30 +080076class PathResolver(object):
77 '''Resolves paths in URLs.'''
78 def __init__(self):
79 self._paths = {}
80
81 def AddPath(self, url_path, local_path):
82 '''Adds a prefix mapping:
83
84 For example,
85
86 AddPath('/foo', '/usr/local/docs')
87
88 will cause paths to resolved as follows:
89
90 /foo -> /usr/local/docs
91 /foo/index.html -> /usr/local/docs/index.html
92
93 Args:
94 url_path: The path in the URL
95 '''
96 self._paths[url_path] = local_path
97
98 def Resolve(self, url_path):
99 '''Resolves a path mapping.
100
101 Returns None if no paths match.'
102
103 Args:
104 url_path: A path in a URL (starting with /).
105 '''
106 if not url_path.startswith('/'):
107 return None
108
109 prefix = url_path
110 while prefix != '':
111 local_prefix = self._paths.get(prefix)
112 if local_prefix:
113 return local_prefix + url_path[len(prefix):]
114 prefix, _, _ = prefix.rpartition('/')
115
116 root_prefix = self._paths.get('/')
117 if root_prefix:
118 return root_prefix + url_path
119
120
Jon Salz258a40c2012-04-19 12:34:01 +0800121@unicode_to_string.UnicodeToStringClass
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800122class FactoryState(object):
123 '''
124 The core implementation for factory state control.
125 The major provided features are:
126
127 SHARED DATA
128 You can get/set simple data into the states and share between all tests.
129 See get_shared_data(name) and set_shared_data(name, value) for more
130 information.
131
132 TEST STATUS
133 To track the execution status of factory auto tests, you can use
134 get_test_state, get_test_states methods, and update_test_state
135 methods.
136
Jon Salz258a40c2012-04-19 12:34:01 +0800137 All arguments may be provided either as strings, or as Unicode strings in
138 which case they are converted to strings using UTF-8. All returned values
139 are strings (not Unicode).
140
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800141 This object is thread-safe.
142
143 See help(FactoryState.[methodname]) for more information.
Jon Salz5eee01c2012-06-04 16:07:33 +0800144
145 Properties:
146 _generated_files: Map from UUID to paths on disk. These are
147 not persisted on disk (though they could be if necessary).
148 _generated_data: Map from UUID to (mime_type, data) pairs for
149 transient objects to serve.
150 _generated_data_expiration: Priority queue of expiration times
151 for objects in _generated_data.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800152 '''
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800153
154 def __init__(self, state_file_path=None):
155 '''
156 Initializes the state server.
157
158 Parameters:
159 state_file_path: External file to store the state information.
160 '''
Jon Salz758e6cc2012-04-03 15:47:07 +0800161 state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800162 if not os.path.exists(state_file_path):
163 os.makedirs(state_file_path)
164 self._tests_shelf = shelve.open(state_file_path + '/tests')
165 self._data_shelf = shelve.open(state_file_path + '/data')
Jon Salz258a40c2012-04-19 12:34:01 +0800166 self._test_history_shelf = shelve.open(state_file_path +
167 '/test_history')
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800168 self._lock = threading.RLock()
Jon Salz258a40c2012-04-19 12:34:01 +0800169 self.test_list_struct = None
170
Jon Salz5eee01c2012-06-04 16:07:33 +0800171 self._generated_files = {}
172 self._generated_data = {}
173 self._generated_data_expiration = Queue.PriorityQueue()
Jon Salzaeb4fd42012-06-05 15:08:30 +0800174 self._resolver = PathResolver()
Jon Salz5eee01c2012-06-04 16:07:33 +0800175
Jon Salz258a40c2012-04-19 12:34:01 +0800176 if TestState not in jsonclass.supported_types:
177 jsonclass.supported_types.append(TestState)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800178
Jon Salz37eccbd2012-05-25 16:06:52 +0800179 @_synchronized
Jon Salz66f65e62012-05-24 17:40:26 +0800180 def close(self):
181 '''
182 Shuts down the state instance.
183 '''
184 for shelf in [self._tests_shelf,
185 self._data_shelf,
186 self._test_history_shelf]:
187 try:
188 shelf.close()
189 except:
190 logging.exception('Unable to close shelf')
191
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800192 @_synchronized
193 def update_test_state(self, path, **kw):
194 '''
195 Updates the state of a test.
196
197 See TestState.update for the allowable keyword arguments.
198
199 @param path: The path to the test (see FactoryTest for a description
200 of test paths).
201 @param kw: See TestState.update for allowable arguments (e.g.,
202 status and increment_count).
Jon Salz20d8e932012-03-17 15:04:23 +0800203
204 @return: A tuple containing the new state, and a boolean indicating
205 whether the state was just changed.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800206 '''
207 state = self._tests_shelf.get(path)
208 old_state_repr = repr(state)
Jon Salz20d8e932012-03-17 15:04:23 +0800209 changed = False
210
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800211 if not state:
Jon Salz20d8e932012-03-17 15:04:23 +0800212 changed = True
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800213 state = TestState()
214
Jon Salz20d8e932012-03-17 15:04:23 +0800215 changed = changed | state.update(**kw) # Don't short-circuit
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800216
Jon Salz20d8e932012-03-17 15:04:23 +0800217 if changed:
218 logging.debug('Updating test state for %s: %s -> %s',
219 path, old_state_repr, state)
220 self._tests_shelf[path] = state
221 self._tests_shelf.sync()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800222
Jon Salz20d8e932012-03-17 15:04:23 +0800223 return state, changed
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800224
225 @_synchronized
226 def get_test_state(self, path):
227 '''
228 Returns the state of a test.
229 '''
230 return self._tests_shelf[path]
231
232 @_synchronized
233 def get_test_paths(self):
234 '''
235 Returns a list of all tests' paths.
236 '''
237 return self._tests_shelf.keys()
238
239 @_synchronized
240 def get_test_states(self):
241 '''
242 Returns a map of each test's path to its state.
243 '''
244 return dict(self._tests_shelf)
245
Jon Salz258a40c2012-04-19 12:34:01 +0800246 def get_test_list(self):
247 '''
248 Returns the test list.
249 '''
250 return self.test_list.to_struct()
251
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800252 @_synchronized
Jon Salzb1b39092012-05-03 02:05:09 +0800253 def set_shared_data(self, *key_value_pairs):
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800254 '''
Jon Salzb1b39092012-05-03 02:05:09 +0800255 Sets shared data items.
256
257 Args:
258 key_value_pairs: A series of alternating keys and values
259 (k1, v1, k2, v2...). In the simple case this can just
260 be a single key and value.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800261 '''
Jon Salzb1b39092012-05-03 02:05:09 +0800262 assert len(key_value_pairs) % 2 == 0, repr(key_value_pairs)
263 for i in range(0, len(key_value_pairs), 2):
264 self._data_shelf[key_value_pairs[i]] = key_value_pairs[i + 1]
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800265 self._data_shelf.sync()
266
267 @_synchronized
Jon Salz4f6c7172012-06-11 20:45:36 +0800268 def get_shared_data(self, key, optional=False):
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800269 '''
270 Retrieves a shared data item.
Jon Salz4f6c7172012-06-11 20:45:36 +0800271
272 Args:
273 key: The key whose value to retrieve.
274 optional: True to return None if not found; False to raise
275 a KeyError.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800276 '''
Jon Salz4f6c7172012-06-11 20:45:36 +0800277 if optional:
278 return self._data_shelf.get(key)
279 else:
280 return self._data_shelf[key]
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800281
Hung-Te Lin163f7512012-02-17 18:58:57 +0800282 @_synchronized
283 def has_shared_data(self, key):
284 '''
285 Returns if a shared data item exists.
286 '''
287 return key in self._data_shelf
288
289 @_synchronized
Jon Salz4f6c7172012-06-11 20:45:36 +0800290 def del_shared_data(self, key, optional=False):
Hung-Te Lin163f7512012-02-17 18:58:57 +0800291 '''
292 Deletes a shared data item.
Jon Salz4f6c7172012-06-11 20:45:36 +0800293
294 Args:
295 key: The key whose value to retrieve.
296 optional: False to raise a KeyError if not found.
Hung-Te Lin163f7512012-02-17 18:58:57 +0800297 '''
Jon Salz4f6c7172012-06-11 20:45:36 +0800298 try:
299 del self._data_shelf[key]
300 except KeyError:
301 if not optional:
302 raise
Hung-Te Lin163f7512012-02-17 18:58:57 +0800303
Jon Salz258a40c2012-04-19 12:34:01 +0800304 @_synchronized
305 def add_test_history(self, history_item):
306 path = history_item.path
307 assert path
308
309 length_key = path + '[length]'
310 num_entries = self._test_history_shelf.get(length_key, 0)
311 self._test_history_shelf[path + '[%d]' % num_entries] = history_item
312 self._test_history_shelf[length_key] = num_entries + 1
313
314 @_synchronized
315 def get_test_history(self, paths):
316 if type(paths) != list:
317 paths = [paths]
318 ret = []
319
320 for path in paths:
321 i = 0
322 while True:
323 value = self._test_history_shelf.get(path + '[%d]' % i)
324
325 i += 1
326 if not value:
327 break
328 ret.append(value)
329
330 ret.sort(key=lambda item: item.time)
331
332 return ret
333
Jon Salz5eee01c2012-06-04 16:07:33 +0800334 @_synchronized
335 def url_for_file(self, path):
336 '''Returns a URL that can be used to serve a local file.
337
338 Args:
339 path: path to the local file
340
341 Returns:
342 url: A (possibly relative) URL that refers to the file
343 '''
344 uuid = str(uuid4())
345 uri_path = '/generated-files/%s/%s' % (uuid, os.path.basename(path))
346 self._generated_files[uuid] = path
347 return uri_path
348
349 @_synchronized
350 def url_for_data(self, mime_type, data, expiration_secs=None):
351 '''Returns a URL that can be used to serve a static collection
352 of bytes.
353
354 Args:
355 mime_type: MIME type for the data
356 data: Data to serve
357 expiration_secs: If not None, the number of seconds in which
358 the data will expire.
359 '''
360 uuid = str(uuid4())
361 self._generated_data[uuid] = mime_type, data
362 if expiration_secs:
363 now = time.time()
364 self._generated_data_expiration.put(
365 (now + expiration_secs, uuid))
366
367 # Reap old items.
368 while True:
369 try:
370 item = self._generated_data_expiration.get_nowait()
371 except Queue.Empty:
372 break
373
374 if item[0] < now:
375 del self._generated_data[item[1]]
376 else:
377 # Not expired yet; put it back and we're done
378 self._generated_data_expiration.put(item)
379 break
380 uri_path = '/generated-data/%s' % uuid
381 return uri_path
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800382
Jon Salzaeb4fd42012-06-05 15:08:30 +0800383 @_synchronized
384 def register_path(self, url_path, local_path):
385 self._resolver.AddPath(url_path, local_path)
386
Jon Salz49a7d152012-06-19 15:04:09 +0800387 def get_system_status(self):
388 '''Returns system status information.
389
390 This may include system load, battery status, etc. See
391 system.SystemStatus().
392 '''
393 return system.SystemStatus().__dict__
394
Jon Salzaeb4fd42012-06-05 15:08:30 +0800395
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800396def get_instance(address=DEFAULT_FACTORY_STATE_ADDRESS,
397 port=DEFAULT_FACTORY_STATE_PORT):
398 '''
399 Gets an instance (for client side) to access the state server.
400
401 @param address: Address of the server to be connected.
402 @param port: Port of the server to be connected.
403 @return An object with all public functions from FactoryState.
404 See help(FactoryState) for more information.
405 '''
Jon Salz258a40c2012-04-19 12:34:01 +0800406 return jsonrpc.ServerProxy('http://%s:%d' % (address, port),
407 verbose=False)
408
409
410class MyJSONRPCRequestHandler(SimpleJSONRPCServer.SimpleJSONRPCRequestHandler):
411 def do_GET(self):
Jon Salzd6361c22012-06-11 22:23:57 +0800412 logging.debug('HTTP request for path %s', self.path)
Jon Salz258a40c2012-04-19 12:34:01 +0800413
414 handler = self.server.handlers.get(self.path)
415 if handler:
416 return handler(self)
417
Jon Salz5eee01c2012-06-04 16:07:33 +0800418 match = re.match('^/generated-data/([-0-9a-f]+)$', self.path)
419 if match:
420 generated_data = self.server._generated_data.get(match.group(1))
421 if not generated_data:
422 logging.warn('Unknown or expired generated data %s',
423 match.group(1))
424 self.send_response(404)
425 return
426
427 mime_type, data = generated_data
428
429 self.send_response(200)
430 self.send_header('Content-Type', mime_type)
431 self.send_header('Content-Length', len(data))
432 self.end_headers()
433 self.wfile.write(data)
434
Jon Salzaeb4fd42012-06-05 15:08:30 +0800435 if self.path.endswith('/'):
436 self.path += 'index.html'
Jon Salz258a40c2012-04-19 12:34:01 +0800437
438 if ".." in self.path.split("/"):
439 logging.warn("Invalid path")
440 self.send_response(404)
441 return
442
443 mime_type = mimetypes.guess_type(self.path)
444 if not mime_type:
445 logging.warn("Unable to guess MIME type")
446 self.send_response(404)
447 return
448
Jon Salz5eee01c2012-06-04 16:07:33 +0800449 local_path = None
450 match = re.match('^/generated-files/([-0-9a-f]+)/', self.path)
451 if match:
452 local_path = self.server._generated_files.get(match.group(1))
453 if not local_path:
454 logging.warn('Unknown generated file %s in path %s',
455 match.group(1), self.path)
456 self.send_response(404)
457 return
458
Jon Salzaeb4fd42012-06-05 15:08:30 +0800459 local_path = self.server._resolver.Resolve(self.path)
460 if not local_path or not os.path.exists(local_path):
461 logging.warn("File not found: %s", (local_path or self.path))
Jon Salz258a40c2012-04-19 12:34:01 +0800462 self.send_response(404)
463 return
464
465 self.send_response(200)
466 self.send_header("Content-Type", mime_type[0])
467 self.send_header("Content-Length", os.path.getsize(local_path))
468 self.end_headers()
469 with open(local_path) as f:
470 shutil.copyfileobj(f, self.wfile)
471
472
473class ThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
474 SimpleJSONRPCServer.SimpleJSONRPCServer):
475 '''The JSON/RPC server.
476
477 Properties:
478 handlers: A map from URLs to callbacks handling them. (The callback
479 takes a single argument: the request to handle.)
480 '''
481 def __init__(self, *args, **kwargs):
482 SimpleJSONRPCServer.SimpleJSONRPCServer.__init__(self, *args, **kwargs)
483 self.handlers = {}
484
485 def add_handler(self, url, callback):
486 self.handlers[url] = callback
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800487
488
489def create_server(state_file_path=None, bind_address=None, port=None):
490 '''
Jon Salz258a40c2012-04-19 12:34:01 +0800491 Creates a FactoryState object and an JSON/RPC server to serve it.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800492
493 @param state_file_path: The path containing the saved state.
494 @param bind_address: Address to bind to, defaulting to
495 DEFAULT_FACTORY_STATE_BIND_ADDRESS.
496 @param port: Port to bind to, defaulting to DEFAULT_FACTORY_STATE_PORT.
Jon Salz258a40c2012-04-19 12:34:01 +0800497 @return A tuple of the FactoryState instance and the SimpleJSONRPCServer
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800498 instance.
499 '''
Jon Salz258a40c2012-04-19 12:34:01 +0800500 # We have some icons in SVG format, but this isn't recognized in
501 # the standard Python mimetypes set.
502 mimetypes.add_type('image/svg+xml', '.svg')
503
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800504 if not bind_address:
505 bind_address = DEFAULT_FACTORY_STATE_BIND_ADDRESS
506 if not port:
507 port = DEFAULT_FACTORY_STATE_PORT
508 instance = FactoryState(state_file_path)
Jon Salzaeb4fd42012-06-05 15:08:30 +0800509 instance._resolver.AddPath(
510 '/',
Jon Salzf3b9d462012-06-26 12:35:44 +0800511 os.path.join(factory.FACTORY_PACKAGE_PATH, 'goofy/static'))
Jon Salzaeb4fd42012-06-05 15:08:30 +0800512
Jon Salz258a40c2012-04-19 12:34:01 +0800513 server = ThreadedJSONRPCServer(
514 (bind_address, port),
515 requestHandler=MyJSONRPCRequestHandler,
516 logRequests=False)
517
Jon Salzaeb4fd42012-06-05 15:08:30 +0800518 # Give the server the information it needs to resolve URLs.
Jon Salz5eee01c2012-06-04 16:07:33 +0800519 server._generated_files = instance._generated_files
520 server._generated_data = instance._generated_data
Jon Salzaeb4fd42012-06-05 15:08:30 +0800521 server._resolver = instance._resolver
Jon Salz5eee01c2012-06-04 16:07:33 +0800522
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800523 server.register_introspection_functions()
524 server.register_instance(instance)
Jon Salz258a40c2012-04-19 12:34:01 +0800525 server.web_socket_handler = None
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800526 return instance, server