blob: 866a04b2edf0ddc5e7896c3e0f03c362a3dd7643 [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
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
Jon Salz98f4f842012-11-12 18:23:20 +080012import factory_common # pylint: disable=W0611
Hung-Te Linf2f78f72012-02-08 19:27:11 +080013
Jon Salzc7c25df2012-07-06 17:19:49 +080014import glob
Jon Salz758e6cc2012-04-03 15:47:07 +080015import logging
Jon Salz258a40c2012-04-19 12:34:01 +080016import mimetypes
Jon Salz758e6cc2012-04-03 15:47:07 +080017import os
Jon Salz5eee01c2012-06-04 16:07:33 +080018import Queue
19import re
Jon Salz758e6cc2012-04-03 15:47:07 +080020import shutil
Jon Salz258a40c2012-04-19 12:34:01 +080021import SocketServer
Jon Salz758e6cc2012-04-03 15:47:07 +080022import threading
Jon Salz258a40c2012-04-19 12:34:01 +080023import time
Jon Salzc7c25df2012-07-06 17:19:49 +080024import yaml
Jon Salz258a40c2012-04-19 12:34:01 +080025
26from hashlib import sha1
Jon Salz5eee01c2012-06-04 16:07:33 +080027from uuid import uuid4
Hung-Te Linf2f78f72012-02-08 19:27:11 +080028
Jon Salz258a40c2012-04-19 12:34:01 +080029from jsonrpclib import jsonclass
30from jsonrpclib import jsonrpc
31from jsonrpclib import SimpleJSONRPCServer
jcliangcd688182012-08-20 21:01:26 +080032from cros.factory import system
Jon Salz83591782012-06-26 11:09:58 +080033from cros.factory.test import factory
34from cros.factory.test.factory import TestState
Jon Salz83591782012-06-26 11:09:58 +080035from cros.factory.test import unicode_to_string
Jon Salz48b01f62012-11-12 12:48:00 +080036from cros.factory.utils.shelve_utils import OpenShelfOrBackup
Jon Salzdca4aac2012-09-20 17:10:45 +080037from cros.factory.utils.string_utils import CleanUTF8
Hung-Te Linf2f78f72012-02-08 19:27:11 +080038
Jon Salz49a7d152012-06-19 15:04:09 +080039
Hung-Te Linf2f78f72012-02-08 19:27:11 +080040DEFAULT_FACTORY_STATE_PORT = 0x0FAC
41DEFAULT_FACTORY_STATE_ADDRESS = 'localhost'
42DEFAULT_FACTORY_STATE_BIND_ADDRESS = 'localhost'
Jon Salz8796e362012-05-24 11:39:09 +080043DEFAULT_FACTORY_STATE_FILE_PATH = factory.get_state_root()
Hung-Te Linf2f78f72012-02-08 19:27:11 +080044
Ricky Liangb7eb8772014-09-15 18:05:22 +080045POST_SHUTDOWN_TAG = '%s.post_shutdown'
46
Hung-Te Linf2f78f72012-02-08 19:27:11 +080047
48def _synchronized(f):
Jon Salz0697cbf2012-07-04 15:14:04 +080049 '''
50 Decorates a function to grab a lock.
51 '''
52 def wrapped(self, *args, **kw):
53 with self._lock: # pylint: disable=W0212
54 return f(self, *args, **kw)
55 return wrapped
Hung-Te Linf2f78f72012-02-08 19:27:11 +080056
57
Jon Salz758e6cc2012-04-03 15:47:07 +080058def clear_state(state_file_path=None):
Jon Salz0697cbf2012-07-04 15:14:04 +080059 '''Clears test state (removes the state file path).
Jon Salz758e6cc2012-04-03 15:47:07 +080060
Jon Salz0697cbf2012-07-04 15:14:04 +080061 Args:
62 state_file_path: Path to state; uses the default path if None.
63 '''
64 state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
65 logging.warn('Clearing state file path %s' % state_file_path)
66 if os.path.exists(state_file_path):
67 shutil.rmtree(state_file_path)
Jon Salz758e6cc2012-04-03 15:47:07 +080068
69
Jon Salzaeb4fd42012-06-05 15:08:30 +080070class PathResolver(object):
Jon Salz0697cbf2012-07-04 15:14:04 +080071 '''Resolves paths in URLs.'''
72 def __init__(self):
73 self._paths = {}
Jon Salzaeb4fd42012-06-05 15:08:30 +080074
Jon Salz0697cbf2012-07-04 15:14:04 +080075 def AddPath(self, url_path, local_path):
76 '''Adds a prefix mapping:
Jon Salzaeb4fd42012-06-05 15:08:30 +080077
Jon Salz0697cbf2012-07-04 15:14:04 +080078 For example,
Jon Salzaeb4fd42012-06-05 15:08:30 +080079
Jon Salz0697cbf2012-07-04 15:14:04 +080080 AddPath('/foo', '/usr/local/docs')
Jon Salzaeb4fd42012-06-05 15:08:30 +080081
Jon Salz0697cbf2012-07-04 15:14:04 +080082 will cause paths to resolved as follows:
Jon Salzaeb4fd42012-06-05 15:08:30 +080083
Jon Salz0697cbf2012-07-04 15:14:04 +080084 /foo -> /usr/local/docs
85 /foo/index.html -> /usr/local/docs/index.html
Jon Salzaeb4fd42012-06-05 15:08:30 +080086
Jon Salz0697cbf2012-07-04 15:14:04 +080087 Args:
88 url_path: The path in the URL
89 '''
90 self._paths[url_path] = local_path
Jon Salzaeb4fd42012-06-05 15:08:30 +080091
Jon Salz0697cbf2012-07-04 15:14:04 +080092 def Resolve(self, url_path):
93 '''Resolves a path mapping.
Jon Salzaeb4fd42012-06-05 15:08:30 +080094
Jon Salz0697cbf2012-07-04 15:14:04 +080095 Returns None if no paths match.'
Jon Salzaeb4fd42012-06-05 15:08:30 +080096
Jon Salz0697cbf2012-07-04 15:14:04 +080097 Args:
98 url_path: A path in a URL (starting with /).
99 '''
100 if not url_path.startswith('/'):
101 return None
Jon Salzaeb4fd42012-06-05 15:08:30 +0800102
Jon Salz0697cbf2012-07-04 15:14:04 +0800103 prefix = url_path
104 while prefix != '':
105 local_prefix = self._paths.get(prefix)
106 if local_prefix:
107 return local_prefix + url_path[len(prefix):]
108 prefix, _, _ = prefix.rpartition('/')
Jon Salzaeb4fd42012-06-05 15:08:30 +0800109
Jon Salz0697cbf2012-07-04 15:14:04 +0800110 root_prefix = self._paths.get('/')
111 if root_prefix:
112 return root_prefix + url_path
Jon Salzaeb4fd42012-06-05 15:08:30 +0800113
114
Jon Salz258a40c2012-04-19 12:34:01 +0800115@unicode_to_string.UnicodeToStringClass
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800116class FactoryState(object):
Jon Salz0697cbf2012-07-04 15:14:04 +0800117 '''
118 The core implementation for factory state control.
119 The major provided features are:
120
121 SHARED DATA
122 You can get/set simple data into the states and share between all tests.
123 See get_shared_data(name) and set_shared_data(name, value) for more
124 information.
125
126 TEST STATUS
127 To track the execution status of factory auto tests, you can use
128 get_test_state, get_test_states methods, and update_test_state
129 methods.
130
131 All arguments may be provided either as strings, or as Unicode strings in
132 which case they are converted to strings using UTF-8. All returned values
133 are strings (not Unicode).
134
135 This object is thread-safe.
136
137 See help(FactoryState.[methodname]) for more information.
138
139 Properties:
140 _generated_files: Map from UUID to paths on disk. These are
141 not persisted on disk (though they could be if necessary).
142 _generated_data: Map from UUID to (mime_type, data) pairs for
143 transient objects to serve.
144 _generated_data_expiration: Priority queue of expiration times
145 for objects in _generated_data.
146 '''
147
148 def __init__(self, state_file_path=None):
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800149 '''
Jon Salz0697cbf2012-07-04 15:14:04 +0800150 Initializes the state server.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800151
Jon Salz0697cbf2012-07-04 15:14:04 +0800152 Parameters:
153 state_file_path: External file to store the state information.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800154 '''
Jon Salz0697cbf2012-07-04 15:14:04 +0800155 state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
156 if not os.path.exists(state_file_path):
157 os.makedirs(state_file_path)
Jon Salz48b01f62012-11-12 12:48:00 +0800158 self._tests_shelf = OpenShelfOrBackup(state_file_path + '/tests')
159 self._data_shelf = OpenShelfOrBackup(state_file_path + '/data')
Jon Salz0697cbf2012-07-04 15:14:04 +0800160 self._lock = threading.RLock()
161 self.test_list_struct = None
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800162
Jon Salz0697cbf2012-07-04 15:14:04 +0800163 self._generated_files = {}
164 self._generated_data = {}
165 self._generated_data_expiration = Queue.PriorityQueue()
166 self._resolver = PathResolver()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800167
Jon Salz0697cbf2012-07-04 15:14:04 +0800168 if TestState not in jsonclass.supported_types:
169 jsonclass.supported_types.append(TestState)
Jon Salz258a40c2012-04-19 12:34:01 +0800170
Jon Salz0697cbf2012-07-04 15:14:04 +0800171 @_synchronized
172 def close(self):
173 '''
174 Shuts down the state instance.
175 '''
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):
185 '''
186 Updates the state of a test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800187
Jon Salz0697cbf2012-07-04 15:14:04 +0800188 See TestState.update for the allowable keyword arguments.
Jon Salz66f65e62012-05-24 17:40:26 +0800189
Jon Salz0697cbf2012-07-04 15:14:04 +0800190 @param path: The path to the test (see FactoryTest for a description
191 of test paths).
192 @param kw: See TestState.update for allowable arguments (e.g.,
193 status and increment_count).
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800194
Jon Salz0697cbf2012-07-04 15:14:04 +0800195 @return: A tuple containing the new state, and a boolean indicating
196 whether the state was just changed.
197 '''
198 state = self._tests_shelf.get(path)
199 old_state_repr = repr(state)
200 changed = False
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800201
Jon Salz0697cbf2012-07-04 15:14:04 +0800202 if not state:
203 changed = True
204 state = TestState()
Jon Salz20d8e932012-03-17 15:04:23 +0800205
Jon Salz0697cbf2012-07-04 15:14:04 +0800206 changed = changed | state.update(**kw) # Don't short-circuit
Jon Salz20d8e932012-03-17 15:04:23 +0800207
Jon Salz0697cbf2012-07-04 15:14:04 +0800208 if changed:
209 logging.debug('Updating test state for %s: %s -> %s',
210 path, old_state_repr, state)
211 self._tests_shelf[path] = state
212 self._tests_shelf.sync()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800213
Jon Salz0697cbf2012-07-04 15:14:04 +0800214 return state, changed
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800215
Jon Salz0697cbf2012-07-04 15:14:04 +0800216 @_synchronized
217 def get_test_state(self, path):
218 '''
219 Returns the state of a test.
220 '''
221 return self._tests_shelf[path]
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800222
Jon Salz0697cbf2012-07-04 15:14:04 +0800223 @_synchronized
224 def get_test_paths(self):
225 '''
226 Returns a list of all tests' paths.
227 '''
228 return self._tests_shelf.keys()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800229
Jon Salz0697cbf2012-07-04 15:14:04 +0800230 @_synchronized
231 def get_test_states(self):
232 '''
233 Returns a map of each test's path to its state.
234 '''
235 return dict(self._tests_shelf)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800236
Jon Salz822838b2013-03-25 17:32:33 +0800237 @_synchronized
238 def clear_test_state(self):
239 '''
240 Clears all test state.
241 '''
242 self._tests_shelf.clear()
243
Jon Salz0697cbf2012-07-04 15:14:04 +0800244 def get_test_list(self):
245 '''
246 Returns the test list.
247 '''
248 return self.test_list.to_struct()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800249
Jon Salz0697cbf2012-07-04 15:14:04 +0800250 @_synchronized
251 def set_shared_data(self, *key_value_pairs):
252 '''
253 Sets shared data items.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800254
Jon Salz0697cbf2012-07-04 15:14:04 +0800255 Args:
256 key_value_pairs: A series of alternating keys and values
257 (k1, v1, k2, v2...). In the simple case this can just
258 be a single key and value.
259 '''
260 assert len(key_value_pairs) % 2 == 0, repr(key_value_pairs)
261 for i in range(0, len(key_value_pairs), 2):
262 self._data_shelf[key_value_pairs[i]] = key_value_pairs[i + 1]
263 self._data_shelf.sync()
Jon Salz258a40c2012-04-19 12:34:01 +0800264
Jon Salz0697cbf2012-07-04 15:14:04 +0800265 @_synchronized
266 def get_shared_data(self, key, optional=False):
267 '''
268 Retrieves a shared data item.
Jon Salzb1b39092012-05-03 02:05:09 +0800269
Jon Salz0697cbf2012-07-04 15:14:04 +0800270 Args:
271 key: The key whose value to retrieve.
272 optional: True to return None if not found; False to raise
273 a KeyError.
274 '''
275 if optional:
276 return self._data_shelf.get(key)
277 else:
278 return self._data_shelf[key]
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800279
Jon Salz0697cbf2012-07-04 15:14:04 +0800280 @_synchronized
281 def has_shared_data(self, key):
282 '''
283 Returns if a shared data item exists.
284 '''
285 return key in self._data_shelf
Jon Salz4f6c7172012-06-11 20:45:36 +0800286
Jon Salz0697cbf2012-07-04 15:14:04 +0800287 @_synchronized
288 def del_shared_data(self, key, optional=False):
289 '''
290 Deletes a shared data item.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800291
Jon Salz0697cbf2012-07-04 15:14:04 +0800292 Args:
293 key: The key whose value to retrieve.
294 optional: False to raise a KeyError if not found.
295 '''
296 try:
297 del self._data_shelf[key]
298 except KeyError:
299 if not optional:
300 raise
Hung-Te Lin163f7512012-02-17 18:58:57 +0800301
Jon Salz33979f62013-03-07 17:11:41 +0800302 @_synchronized
303 def update_shared_data_dict(self, key, new_data):
304 '''
305 Updates values a shared data item whose value is a dictionary.
306
307 This is roughly equivalent to
308
309 data = get_shared_data(key) or {}
310 data.update(new_data)
311 set_shared_data(key, data)
312 return data
313
314 except that it is atomic.
315
316 Args:
317 key: The key for the data item to update.
318 new_data: A dictionary of items to update.
319
320 Returns:
321 The updated value.
322 '''
323 data = self._data_shelf.get(key, {})
324 data.update(new_data)
325 self._data_shelf[key] = data
326 return data
327
Vic Yangaabf9fd2013-04-09 18:56:13 +0800328 @_synchronized
Cheng-Yi Chiang2d92cde2013-10-04 20:20:33 +0800329 def delete_shared_data_dict_item(self, shared_data_key,
330 delete_keys, optional):
331 '''
332 Deletes items from a shared data item whose value is a dict.
333
334 This is roughly equivalent to
335
336 data = get_shared_data(shared_data_key) or {}
337 for key in delete_keys:
338 try:
339 del data[key]
340 except KeyError:
341 if not optional:
342 raise
343 set_shared_data(shared_data_key, data)
344 return data
345
346 except that it is atomic.
347
348 Args:
349 shared_data_key: The key for the data item to update.
350 delete_keys: A list of keys to delete from the dict.
351 optional: False to raise a KeyError if not found.
352
353 Returns:
354 The updated value.
355 '''
356 data = self._data_shelf.get(shared_data_key, {})
357 for key in delete_keys:
358 try:
359 del data[key]
360 except KeyError:
361 if not optional:
362 raise
363 self._data_shelf[shared_data_key] = data
364 return data
365
366 @_synchronized
Vic Yangaabf9fd2013-04-09 18:56:13 +0800367 def append_shared_data_list(self, key, new_item):
368 '''
369 Appends an item to a shared data item whose value is a list.
370
371 This is roughly equivalent to
372
373 data = get_shared_data(key) or []
374 data.append(new_item)
375 set_shared_data(key, data)
376 return data
377
378 except that it is atomic.
379
380 Args:
381 key: The key for the data item to append.
382 new_item: The item to be appended.
383
384 Returns:
385 The updated value.
386 '''
387 data = self._data_shelf.get(key, [])
388 data.append(new_item)
389 self._data_shelf[key] = data
390 return data
391
Jon Salzc7c25df2012-07-06 17:19:49 +0800392 def get_test_history(self, *test_paths):
393 '''Returns metadata for all previous (and current) runs of a test.'''
Jon Salz0697cbf2012-07-04 15:14:04 +0800394 ret = []
395
Jon Salzc7c25df2012-07-06 17:19:49 +0800396 for path in test_paths:
397 for f in glob.glob(os.path.join(factory.get_test_data_root(),
398 path + '-*',
399 'metadata')):
400 try:
401 ret.append(yaml.load(open(f)))
402 except:
403 logging.exception('Unable to load test metadata %s', f)
Jon Salz0697cbf2012-07-04 15:14:04 +0800404
Jon Salz27dfe032012-08-01 14:57:33 +0800405 ret.sort(key=lambda item: item.get('init_time', None))
Jon Salz0697cbf2012-07-04 15:14:04 +0800406 return ret
407
Jon Salzc7c25df2012-07-06 17:19:49 +0800408 def get_test_history_entry(self, path, invocation):
409 '''Returns metadata and log for one test invocation.'''
410 test_dir = os.path.join(factory.get_test_data_root(),
411 '%s-%s' % (path, invocation))
412
413 log_file = os.path.join(test_dir, 'log')
414 try:
Jon Salzdca4aac2012-09-20 17:10:45 +0800415 log = CleanUTF8(open(log_file).read())
Jon Salzc7c25df2012-07-06 17:19:49 +0800416 except:
417 # Oh well
418 logging.exception('Unable to read log file %s', log_file)
419 log = None
420
421 return {'metadata': yaml.load(open(os.path.join(test_dir, 'metadata'))),
422 'log': log}
423
Jon Salz0697cbf2012-07-04 15:14:04 +0800424 @_synchronized
425 def url_for_file(self, path):
426 '''Returns a URL that can be used to serve a local file.
427
428 Args:
429 path: path to the local file
430
431 Returns:
432 url: A (possibly relative) URL that refers to the file
433 '''
434 uuid = str(uuid4())
435 uri_path = '/generated-files/%s/%s' % (uuid, os.path.basename(path))
436 self._generated_files[uuid] = path
437 return uri_path
438
439 @_synchronized
440 def url_for_data(self, mime_type, data, expiration_secs=None):
441 '''Returns a URL that can be used to serve a static collection
442 of bytes.
443
444 Args:
445 mime_type: MIME type for the data
446 data: Data to serve
447 expiration_secs: If not None, the number of seconds in which
448 the data will expire.
449 '''
450 uuid = str(uuid4())
451 self._generated_data[uuid] = mime_type, data
452 if expiration_secs:
453 now = time.time()
454 self._generated_data_expiration.put(
455 (now + expiration_secs, uuid))
456
457 # Reap old items.
458 while True:
Jon Salz4f6c7172012-06-11 20:45:36 +0800459 try:
Jon Salz0697cbf2012-07-04 15:14:04 +0800460 item = self._generated_data_expiration.get_nowait()
461 except Queue.Empty:
462 break
Hung-Te Lin163f7512012-02-17 18:58:57 +0800463
Jon Salz0697cbf2012-07-04 15:14:04 +0800464 if item[0] < now:
465 del self._generated_data[item[1]]
466 else:
467 # Not expired yet; put it back and we're done
468 self._generated_data_expiration.put(item)
469 break
470 uri_path = '/generated-data/%s' % uuid
471 return uri_path
Jon Salz258a40c2012-04-19 12:34:01 +0800472
Jon Salz0697cbf2012-07-04 15:14:04 +0800473 @_synchronized
474 def register_path(self, url_path, local_path):
475 self._resolver.AddPath(url_path, local_path)
Jon Salz258a40c2012-04-19 12:34:01 +0800476
Jon Salz0697cbf2012-07-04 15:14:04 +0800477 def get_system_status(self):
478 '''Returns system status information.
Jon Salz258a40c2012-04-19 12:34:01 +0800479
Jon Salz0697cbf2012-07-04 15:14:04 +0800480 This may include system load, battery status, etc. See
481 system.SystemStatus().
482 '''
483 return system.SystemStatus().__dict__
Jon Salz49a7d152012-06-19 15:04:09 +0800484
Jon Salzaeb4fd42012-06-05 15:08:30 +0800485
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800486def get_instance(address=DEFAULT_FACTORY_STATE_ADDRESS,
Jon Salz1ff207f2014-07-03 14:16:04 +0800487 port=None):
Jon Salz0697cbf2012-07-04 15:14:04 +0800488 '''
489 Gets an instance (for client side) to access the state server.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800490
Jon Salz0697cbf2012-07-04 15:14:04 +0800491 @param address: Address of the server to be connected.
Jon Salz1ff207f2014-07-03 14:16:04 +0800492 @param port: Port of the server to be connected. Defaults to
493 DEFAULT_FACTORY_STATE_PORT.
Jon Salz0697cbf2012-07-04 15:14:04 +0800494 @return An object with all public functions from FactoryState.
495 See help(FactoryState) for more information.
496 '''
Jon Salz1ff207f2014-07-03 14:16:04 +0800497 return jsonrpc.ServerProxy('http://%s:%d' % (
498 address, port or DEFAULT_FACTORY_STATE_PORT), verbose=False)
Jon Salz258a40c2012-04-19 12:34:01 +0800499
500
501class MyJSONRPCRequestHandler(SimpleJSONRPCServer.SimpleJSONRPCRequestHandler):
Jon Salz0697cbf2012-07-04 15:14:04 +0800502 def do_GET(self):
503 logging.debug('HTTP request for path %s', self.path)
Jon Salz258a40c2012-04-19 12:34:01 +0800504
Jon Salz0697cbf2012-07-04 15:14:04 +0800505 handler = self.server.handlers.get(self.path)
506 if handler:
507 return handler(self)
Jon Salz258a40c2012-04-19 12:34:01 +0800508
Jon Salz0697cbf2012-07-04 15:14:04 +0800509 match = re.match('^/generated-data/([-0-9a-f]+)$', self.path)
510 if match:
511 generated_data = self.server._generated_data.get(match.group(1))
512 if not generated_data:
513 logging.warn('Unknown or expired generated data %s',
514 match.group(1))
515 self.send_response(404)
516 return
Jon Salz5eee01c2012-06-04 16:07:33 +0800517
Jon Salz0697cbf2012-07-04 15:14:04 +0800518 mime_type, data = generated_data
Jon Salz5eee01c2012-06-04 16:07:33 +0800519
Jon Salz0697cbf2012-07-04 15:14:04 +0800520 self.send_response(200)
521 self.send_header('Content-Type', mime_type)
522 self.send_header('Content-Length', len(data))
523 self.end_headers()
524 self.wfile.write(data)
Jon Salz5eee01c2012-06-04 16:07:33 +0800525
Jon Salz0697cbf2012-07-04 15:14:04 +0800526 if self.path.endswith('/'):
527 self.path += 'index.html'
Jon Salz258a40c2012-04-19 12:34:01 +0800528
Jon Salz0697cbf2012-07-04 15:14:04 +0800529 if ".." in self.path.split("/"):
530 logging.warn("Invalid path")
531 self.send_response(404)
532 return
Jon Salz258a40c2012-04-19 12:34:01 +0800533
Jon Salz0697cbf2012-07-04 15:14:04 +0800534 mime_type = mimetypes.guess_type(self.path)
535 if not mime_type:
536 logging.warn("Unable to guess MIME type")
537 self.send_response(404)
538 return
Jon Salz258a40c2012-04-19 12:34:01 +0800539
Jon Salz0697cbf2012-07-04 15:14:04 +0800540 local_path = None
541 match = re.match('^/generated-files/([-0-9a-f]+)/', self.path)
542 if match:
543 local_path = self.server._generated_files.get(match.group(1))
544 if not local_path:
545 logging.warn('Unknown generated file %s in path %s',
546 match.group(1), self.path)
547 self.send_response(404)
548 return
Jon Salz5eee01c2012-06-04 16:07:33 +0800549
Jon Salz0697cbf2012-07-04 15:14:04 +0800550 local_path = self.server._resolver.Resolve(self.path)
551 if not local_path or not os.path.exists(local_path):
552 logging.warn("File not found: %s", (local_path or self.path))
553 self.send_response(404)
554 return
Jon Salz258a40c2012-04-19 12:34:01 +0800555
Jon Salz0697cbf2012-07-04 15:14:04 +0800556 self.send_response(200)
557 self.send_header("Content-Type", mime_type[0])
558 self.send_header("Content-Length", os.path.getsize(local_path))
559 self.end_headers()
560 with open(local_path) as f:
561 shutil.copyfileobj(f, self.wfile)
Jon Salz258a40c2012-04-19 12:34:01 +0800562
563
564class ThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
Jon Salz0697cbf2012-07-04 15:14:04 +0800565 SimpleJSONRPCServer.SimpleJSONRPCServer):
566 '''The JSON/RPC server.
Jon Salz258a40c2012-04-19 12:34:01 +0800567
Jon Salz0697cbf2012-07-04 15:14:04 +0800568 Properties:
569 handlers: A map from URLs to callbacks handling them. (The callback
570 takes a single argument: the request to handle.)
571 '''
572 def __init__(self, *args, **kwargs):
573 SimpleJSONRPCServer.SimpleJSONRPCServer.__init__(self, *args, **kwargs)
574 self.handlers = {}
Jon Salz258a40c2012-04-19 12:34:01 +0800575
Jon Salz0697cbf2012-07-04 15:14:04 +0800576 def add_handler(self, url, callback):
577 self.handlers[url] = callback
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800578
579
580def create_server(state_file_path=None, bind_address=None, port=None):
Jon Salz0697cbf2012-07-04 15:14:04 +0800581 '''
582 Creates a FactoryState object and an JSON/RPC server to serve it.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800583
Jon Salz0697cbf2012-07-04 15:14:04 +0800584 @param state_file_path: The path containing the saved state.
585 @param bind_address: Address to bind to, defaulting to
586 DEFAULT_FACTORY_STATE_BIND_ADDRESS.
587 @param port: Port to bind to, defaulting to DEFAULT_FACTORY_STATE_PORT.
588 @return A tuple of the FactoryState instance and the SimpleJSONRPCServer
589 instance.
590 '''
591 # We have some icons in SVG format, but this isn't recognized in
592 # the standard Python mimetypes set.
593 mimetypes.add_type('image/svg+xml', '.svg')
Jon Salz258a40c2012-04-19 12:34:01 +0800594
Jon Salz0697cbf2012-07-04 15:14:04 +0800595 if not bind_address:
596 bind_address = DEFAULT_FACTORY_STATE_BIND_ADDRESS
597 if not port:
598 port = DEFAULT_FACTORY_STATE_PORT
599 instance = FactoryState(state_file_path)
600 instance._resolver.AddPath(
601 '/',
602 os.path.join(factory.FACTORY_PACKAGE_PATH, 'goofy/static'))
Jon Salzaeb4fd42012-06-05 15:08:30 +0800603
Jon Salz0697cbf2012-07-04 15:14:04 +0800604 server = ThreadedJSONRPCServer(
605 (bind_address, port),
606 requestHandler=MyJSONRPCRequestHandler,
607 logRequests=False)
Jon Salz258a40c2012-04-19 12:34:01 +0800608
Jon Salz0697cbf2012-07-04 15:14:04 +0800609 # Give the server the information it needs to resolve URLs.
610 server._generated_files = instance._generated_files
611 server._generated_data = instance._generated_data
612 server._resolver = instance._resolver
Jon Salz5eee01c2012-06-04 16:07:33 +0800613
Jon Salz0697cbf2012-07-04 15:14:04 +0800614 server.register_introspection_functions()
615 server.register_instance(instance)
616 server.web_socket_handler = None
617 return instance, server