blob: 391145a4227fd170115d9a440854fc46166f1de7 [file] [log] [blame]
Hung-Te Linf2f78f72012-02-08 19:27:11 +08001#!/usr/bin/env python
Jon Salz27dfe032012-08-01 14:57:33 +08002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Hung-Te Linf2f78f72012-02-08 19:27:11 +08003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6
Hung-Te Lin56b18402015-01-16 14:52:30 +08007'''This module provides both client and server side of a XML RPC based server which can be used to handle factory test states (status) and shared persistent data.
Hung-Te Linf2f78f72012-02-08 19:27:11 +08008'''
9
Joel Kitchingb85ed7f2014-10-08 18:24:39 +080010
11from __future__ import print_function
12
Hung-Te Lin56b18402015-01-16 14:52:30 +080013import factory_common # pylint: disable=W0611
Hung-Te Linf2f78f72012-02-08 19:27:11 +080014
Jon Salzc7c25df2012-07-06 17:19:49 +080015import glob
Jon Salz758e6cc2012-04-03 15:47:07 +080016import logging
Jon Salz258a40c2012-04-19 12:34:01 +080017import mimetypes
Jon Salz758e6cc2012-04-03 15:47:07 +080018import os
Jon Salz5eee01c2012-06-04 16:07:33 +080019import Queue
20import re
Jon Salz758e6cc2012-04-03 15:47:07 +080021import shutil
Jon Salz258a40c2012-04-19 12:34:01 +080022import SocketServer
Jon Salz758e6cc2012-04-03 15:47:07 +080023import threading
Jon Salz258a40c2012-04-19 12:34:01 +080024import time
Jon Salzc7c25df2012-07-06 17:19:49 +080025import yaml
Jon Salz258a40c2012-04-19 12:34:01 +080026
27from hashlib import sha1
Jon Salz5eee01c2012-06-04 16:07:33 +080028from uuid import uuid4
Hung-Te Linf2f78f72012-02-08 19:27:11 +080029
Jon Salz258a40c2012-04-19 12:34:01 +080030from jsonrpclib import jsonclass
31from jsonrpclib import jsonrpc
32from jsonrpclib import SimpleJSONRPCServer
jcliangcd688182012-08-20 21:01:26 +080033from cros.factory import system
Jon Salz83591782012-06-26 11:09:58 +080034from cros.factory.test import factory
35from cros.factory.test.factory import TestState
Jon Salz83591782012-06-26 11:09:58 +080036from cros.factory.test import unicode_to_string
Joel Kitchingb85ed7f2014-10-08 18:24:39 +080037from cros.factory.utils import net_utils
Jon Salz48b01f62012-11-12 12:48:00 +080038from cros.factory.utils.shelve_utils import OpenShelfOrBackup
Jon Salzdca4aac2012-09-20 17:10:45 +080039from cros.factory.utils.string_utils import CleanUTF8
Hung-Te Linf2f78f72012-02-08 19:27:11 +080040
Jon Salz49a7d152012-06-19 15:04:09 +080041
Hung-Te Linf2f78f72012-02-08 19:27:11 +080042DEFAULT_FACTORY_STATE_PORT = 0x0FAC
Joel Kitchingb85ed7f2014-10-08 18:24:39 +080043DEFAULT_FACTORY_STATE_ADDRESS = net_utils.LOCALHOST
44DEFAULT_FACTORY_STATE_BIND_ADDRESS = net_utils.LOCALHOST
Jon Salz8796e362012-05-24 11:39:09 +080045DEFAULT_FACTORY_STATE_FILE_PATH = factory.get_state_root()
Hung-Te Linf2f78f72012-02-08 19:27:11 +080046
Ricky Liangb7eb8772014-09-15 18:05:22 +080047POST_SHUTDOWN_TAG = '%s.post_shutdown'
48
Hung-Te Linf2f78f72012-02-08 19:27:11 +080049
50def _synchronized(f):
Hung-Te Lin56b18402015-01-16 14:52:30 +080051 '''Decorates a function to grab a lock.
Jon Salz0697cbf2012-07-04 15:14:04 +080052 '''
Hung-Te Lin56b18402015-01-16 14:52:30 +080053
Jon Salz0697cbf2012-07-04 15:14:04 +080054 def wrapped(self, *args, **kw):
Hung-Te Lin56b18402015-01-16 14:52:30 +080055 with self._lock: # pylint: disable=W0212
Jon Salz0697cbf2012-07-04 15:14:04 +080056 return f(self, *args, **kw)
57 return wrapped
Hung-Te Linf2f78f72012-02-08 19:27:11 +080058
59
Jon Salz758e6cc2012-04-03 15:47:07 +080060def clear_state(state_file_path=None):
Jon Salz0697cbf2012-07-04 15:14:04 +080061 '''Clears test state (removes the state file path).
Jon Salz758e6cc2012-04-03 15:47:07 +080062
Jon Salz0697cbf2012-07-04 15:14:04 +080063 Args:
64 state_file_path: Path to state; uses the default path if None.
65 '''
66 state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
Hung-Te Lin56b18402015-01-16 14:52:30 +080067 logging.warn('Clearing state file path %s', state_file_path)
Jon Salz0697cbf2012-07-04 15:14:04 +080068 if os.path.exists(state_file_path):
69 shutil.rmtree(state_file_path)
Jon Salz758e6cc2012-04-03 15:47:07 +080070
71
Jon Salzaeb4fd42012-06-05 15:08:30 +080072class PathResolver(object):
Hung-Te Lin56b18402015-01-16 14:52:30 +080073 """Resolves paths in URLs."""
74
Jon Salz0697cbf2012-07-04 15:14:04 +080075 def __init__(self):
76 self._paths = {}
Jon Salzaeb4fd42012-06-05 15:08:30 +080077
Jon Salz0697cbf2012-07-04 15:14:04 +080078 def AddPath(self, url_path, local_path):
79 '''Adds a prefix mapping:
Jon Salzaeb4fd42012-06-05 15:08:30 +080080
Jon Salz0697cbf2012-07-04 15:14:04 +080081 For example,
Jon Salzaeb4fd42012-06-05 15:08:30 +080082
Jon Salz0697cbf2012-07-04 15:14:04 +080083 AddPath('/foo', '/usr/local/docs')
Jon Salzaeb4fd42012-06-05 15:08:30 +080084
Jon Salz0697cbf2012-07-04 15:14:04 +080085 will cause paths to resolved as follows:
Jon Salzaeb4fd42012-06-05 15:08:30 +080086
Jon Salz0697cbf2012-07-04 15:14:04 +080087 /foo -> /usr/local/docs
88 /foo/index.html -> /usr/local/docs/index.html
Jon Salzaeb4fd42012-06-05 15:08:30 +080089
Jon Salz0697cbf2012-07-04 15:14:04 +080090 Args:
91 url_path: The path in the URL
92 '''
93 self._paths[url_path] = local_path
Jon Salzaeb4fd42012-06-05 15:08:30 +080094
Jon Salz0697cbf2012-07-04 15:14:04 +080095 def Resolve(self, url_path):
96 '''Resolves a path mapping.
Jon Salzaeb4fd42012-06-05 15:08:30 +080097
Jon Salz0697cbf2012-07-04 15:14:04 +080098 Returns None if no paths match.'
Jon Salzaeb4fd42012-06-05 15:08:30 +080099
Jon Salz0697cbf2012-07-04 15:14:04 +0800100 Args:
101 url_path: A path in a URL (starting with /).
102 '''
103 if not url_path.startswith('/'):
104 return None
Jon Salzaeb4fd42012-06-05 15:08:30 +0800105
Jon Salz0697cbf2012-07-04 15:14:04 +0800106 prefix = url_path
107 while prefix != '':
108 local_prefix = self._paths.get(prefix)
109 if local_prefix:
110 return local_prefix + url_path[len(prefix):]
111 prefix, _, _ = prefix.rpartition('/')
Jon Salzaeb4fd42012-06-05 15:08:30 +0800112
Jon Salz0697cbf2012-07-04 15:14:04 +0800113 root_prefix = self._paths.get('/')
114 if root_prefix:
115 return root_prefix + url_path
Jon Salzaeb4fd42012-06-05 15:08:30 +0800116
117
Jon Salz258a40c2012-04-19 12:34:01 +0800118@unicode_to_string.UnicodeToStringClass
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800119class FactoryState(object):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800120 '''The core implementation for factory state control.
Jon Salz0697cbf2012-07-04 15:14:04 +0800121
Hung-Te Lin56b18402015-01-16 14:52:30 +0800122 The major provided features are:
Jon Salz0697cbf2012-07-04 15:14:04 +0800123 SHARED DATA
124 You can get/set simple data into the states and share between all tests.
125 See get_shared_data(name) and set_shared_data(name, value) for more
126 information.
127
128 TEST STATUS
129 To track the execution status of factory auto tests, you can use
130 get_test_state, get_test_states methods, and update_test_state
131 methods.
132
133 All arguments may be provided either as strings, or as Unicode strings in
134 which case they are converted to strings using UTF-8. All returned values
135 are strings (not Unicode).
136
137 This object is thread-safe.
138
139 See help(FactoryState.[methodname]) for more information.
140
141 Properties:
142 _generated_files: Map from UUID to paths on disk. These are
143 not persisted on disk (though they could be if necessary).
144 _generated_data: Map from UUID to (mime_type, data) pairs for
145 transient objects to serve.
146 _generated_data_expiration: Priority queue of expiration times
147 for objects in _generated_data.
148 '''
149
150 def __init__(self, state_file_path=None):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800151 '''Initializes the state server.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800152
Jon Salz0697cbf2012-07-04 15:14:04 +0800153 Parameters:
154 state_file_path: External file to store the state information.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800155 '''
Jon Salz0697cbf2012-07-04 15:14:04 +0800156 state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
157 if not os.path.exists(state_file_path):
158 os.makedirs(state_file_path)
Jon Salz48b01f62012-11-12 12:48:00 +0800159 self._tests_shelf = OpenShelfOrBackup(state_file_path + '/tests')
160 self._data_shelf = OpenShelfOrBackup(state_file_path + '/data')
Jon Salz0697cbf2012-07-04 15:14:04 +0800161 self._lock = threading.RLock()
162 self.test_list_struct = None
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800163
Jon Salz0697cbf2012-07-04 15:14:04 +0800164 self._generated_files = {}
165 self._generated_data = {}
166 self._generated_data_expiration = Queue.PriorityQueue()
167 self._resolver = PathResolver()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800168
Jon Salz0697cbf2012-07-04 15:14:04 +0800169 if TestState not in jsonclass.supported_types:
170 jsonclass.supported_types.append(TestState)
Jon Salz258a40c2012-04-19 12:34:01 +0800171
Jon Salz0697cbf2012-07-04 15:14:04 +0800172 @_synchronized
173 def close(self):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800174 '''Shuts down the state instance.
Jon Salz0697cbf2012-07-04 15:14:04 +0800175 '''
176 for shelf in [self._tests_shelf,
Jon Salzc7c25df2012-07-06 17:19:49 +0800177 self._data_shelf]:
Jon Salz0697cbf2012-07-04 15:14:04 +0800178 try:
179 shelf.close()
180 except:
181 logging.exception('Unable to close shelf')
Jon Salz5eee01c2012-06-04 16:07:33 +0800182
Jon Salz0697cbf2012-07-04 15:14:04 +0800183 @_synchronized
184 def update_test_state(self, path, **kw):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800185 '''Updates the state of a test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800186
Jon Salz0697cbf2012-07-04 15:14:04 +0800187 See TestState.update for the allowable keyword arguments.
Jon Salz66f65e62012-05-24 17:40:26 +0800188
Jon Salz0697cbf2012-07-04 15:14:04 +0800189 @param path: The path to the test (see FactoryTest for a description
190 of test paths).
191 @param kw: See TestState.update for allowable arguments (e.g.,
192 status and increment_count).
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800193
Jon Salz0697cbf2012-07-04 15:14:04 +0800194 @return: A tuple containing the new state, and a boolean indicating
195 whether the state was just changed.
196 '''
197 state = self._tests_shelf.get(path)
198 old_state_repr = repr(state)
199 changed = False
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800200
Jon Salz0697cbf2012-07-04 15:14:04 +0800201 if not state:
202 changed = True
203 state = TestState()
Jon Salz20d8e932012-03-17 15:04:23 +0800204
Hung-Te Lin56b18402015-01-16 14:52:30 +0800205 changed = changed | state.update(**kw) # Don't short-circuit
Jon Salz20d8e932012-03-17 15:04:23 +0800206
Jon Salz0697cbf2012-07-04 15:14:04 +0800207 if changed:
208 logging.debug('Updating test state for %s: %s -> %s',
Hung-Te Lin56b18402015-01-16 14:52:30 +0800209 path, old_state_repr, state)
Jon Salz0697cbf2012-07-04 15:14:04 +0800210 self._tests_shelf[path] = state
211 self._tests_shelf.sync()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800212
Jon Salz0697cbf2012-07-04 15:14:04 +0800213 return state, changed
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800214
Jon Salz0697cbf2012-07-04 15:14:04 +0800215 @_synchronized
216 def get_test_state(self, path):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800217 '''Returns the state of a test.
Jon Salz0697cbf2012-07-04 15:14:04 +0800218 '''
219 return self._tests_shelf[path]
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800220
Jon Salz0697cbf2012-07-04 15:14:04 +0800221 @_synchronized
222 def get_test_paths(self):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800223 '''Returns a list of all tests' paths.
Jon Salz0697cbf2012-07-04 15:14:04 +0800224 '''
225 return self._tests_shelf.keys()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800226
Jon Salz0697cbf2012-07-04 15:14:04 +0800227 @_synchronized
228 def get_test_states(self):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800229 '''Returns a map of each test's path to its state.
Jon Salz0697cbf2012-07-04 15:14:04 +0800230 '''
231 return dict(self._tests_shelf)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800232
Jon Salz822838b2013-03-25 17:32:33 +0800233 @_synchronized
234 def clear_test_state(self):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800235 '''Clears all test state.
Jon Salz822838b2013-03-25 17:32:33 +0800236 '''
237 self._tests_shelf.clear()
238
Jon Salz0697cbf2012-07-04 15:14:04 +0800239 def get_test_list(self):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800240 '''Returns the test list.
Jon Salz0697cbf2012-07-04 15:14:04 +0800241 '''
242 return self.test_list.to_struct()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800243
Jon Salz0697cbf2012-07-04 15:14:04 +0800244 @_synchronized
245 def set_shared_data(self, *key_value_pairs):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800246 '''Sets shared data items.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800247
Jon Salz0697cbf2012-07-04 15:14:04 +0800248 Args:
249 key_value_pairs: A series of alternating keys and values
250 (k1, v1, k2, v2...). In the simple case this can just
251 be a single key and value.
252 '''
253 assert len(key_value_pairs) % 2 == 0, repr(key_value_pairs)
254 for i in range(0, len(key_value_pairs), 2):
255 self._data_shelf[key_value_pairs[i]] = key_value_pairs[i + 1]
256 self._data_shelf.sync()
Jon Salz258a40c2012-04-19 12:34:01 +0800257
Jon Salz0697cbf2012-07-04 15:14:04 +0800258 @_synchronized
259 def get_shared_data(self, key, optional=False):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800260 '''Retrieves a shared data item.
Jon Salzb1b39092012-05-03 02:05:09 +0800261
Jon Salz0697cbf2012-07-04 15:14:04 +0800262 Args:
263 key: The key whose value to retrieve.
264 optional: True to return None if not found; False to raise
265 a KeyError.
266 '''
267 if optional:
268 return self._data_shelf.get(key)
269 else:
270 return self._data_shelf[key]
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800271
Jon Salz0697cbf2012-07-04 15:14:04 +0800272 @_synchronized
273 def has_shared_data(self, key):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800274 '''Returns if a shared data item exists.
Jon Salz0697cbf2012-07-04 15:14:04 +0800275 '''
276 return key in self._data_shelf
Jon Salz4f6c7172012-06-11 20:45:36 +0800277
Jon Salz0697cbf2012-07-04 15:14:04 +0800278 @_synchronized
279 def del_shared_data(self, key, optional=False):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800280 '''Deletes a shared data item.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800281
Jon Salz0697cbf2012-07-04 15:14:04 +0800282 Args:
283 key: The key whose value to retrieve.
284 optional: False to raise a KeyError if not found.
285 '''
286 try:
287 del self._data_shelf[key]
288 except KeyError:
289 if not optional:
290 raise
Hung-Te Lin163f7512012-02-17 18:58:57 +0800291
Jon Salz33979f62013-03-07 17:11:41 +0800292 @_synchronized
293 def update_shared_data_dict(self, key, new_data):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800294 '''Updates values a shared data item whose value is a dictionary.
Jon Salz33979f62013-03-07 17:11:41 +0800295
296 This is roughly equivalent to
297
298 data = get_shared_data(key) or {}
299 data.update(new_data)
300 set_shared_data(key, data)
301 return data
302
303 except that it is atomic.
304
305 Args:
306 key: The key for the data item to update.
307 new_data: A dictionary of items to update.
308
309 Returns:
310 The updated value.
311 '''
312 data = self._data_shelf.get(key, {})
313 data.update(new_data)
314 self._data_shelf[key] = data
315 return data
316
Vic Yangaabf9fd2013-04-09 18:56:13 +0800317 @_synchronized
Cheng-Yi Chiang2d92cde2013-10-04 20:20:33 +0800318 def delete_shared_data_dict_item(self, shared_data_key,
319 delete_keys, optional):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800320 '''Deletes items from a shared data item whose value is a dict.
Cheng-Yi Chiang2d92cde2013-10-04 20:20:33 +0800321
322 This is roughly equivalent to
323
324 data = get_shared_data(shared_data_key) or {}
325 for key in delete_keys:
326 try:
327 del data[key]
328 except KeyError:
329 if not optional:
330 raise
331 set_shared_data(shared_data_key, data)
332 return data
333
334 except that it is atomic.
335
336 Args:
337 shared_data_key: The key for the data item to update.
338 delete_keys: A list of keys to delete from the dict.
339 optional: False to raise a KeyError if not found.
340
341 Returns:
342 The updated value.
343 '''
344 data = self._data_shelf.get(shared_data_key, {})
345 for key in delete_keys:
346 try:
347 del data[key]
348 except KeyError:
349 if not optional:
350 raise
351 self._data_shelf[shared_data_key] = data
352 return data
353
354 @_synchronized
Vic Yangaabf9fd2013-04-09 18:56:13 +0800355 def append_shared_data_list(self, key, new_item):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800356 '''Appends an item to a shared data item whose value is a list.
Vic Yangaabf9fd2013-04-09 18:56:13 +0800357
358 This is roughly equivalent to
359
360 data = get_shared_data(key) or []
361 data.append(new_item)
362 set_shared_data(key, data)
363 return data
364
365 except that it is atomic.
366
367 Args:
368 key: The key for the data item to append.
369 new_item: The item to be appended.
370
371 Returns:
372 The updated value.
373 '''
374 data = self._data_shelf.get(key, [])
375 data.append(new_item)
376 self._data_shelf[key] = data
377 return data
378
Jon Salzc7c25df2012-07-06 17:19:49 +0800379 def get_test_history(self, *test_paths):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800380 """Returns metadata for all previous (and current) runs of a test."""
Jon Salz0697cbf2012-07-04 15:14:04 +0800381 ret = []
382
Jon Salzc7c25df2012-07-06 17:19:49 +0800383 for path in test_paths:
384 for f in glob.glob(os.path.join(factory.get_test_data_root(),
385 path + '-*',
386 'metadata')):
387 try:
388 ret.append(yaml.load(open(f)))
389 except:
390 logging.exception('Unable to load test metadata %s', f)
Jon Salz0697cbf2012-07-04 15:14:04 +0800391
Jon Salz27dfe032012-08-01 14:57:33 +0800392 ret.sort(key=lambda item: item.get('init_time', None))
Jon Salz0697cbf2012-07-04 15:14:04 +0800393 return ret
394
Jon Salzc7c25df2012-07-06 17:19:49 +0800395 def get_test_history_entry(self, path, invocation):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800396 """Returns metadata and log for one test invocation."""
Jon Salzc7c25df2012-07-06 17:19:49 +0800397 test_dir = os.path.join(factory.get_test_data_root(),
398 '%s-%s' % (path, invocation))
399
400 log_file = os.path.join(test_dir, 'log')
401 try:
Jon Salzdca4aac2012-09-20 17:10:45 +0800402 log = CleanUTF8(open(log_file).read())
Jon Salzc7c25df2012-07-06 17:19:49 +0800403 except:
404 # Oh well
405 logging.exception('Unable to read log file %s', log_file)
406 log = None
407
408 return {'metadata': yaml.load(open(os.path.join(test_dir, 'metadata'))),
409 'log': log}
410
Jon Salz0697cbf2012-07-04 15:14:04 +0800411 @_synchronized
412 def url_for_file(self, path):
413 '''Returns a URL that can be used to serve a local file.
414
415 Args:
416 path: path to the local file
417
418 Returns:
419 url: A (possibly relative) URL that refers to the file
420 '''
421 uuid = str(uuid4())
422 uri_path = '/generated-files/%s/%s' % (uuid, os.path.basename(path))
Ricky Liangfaa96122014-11-10 10:59:28 +0800423 self._resolver.AddPath('/generated-files/%s' % uuid, os.path.dirname(path))
Jon Salz0697cbf2012-07-04 15:14:04 +0800424 self._generated_files[uuid] = path
425 return uri_path
426
427 @_synchronized
428 def url_for_data(self, mime_type, data, expiration_secs=None):
429 '''Returns a URL that can be used to serve a static collection
430 of bytes.
431
432 Args:
433 mime_type: MIME type for the data
434 data: Data to serve
435 expiration_secs: If not None, the number of seconds in which
436 the data will expire.
437 '''
438 uuid = str(uuid4())
439 self._generated_data[uuid] = mime_type, data
440 if expiration_secs:
441 now = time.time()
442 self._generated_data_expiration.put(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800443 (now + expiration_secs, uuid))
Jon Salz0697cbf2012-07-04 15:14:04 +0800444
445 # Reap old items.
446 while True:
Jon Salz4f6c7172012-06-11 20:45:36 +0800447 try:
Jon Salz0697cbf2012-07-04 15:14:04 +0800448 item = self._generated_data_expiration.get_nowait()
449 except Queue.Empty:
450 break
Hung-Te Lin163f7512012-02-17 18:58:57 +0800451
Jon Salz0697cbf2012-07-04 15:14:04 +0800452 if item[0] < now:
453 del self._generated_data[item[1]]
454 else:
455 # Not expired yet; put it back and we're done
456 self._generated_data_expiration.put(item)
457 break
458 uri_path = '/generated-data/%s' % uuid
459 return uri_path
Jon Salz258a40c2012-04-19 12:34:01 +0800460
Jon Salz0697cbf2012-07-04 15:14:04 +0800461 @_synchronized
462 def register_path(self, url_path, local_path):
463 self._resolver.AddPath(url_path, local_path)
Jon Salz258a40c2012-04-19 12:34:01 +0800464
Jon Salz0697cbf2012-07-04 15:14:04 +0800465 def get_system_status(self):
466 '''Returns system status information.
Jon Salz258a40c2012-04-19 12:34:01 +0800467
Jon Salz0697cbf2012-07-04 15:14:04 +0800468 This may include system load, battery status, etc. See
Hung-Te Lin8fa29062015-11-25 18:00:59 +0800469 system.state.SystemStatus().
Jon Salz0697cbf2012-07-04 15:14:04 +0800470 '''
Hung-Te Lin8fa29062015-11-25 18:00:59 +0800471 return system.state.SystemStatus().__dict__
Jon Salz49a7d152012-06-19 15:04:09 +0800472
Jon Salzaeb4fd42012-06-05 15:08:30 +0800473
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800474def get_instance(address=DEFAULT_FACTORY_STATE_ADDRESS,
Jon Salz1ff207f2014-07-03 14:16:04 +0800475 port=None):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800476 '''Gets an instance (for client side) to access the state server.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800477
Jon Salz0697cbf2012-07-04 15:14:04 +0800478 @param address: Address of the server to be connected.
Jon Salz1ff207f2014-07-03 14:16:04 +0800479 @param port: Port of the server to be connected. Defaults to
480 DEFAULT_FACTORY_STATE_PORT.
Jon Salz0697cbf2012-07-04 15:14:04 +0800481 @return An object with all public functions from FactoryState.
482 See help(FactoryState) for more information.
483 '''
Jon Salz1ff207f2014-07-03 14:16:04 +0800484 return jsonrpc.ServerProxy('http://%s:%d' % (
485 address, port or DEFAULT_FACTORY_STATE_PORT), verbose=False)
Jon Salz258a40c2012-04-19 12:34:01 +0800486
487
488class MyJSONRPCRequestHandler(SimpleJSONRPCServer.SimpleJSONRPCRequestHandler):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800489
Jon Salz0697cbf2012-07-04 15:14:04 +0800490 def do_GET(self):
491 logging.debug('HTTP request for path %s', self.path)
Jon Salz258a40c2012-04-19 12:34:01 +0800492
Jon Salz0697cbf2012-07-04 15:14:04 +0800493 handler = self.server.handlers.get(self.path)
494 if handler:
495 return handler(self)
Jon Salz258a40c2012-04-19 12:34:01 +0800496
Hung-Te Lin56b18402015-01-16 14:52:30 +0800497 match = re.match(r'^/generated-data/([-0-9a-f]+)$', self.path)
Jon Salz0697cbf2012-07-04 15:14:04 +0800498 if match:
499 generated_data = self.server._generated_data.get(match.group(1))
500 if not generated_data:
501 logging.warn('Unknown or expired generated data %s',
Hung-Te Lin56b18402015-01-16 14:52:30 +0800502 match.group(1))
Jon Salz0697cbf2012-07-04 15:14:04 +0800503 self.send_response(404)
504 return
Jon Salz5eee01c2012-06-04 16:07:33 +0800505
Jon Salz0697cbf2012-07-04 15:14:04 +0800506 mime_type, data = generated_data
Jon Salz5eee01c2012-06-04 16:07:33 +0800507
Jon Salz0697cbf2012-07-04 15:14:04 +0800508 self.send_response(200)
509 self.send_header('Content-Type', mime_type)
510 self.send_header('Content-Length', len(data))
511 self.end_headers()
512 self.wfile.write(data)
Jon Salz5eee01c2012-06-04 16:07:33 +0800513
Jon Salz0697cbf2012-07-04 15:14:04 +0800514 if self.path.endswith('/'):
515 self.path += 'index.html'
Jon Salz258a40c2012-04-19 12:34:01 +0800516
Hung-Te Lin56b18402015-01-16 14:52:30 +0800517 if '..' in self.path.split('/'):
518 logging.warn('Invalid path')
Jon Salz0697cbf2012-07-04 15:14:04 +0800519 self.send_response(404)
520 return
Jon Salz258a40c2012-04-19 12:34:01 +0800521
Jon Salz0697cbf2012-07-04 15:14:04 +0800522 mime_type = mimetypes.guess_type(self.path)
523 if not mime_type:
Hung-Te Lin56b18402015-01-16 14:52:30 +0800524 logging.warn('Unable to guess MIME type')
Jon Salz0697cbf2012-07-04 15:14:04 +0800525 self.send_response(404)
526 return
Jon Salz258a40c2012-04-19 12:34:01 +0800527
Jon Salz0697cbf2012-07-04 15:14:04 +0800528 local_path = None
Hung-Te Lin56b18402015-01-16 14:52:30 +0800529 match = re.match(r'^/generated-files/([-0-9a-f]+)/', self.path)
Jon Salz0697cbf2012-07-04 15:14:04 +0800530 if match:
531 local_path = self.server._generated_files.get(match.group(1))
532 if not local_path:
533 logging.warn('Unknown generated file %s in path %s',
Hung-Te Lin56b18402015-01-16 14:52:30 +0800534 match.group(1), self.path)
Jon Salz0697cbf2012-07-04 15:14:04 +0800535 self.send_response(404)
536 return
Jon Salz5eee01c2012-06-04 16:07:33 +0800537
Jon Salz0697cbf2012-07-04 15:14:04 +0800538 local_path = self.server._resolver.Resolve(self.path)
539 if not local_path or not os.path.exists(local_path):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800540 logging.warn('File not found: %s', (local_path or self.path))
Jon Salz0697cbf2012-07-04 15:14:04 +0800541 self.send_response(404)
542 return
Jon Salz258a40c2012-04-19 12:34:01 +0800543
Jon Salz0697cbf2012-07-04 15:14:04 +0800544 self.send_response(200)
Hung-Te Lin56b18402015-01-16 14:52:30 +0800545 self.send_header('Content-Type', mime_type[0])
546 self.send_header('Content-Length', os.path.getsize(local_path))
Jon Salz0697cbf2012-07-04 15:14:04 +0800547 self.end_headers()
548 with open(local_path) as f:
549 shutil.copyfileobj(f, self.wfile)
Jon Salz258a40c2012-04-19 12:34:01 +0800550
551
552class ThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
Hung-Te Lin56b18402015-01-16 14:52:30 +0800553 SimpleJSONRPCServer.SimpleJSONRPCServer):
Jon Salz0697cbf2012-07-04 15:14:04 +0800554 '''The JSON/RPC server.
Jon Salz258a40c2012-04-19 12:34:01 +0800555
Jon Salz0697cbf2012-07-04 15:14:04 +0800556 Properties:
557 handlers: A map from URLs to callbacks handling them. (The callback
558 takes a single argument: the request to handle.)
559 '''
Hung-Te Lin56b18402015-01-16 14:52:30 +0800560
Jon Salz0697cbf2012-07-04 15:14:04 +0800561 def __init__(self, *args, **kwargs):
562 SimpleJSONRPCServer.SimpleJSONRPCServer.__init__(self, *args, **kwargs)
563 self.handlers = {}
Jon Salz258a40c2012-04-19 12:34:01 +0800564
Jon Salz0697cbf2012-07-04 15:14:04 +0800565 def add_handler(self, url, callback):
566 self.handlers[url] = callback
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800567
568
569def create_server(state_file_path=None, bind_address=None, port=None):
Hung-Te Lin56b18402015-01-16 14:52:30 +0800570 '''Creates a FactoryState object and an JSON/RPC server to serve it.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800571
Jon Salz0697cbf2012-07-04 15:14:04 +0800572 @param state_file_path: The path containing the saved state.
573 @param bind_address: Address to bind to, defaulting to
574 DEFAULT_FACTORY_STATE_BIND_ADDRESS.
575 @param port: Port to bind to, defaulting to DEFAULT_FACTORY_STATE_PORT.
576 @return A tuple of the FactoryState instance and the SimpleJSONRPCServer
577 instance.
578 '''
579 # We have some icons in SVG format, but this isn't recognized in
580 # the standard Python mimetypes set.
581 mimetypes.add_type('image/svg+xml', '.svg')
Jon Salz258a40c2012-04-19 12:34:01 +0800582
Jon Salz0697cbf2012-07-04 15:14:04 +0800583 if not bind_address:
584 bind_address = DEFAULT_FACTORY_STATE_BIND_ADDRESS
585 if not port:
586 port = DEFAULT_FACTORY_STATE_PORT
587 instance = FactoryState(state_file_path)
588 instance._resolver.AddPath(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800589 '/',
590 os.path.join(factory.FACTORY_PACKAGE_PATH, 'goofy/static'))
Jon Salzaeb4fd42012-06-05 15:08:30 +0800591
Jon Salz0697cbf2012-07-04 15:14:04 +0800592 server = ThreadedJSONRPCServer(
Hung-Te Lin56b18402015-01-16 14:52:30 +0800593 (bind_address, port),
594 requestHandler=MyJSONRPCRequestHandler,
595 logRequests=False)
Jon Salz258a40c2012-04-19 12:34:01 +0800596
Jon Salz0697cbf2012-07-04 15:14:04 +0800597 # Give the server the information it needs to resolve URLs.
598 server._generated_files = instance._generated_files
599 server._generated_data = instance._generated_data
600 server._resolver = instance._resolver
Jon Salz5eee01c2012-06-04 16:07:33 +0800601
Jon Salz0697cbf2012-07-04 15:14:04 +0800602 server.register_introspection_functions()
603 server.register_instance(instance)
604 server.web_socket_handler = None
605 return instance, server