blob: d6cc273833de933341b9b40135e7ee1ee45e70eb [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 shelve
21import shutil
Jon Salz258a40c2012-04-19 12:34:01 +080022import SocketServer
Jon Salz758e6cc2012-04-03 15:47:07 +080023import sys
24import threading
Jon Salz258a40c2012-04-19 12:34:01 +080025import time
Jon Salzc7c25df2012-07-06 17:19:49 +080026import yaml
Jon Salz258a40c2012-04-19 12:34:01 +080027
28from hashlib import sha1
Jon Salz5eee01c2012-06-04 16:07:33 +080029from uuid import uuid4
Hung-Te Linf2f78f72012-02-08 19:27:11 +080030
Jon Salz258a40c2012-04-19 12:34:01 +080031from jsonrpclib import jsonclass
32from jsonrpclib import jsonrpc
33from jsonrpclib import SimpleJSONRPCServer
jcliangcd688182012-08-20 21:01:26 +080034from cros.factory import system
Jon Salz83591782012-06-26 11:09:58 +080035from cros.factory.test import factory
36from cros.factory.test.factory import TestState
Jon Salz83591782012-06-26 11:09:58 +080037from cros.factory.test import unicode_to_string
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
43DEFAULT_FACTORY_STATE_ADDRESS = 'localhost'
44DEFAULT_FACTORY_STATE_BIND_ADDRESS = '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
47
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
329 def append_shared_data_list(self, key, new_item):
330 '''
331 Appends an item to a shared data item whose value is a list.
332
333 This is roughly equivalent to
334
335 data = get_shared_data(key) or []
336 data.append(new_item)
337 set_shared_data(key, data)
338 return data
339
340 except that it is atomic.
341
342 Args:
343 key: The key for the data item to append.
344 new_item: The item to be appended.
345
346 Returns:
347 The updated value.
348 '''
349 data = self._data_shelf.get(key, [])
350 data.append(new_item)
351 self._data_shelf[key] = data
352 return data
353
Jon Salzc7c25df2012-07-06 17:19:49 +0800354 def get_test_history(self, *test_paths):
355 '''Returns metadata for all previous (and current) runs of a test.'''
Jon Salz0697cbf2012-07-04 15:14:04 +0800356 ret = []
357
Jon Salzc7c25df2012-07-06 17:19:49 +0800358 for path in test_paths:
359 for f in glob.glob(os.path.join(factory.get_test_data_root(),
360 path + '-*',
361 'metadata')):
362 try:
363 ret.append(yaml.load(open(f)))
364 except:
365 logging.exception('Unable to load test metadata %s', f)
Jon Salz0697cbf2012-07-04 15:14:04 +0800366
Jon Salz27dfe032012-08-01 14:57:33 +0800367 ret.sort(key=lambda item: item.get('init_time', None))
Jon Salz0697cbf2012-07-04 15:14:04 +0800368 return ret
369
Jon Salzc7c25df2012-07-06 17:19:49 +0800370 def get_test_history_entry(self, path, invocation):
371 '''Returns metadata and log for one test invocation.'''
372 test_dir = os.path.join(factory.get_test_data_root(),
373 '%s-%s' % (path, invocation))
374
375 log_file = os.path.join(test_dir, 'log')
376 try:
Jon Salzdca4aac2012-09-20 17:10:45 +0800377 log = CleanUTF8(open(log_file).read())
Jon Salzc7c25df2012-07-06 17:19:49 +0800378 except:
379 # Oh well
380 logging.exception('Unable to read log file %s', log_file)
381 log = None
382
383 return {'metadata': yaml.load(open(os.path.join(test_dir, 'metadata'))),
384 'log': log}
385
Jon Salz0697cbf2012-07-04 15:14:04 +0800386 @_synchronized
387 def url_for_file(self, path):
388 '''Returns a URL that can be used to serve a local file.
389
390 Args:
391 path: path to the local file
392
393 Returns:
394 url: A (possibly relative) URL that refers to the file
395 '''
396 uuid = str(uuid4())
397 uri_path = '/generated-files/%s/%s' % (uuid, os.path.basename(path))
398 self._generated_files[uuid] = path
399 return uri_path
400
401 @_synchronized
402 def url_for_data(self, mime_type, data, expiration_secs=None):
403 '''Returns a URL that can be used to serve a static collection
404 of bytes.
405
406 Args:
407 mime_type: MIME type for the data
408 data: Data to serve
409 expiration_secs: If not None, the number of seconds in which
410 the data will expire.
411 '''
412 uuid = str(uuid4())
413 self._generated_data[uuid] = mime_type, data
414 if expiration_secs:
415 now = time.time()
416 self._generated_data_expiration.put(
417 (now + expiration_secs, uuid))
418
419 # Reap old items.
420 while True:
Jon Salz4f6c7172012-06-11 20:45:36 +0800421 try:
Jon Salz0697cbf2012-07-04 15:14:04 +0800422 item = self._generated_data_expiration.get_nowait()
423 except Queue.Empty:
424 break
Hung-Te Lin163f7512012-02-17 18:58:57 +0800425
Jon Salz0697cbf2012-07-04 15:14:04 +0800426 if item[0] < now:
427 del self._generated_data[item[1]]
428 else:
429 # Not expired yet; put it back and we're done
430 self._generated_data_expiration.put(item)
431 break
432 uri_path = '/generated-data/%s' % uuid
433 return uri_path
Jon Salz258a40c2012-04-19 12:34:01 +0800434
Jon Salz0697cbf2012-07-04 15:14:04 +0800435 @_synchronized
436 def register_path(self, url_path, local_path):
437 self._resolver.AddPath(url_path, local_path)
Jon Salz258a40c2012-04-19 12:34:01 +0800438
Jon Salz0697cbf2012-07-04 15:14:04 +0800439 def get_system_status(self):
440 '''Returns system status information.
Jon Salz258a40c2012-04-19 12:34:01 +0800441
Jon Salz0697cbf2012-07-04 15:14:04 +0800442 This may include system load, battery status, etc. See
443 system.SystemStatus().
444 '''
445 return system.SystemStatus().__dict__
Jon Salz49a7d152012-06-19 15:04:09 +0800446
Jon Salzaeb4fd42012-06-05 15:08:30 +0800447
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800448def get_instance(address=DEFAULT_FACTORY_STATE_ADDRESS,
Jon Salz0697cbf2012-07-04 15:14:04 +0800449 port=DEFAULT_FACTORY_STATE_PORT):
450 '''
451 Gets an instance (for client side) to access the state server.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800452
Jon Salz0697cbf2012-07-04 15:14:04 +0800453 @param address: Address of the server to be connected.
454 @param port: Port of the server to be connected.
455 @return An object with all public functions from FactoryState.
456 See help(FactoryState) for more information.
457 '''
458 return jsonrpc.ServerProxy('http://%s:%d' % (address, port),
459 verbose=False)
Jon Salz258a40c2012-04-19 12:34:01 +0800460
461
462class MyJSONRPCRequestHandler(SimpleJSONRPCServer.SimpleJSONRPCRequestHandler):
Jon Salz0697cbf2012-07-04 15:14:04 +0800463 def do_GET(self):
464 logging.debug('HTTP request for path %s', self.path)
Jon Salz258a40c2012-04-19 12:34:01 +0800465
Jon Salz0697cbf2012-07-04 15:14:04 +0800466 handler = self.server.handlers.get(self.path)
467 if handler:
468 return handler(self)
Jon Salz258a40c2012-04-19 12:34:01 +0800469
Jon Salz0697cbf2012-07-04 15:14:04 +0800470 match = re.match('^/generated-data/([-0-9a-f]+)$', self.path)
471 if match:
472 generated_data = self.server._generated_data.get(match.group(1))
473 if not generated_data:
474 logging.warn('Unknown or expired generated data %s',
475 match.group(1))
476 self.send_response(404)
477 return
Jon Salz5eee01c2012-06-04 16:07:33 +0800478
Jon Salz0697cbf2012-07-04 15:14:04 +0800479 mime_type, data = generated_data
Jon Salz5eee01c2012-06-04 16:07:33 +0800480
Jon Salz0697cbf2012-07-04 15:14:04 +0800481 self.send_response(200)
482 self.send_header('Content-Type', mime_type)
483 self.send_header('Content-Length', len(data))
484 self.end_headers()
485 self.wfile.write(data)
Jon Salz5eee01c2012-06-04 16:07:33 +0800486
Jon Salz0697cbf2012-07-04 15:14:04 +0800487 if self.path.endswith('/'):
488 self.path += 'index.html'
Jon Salz258a40c2012-04-19 12:34:01 +0800489
Jon Salz0697cbf2012-07-04 15:14:04 +0800490 if ".." in self.path.split("/"):
491 logging.warn("Invalid path")
492 self.send_response(404)
493 return
Jon Salz258a40c2012-04-19 12:34:01 +0800494
Jon Salz0697cbf2012-07-04 15:14:04 +0800495 mime_type = mimetypes.guess_type(self.path)
496 if not mime_type:
497 logging.warn("Unable to guess MIME type")
498 self.send_response(404)
499 return
Jon Salz258a40c2012-04-19 12:34:01 +0800500
Jon Salz0697cbf2012-07-04 15:14:04 +0800501 local_path = None
502 match = re.match('^/generated-files/([-0-9a-f]+)/', self.path)
503 if match:
504 local_path = self.server._generated_files.get(match.group(1))
505 if not local_path:
506 logging.warn('Unknown generated file %s in path %s',
507 match.group(1), self.path)
508 self.send_response(404)
509 return
Jon Salz5eee01c2012-06-04 16:07:33 +0800510
Jon Salz0697cbf2012-07-04 15:14:04 +0800511 local_path = self.server._resolver.Resolve(self.path)
512 if not local_path or not os.path.exists(local_path):
513 logging.warn("File not found: %s", (local_path or self.path))
514 self.send_response(404)
515 return
Jon Salz258a40c2012-04-19 12:34:01 +0800516
Jon Salz0697cbf2012-07-04 15:14:04 +0800517 self.send_response(200)
518 self.send_header("Content-Type", mime_type[0])
519 self.send_header("Content-Length", os.path.getsize(local_path))
520 self.end_headers()
521 with open(local_path) as f:
522 shutil.copyfileobj(f, self.wfile)
Jon Salz258a40c2012-04-19 12:34:01 +0800523
524
525class ThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
Jon Salz0697cbf2012-07-04 15:14:04 +0800526 SimpleJSONRPCServer.SimpleJSONRPCServer):
527 '''The JSON/RPC server.
Jon Salz258a40c2012-04-19 12:34:01 +0800528
Jon Salz0697cbf2012-07-04 15:14:04 +0800529 Properties:
530 handlers: A map from URLs to callbacks handling them. (The callback
531 takes a single argument: the request to handle.)
532 '''
533 def __init__(self, *args, **kwargs):
534 SimpleJSONRPCServer.SimpleJSONRPCServer.__init__(self, *args, **kwargs)
535 self.handlers = {}
Jon Salz258a40c2012-04-19 12:34:01 +0800536
Jon Salz0697cbf2012-07-04 15:14:04 +0800537 def add_handler(self, url, callback):
538 self.handlers[url] = callback
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800539
540
541def create_server(state_file_path=None, bind_address=None, port=None):
Jon Salz0697cbf2012-07-04 15:14:04 +0800542 '''
543 Creates a FactoryState object and an JSON/RPC server to serve it.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800544
Jon Salz0697cbf2012-07-04 15:14:04 +0800545 @param state_file_path: The path containing the saved state.
546 @param bind_address: Address to bind to, defaulting to
547 DEFAULT_FACTORY_STATE_BIND_ADDRESS.
548 @param port: Port to bind to, defaulting to DEFAULT_FACTORY_STATE_PORT.
549 @return A tuple of the FactoryState instance and the SimpleJSONRPCServer
550 instance.
551 '''
552 # We have some icons in SVG format, but this isn't recognized in
553 # the standard Python mimetypes set.
554 mimetypes.add_type('image/svg+xml', '.svg')
Jon Salz258a40c2012-04-19 12:34:01 +0800555
Jon Salz0697cbf2012-07-04 15:14:04 +0800556 if not bind_address:
557 bind_address = DEFAULT_FACTORY_STATE_BIND_ADDRESS
558 if not port:
559 port = DEFAULT_FACTORY_STATE_PORT
560 instance = FactoryState(state_file_path)
561 instance._resolver.AddPath(
562 '/',
563 os.path.join(factory.FACTORY_PACKAGE_PATH, 'goofy/static'))
Jon Salzaeb4fd42012-06-05 15:08:30 +0800564
Jon Salz0697cbf2012-07-04 15:14:04 +0800565 server = ThreadedJSONRPCServer(
566 (bind_address, port),
567 requestHandler=MyJSONRPCRequestHandler,
568 logRequests=False)
Jon Salz258a40c2012-04-19 12:34:01 +0800569
Jon Salz0697cbf2012-07-04 15:14:04 +0800570 # Give the server the information it needs to resolve URLs.
571 server._generated_files = instance._generated_files
572 server._generated_data = instance._generated_data
573 server._resolver = instance._resolver
Jon Salz5eee01c2012-06-04 16:07:33 +0800574
Jon Salz0697cbf2012-07-04 15:14:04 +0800575 server.register_introspection_functions()
576 server.register_instance(instance)
577 server.web_socket_handler = None
578 return instance, server