www_server.py revision effb81e5f8246d0db0270817048dc992db66e9fb
1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This module implements a simple WSGI server for the memory_inspector Web UI.
6
7The WSGI server essentially handles two kinds of requests:
8 - /ajax/foo/bar: The AJAX endpoints which exchange JSON data with the JS.
9    Requests routing is achieved using a simple @uri decorator which simply
10    performs regex matching on the request path.
11 - /static/content: Anything not matching the /ajax/ prefix is treated as a
12    static content request (for serving the index.html and JS/CSS resources).
13
14The following HTTP status code are returned by the server:
15 - 200 - OK: The request was handled correctly.
16 - 404 - Not found: None of the defined handlers did match the /request/path.
17 - 410 - Gone: The path was matched but the handler returned an empty response.
18    This typically happens when the target device is disconnected.
19"""
20
21import collections
22import datetime
23import dateutil.parser
24import os
25import memory_inspector
26import mimetypes
27import json
28import re
29import urlparse
30import uuid
31import wsgiref.simple_server
32
33from memory_inspector.core import backends
34from memory_inspector.core import memory_map
35from memory_inspector.classification import mmap_classifier
36from memory_inspector.data import serialization
37from memory_inspector.data import file_storage
38from memory_inspector.frontends import background_tasks
39
40
41_HTTP_OK = '200 - OK'
42_HTTP_GONE = '410 - Gone'
43_HTTP_NOT_FOUND = '404 - Not Found'
44_PERSISTENT_STORAGE_PATH = os.path.join(
45    os.path.expanduser('~'), '.config', 'memory_inspector')
46_CONTENT_DIR = os.path.abspath(os.path.join(
47    os.path.dirname(__file__), 'www_content'))
48_APP_PROCESS_RE = r'^[\w.:]+$'  # Regex for matching app processes.
49_STATS_HIST_SIZE = 120  # Keep at most 120 samples of stats per process.
50_CACHE_LEN = 10  # Max length of |_cached_objs|.
51
52# |_cached_objs| keeps the state of short-lived objects that the client needs to
53# _cached_objs subsequent AJAX calls.
54_cached_objs = collections.OrderedDict()
55_persistent_storage = file_storage.Storage(_PERSISTENT_STORAGE_PATH)
56_proc_stats_history = {}  # /Android/device/PID -> deque([stats@T=0, stats@T=1])
57
58
59class UriHandler(object):
60  """Base decorator used to automatically route /requests/by/path.
61
62  Each handler is called with the following args:
63    args: a tuple of the matching regex groups.
64    req_vars: a dictionary of request args (querystring for GET, body for POST).
65  Each handler must return a tuple with the following elements:
66    http_code: a string with the HTTP status code (e.g., '200 - OK')
67    headers: a list of HTTP headers (e.g., [('Content-Type': 'foo/bar')])
68    body: the HTTP response body.
69  """
70  _handlers = []
71
72  def __init__(self, path_regex, verb='GET', output_filter=None):
73    self._path_regex = path_regex
74    self._verb = verb
75    default_output_filter = lambda *x: x  # Just return the same args unchanged.
76    self._output_filter = output_filter or default_output_filter
77
78  def __call__(self, handler):
79    UriHandler._handlers += [(
80        self._verb, self._path_regex, self._output_filter, handler)]
81
82  @staticmethod
83  def Handle(method, path, req_vars):
84    """Finds a matching handler and calls it (or returns a 404 - Not Found)."""
85    for (match_method, path_regex, output_filter, fn) in UriHandler._handlers:
86      if method != match_method:
87        continue
88      m = re.match(path_regex, path)
89      if not m:
90        continue
91      (http_code, headers, body) = fn(m.groups(), req_vars)
92      return output_filter(http_code, headers, body)
93    return (_HTTP_NOT_FOUND, [], 'No AJAX handlers found')
94
95
96class AjaxHandler(UriHandler):
97  """Decorator for routing AJAX requests.
98
99  This decorator essentially groups the JSON serialization and the cache headers
100  which is shared by most of the handlers defined below.
101  """
102  def __init__(self, path_regex, verb='GET'):
103    super(AjaxHandler, self).__init__(
104        path_regex, verb, AjaxHandler.AjaxOutputFilter)
105
106  @staticmethod
107  def AjaxOutputFilter(http_code, headers, body):
108    serialized_content = json.dumps(body, cls=serialization.Encoder)
109    extra_headers = [('Cache-Control', 'no-cache'),
110                     ('Expires', 'Fri, 19 Sep 1986 05:00:00 GMT')]
111    return http_code, headers + extra_headers, serialized_content
112
113
114@AjaxHandler('/ajax/backends')
115def _ListBackends(args, req_vars):  # pylint: disable=W0613
116  return _HTTP_OK, [], [backend.name for backend in backends.ListBackends()]
117
118
119@AjaxHandler('/ajax/devices')
120def _ListDevices(args, req_vars):  # pylint: disable=W0613
121  resp = []
122  for device in backends.ListDevices():
123    # The device settings must loaded at discovery time (i.e. here), not during
124    # startup, because it might have been plugged later.
125    for k, v in _persistent_storage.LoadSettings(device.id).iteritems():
126      device.settings[k] = v
127
128    resp += [{'backend': device.backend.name,
129              'id': device.id,
130              'name': device.name}]
131  return _HTTP_OK, [], resp
132
133
134@AjaxHandler(r'/ajax/dump/mmap/(\w+)/(\w+)/(\d+)')
135def _DumpMmapsForProcess(args, req_vars):  # pylint: disable=W0613
136  """Dumps memory maps for a process.
137
138  The response is formatted according to the Google Charts DataTable format.
139  """
140  process = _GetProcess(args)
141  if not process:
142    return _HTTP_GONE, [], 'Device not found or process died'
143  mmap = process.DumpMemoryMaps()
144  table = _ConvertMmapToGTable(mmap)
145
146  # Store the dump in the cache. The client might need it later for profiling.
147  cache_id = _CacheObject(mmap)
148  return _HTTP_OK, [], {'table': table, 'id': cache_id}
149
150
151@AjaxHandler('/ajax/initialize/(\w+)/(\w+)$', 'POST')
152def _InitializeDevice(args, req_vars):  # pylint: disable=W0613
153  device = _GetDevice(args)
154  if not device:
155    return _HTTP_GONE, [], 'Device not found'
156  device.Initialize()
157  return _HTTP_OK, [], {
158    'isNativeTracingEnabled': device.IsNativeTracingEnabled()}
159
160
161@AjaxHandler(r'/ajax/profile/create', 'POST')
162def _CreateProfile(args, req_vars):  # pylint: disable=W0613
163  """Creates (and caches) a profile from a set of dumps.
164
165  The profiling data can be retrieved afterwards using the /profile/{PROFILE_ID}
166  endpoints (below).
167  """
168  classifier = None  # A classifier module (/classification/*_classifier.py).
169  dumps = {}  # dump-time -> obj. to classify (e.g., |memory_map.Map|).
170  for arg in 'type', 'source', 'ruleset':
171    assert(arg in req_vars), 'Expecting %s argument in POST data' % arg
172
173  # Step 1: collect the memory dumps, according to what the client specified in
174  # the 'type' and 'source' POST arguments.
175
176  # Case 1: Generate a profile from a set of mmap dumps.
177  if req_vars['type'] == 'mmap':
178    classifier = mmap_classifier
179    # Case 1a: Use a cached mmap dumps.
180    if req_vars['source'] == 'cache':
181      dumps[0] = _GetCacheObject(req_vars['id'])
182    # Case 1b: Load mem dumps from an archive.
183    elif req_vars['source'] == 'archive':
184      archive = _persistent_storage.OpenArchive(req_vars['archive'])
185      if not archive:
186        return _HTTP_GONE, [], 'Cannot open archive %s' % req_vars['archive']
187      first_timestamp = None
188      for timestamp_str in req_vars['snapshots']:
189        timestamp = dateutil.parser.parse(timestamp_str)
190        first_timestamp = timestamp if not first_timestamp else first_timestamp
191        time_delta = int((timestamp - first_timestamp).total_seconds())
192        dumps[time_delta] = archive.LoadMemMaps(timestamp)
193
194  # TODO(primiano): Add support for native_heap types.
195
196  # Step 2: Load the rule-set specified by the client in the 'ruleset' POST arg.
197  # Also, perform some basic sanity checking.
198  rules_path = os.path.join(memory_inspector.ROOT_DIR, 'classification_rules',
199                            req_vars['ruleset'])
200  if not classifier:
201    return _HTTP_GONE, [], 'Classifier %s not supported.' % req_vars['type']
202  if not dumps:
203    return _HTTP_GONE, [], 'No memory dumps could be retrieved'
204  if not os.path.isfile(rules_path):
205    return _HTTP_GONE, [], 'Cannot find the rule-set %s' % rules_path
206  with open(rules_path) as f:
207    rules = mmap_classifier.LoadRules(f.read())
208
209  # Step 3: Aggregate the data using the desired classifier and generate the
210  # profile dictionary (which will be kept cached here in the server).
211  # The resulting profile will consist of 1+ snapshots (depending on the number
212  # dumps the client has requested to process) and a number of 1+ metrics
213  # (depending on the buckets' keys returned by the classifier).
214
215  # Converts the {time: dump_obj} dict into a {time: |AggregatedResult|} dict.
216  # using the classifier.
217  snapshots = collections.OrderedDict((time, classifier.Classify(dump, rules))
218     for time, dump in sorted(dumps.iteritems()))
219
220  # Add the profile to the cache (and eventually discard old items).
221  # |profile_id| is the key that the client will use in subsequent requests
222  # (to the /ajax/profile/{ID}/ endpoints) to refer to this particular profile.
223  profile_id = _CacheObject(snapshots)
224
225  first_snapshot = next(snapshots.itervalues())
226
227  # |metrics| is the key set of any of the aggregated result
228  return _HTTP_OK, [], {'id': profile_id,
229                        'times': snapshots.keys(),
230                        'metrics': first_snapshot.keys,
231                        'rootBucket': first_snapshot.total.name + '/'}
232
233
234@AjaxHandler(r'/ajax/profile/(\w+)/tree/(\d+)/(\d+)')
235def _GetProfileTreeDataForSnapshot(args, req_vars):  # pylint: disable=W0613
236  """Gets the data for the tree chart for a given time and metric.
237
238  The response is formatted according to the Google Charts DataTable format.
239  """
240  snapshot_id = args[0]
241  metric_index = int(args[1])
242  time = int(args[2])
243  snapshots = _GetCacheObject(snapshot_id)
244  if not snapshots:
245    return _HTTP_GONE, [], 'Cannot find the selected profile.'
246  if time not in snapshots:
247    return _HTTP_GONE, [], 'Cannot find snapshot at T=%d.' % time
248  snapshot = snapshots[time]
249  if metric_index >= len(snapshot.keys):
250    return _HTTP_GONE, [], 'Invalid metric id %d' % metric_index
251
252  resp = {'cols': [{'label': 'bucket', 'type': 'string'},
253                   {'label': 'parent', 'type': 'string'}],
254          'rows': []}
255
256  def VisitBucketAndAddRows(bucket, parent_id=''):
257    """Recursively creates the (node, parent) visiting |ResultTree| in DFS."""
258    node_id = parent_id + bucket.name + '/'
259    node_label = '<dl><dt>%s</dt><dd>%s</dd></dl>' % (
260        bucket.name, _StrMem(bucket.values[metric_index]))
261    resp['rows'] += [{'c': [
262        {'v': node_id, 'f': node_label},
263        {'v': parent_id, 'f': None},
264    ]}]
265    for child in bucket.children:
266      VisitBucketAndAddRows(child, node_id)
267
268  VisitBucketAndAddRows(snapshot.total)
269  return _HTTP_OK, [], resp
270
271
272@AjaxHandler(r'/ajax/profile/(\w+)/time_serie/(\d+)/(.*)$')
273def _GetTimeSerieForSnapshot(args, req_vars):  # pylint: disable=W0613
274  """Gets the data for the area chart for a given metric and bucket.
275
276  The response is formatted according to the Google Charts DataTable format.
277  """
278  snapshot_id = args[0]
279  metric_index = int(args[1])
280  bucket_path = args[2]
281  snapshots = _GetCacheObject(snapshot_id)
282  if not snapshots:
283    return _HTTP_GONE, [], 'Cannot find the selected profile.'
284  if metric_index >= len(next(snapshots.itervalues()).keys):
285    return _HTTP_GONE, [], 'Invalid metric id %d' % metric_index
286
287  def FindBucketByPath(bucket, path, parent_path=''):  # Essentially a DFS.
288    cur_path = parent_path + bucket.name + '/'
289    if cur_path == path:
290      return bucket
291    for child in bucket.children:
292      res = FindBucketByPath(child, path, cur_path)
293      if res:
294        return res
295    return None
296
297  # The resulting data table will look like this (assuming len(metrics) == 2):
298  # Time  Ashmem      Dalvik     Other
299  # 0    (1024,0)  (4096,1024)  (0,0)
300  # 30   (512,512) (1024,1024)  (0,512)
301  # 60   (0,512)   (1024,0)     (512,0)
302  resp = {'cols': [], 'rows': []}
303  for time, aggregated_result in snapshots.iteritems():
304    bucket = FindBucketByPath(aggregated_result.total, bucket_path)
305    if not bucket:
306      return _HTTP_GONE, [], 'Bucket %s not found' % bucket_path
307
308    # If the user selected a non-leaf bucket, display the breakdown of its
309    # direct children. Otherwise just the leaf bucket.
310    children_buckets = bucket.children if bucket.children else [bucket]
311
312    # Create the columns (form the buckets) when processing the first snapshot.
313    if not resp['cols']:
314      resp['cols'] += [{'label': 'Time', 'type': 'string'}]
315      for child_bucket in children_buckets:
316        resp['cols'] += [{'label': child_bucket.name, 'type': 'number'}]
317
318    row = [{'v': str(time), 'f': None}]
319    for child_bucket in children_buckets:
320      row += [{'v': child_bucket.values[metric_index] / 1024, 'f': None}]
321    resp['rows'] += [{'c': row}]
322
323  return _HTTP_OK, [], resp
324
325
326@AjaxHandler(r'/ajax/ps/(\w+)/(\w+)$')  # /ajax/ps/Android/a0b1c2[?all=1]
327def _ListProcesses(args, req_vars):  # pylint: disable=W0613
328  """Lists processes and their CPU / mem stats.
329
330  The response is formatted according to the Google Charts DataTable format.
331  """
332  device = _GetDevice(args)
333  if not device:
334    return _HTTP_GONE, [], 'Device not found'
335  resp = {
336      'cols': [
337          {'label': 'Pid', 'type':'number'},
338          {'label': 'Name', 'type':'string'},
339          {'label': 'Cpu %', 'type':'number'},
340          {'label': 'Mem RSS Kb', 'type':'number'},
341          {'label': '# Threads', 'type':'number'},
342        ],
343      'rows': []}
344  for process in device.ListProcesses():
345    # Exclude system apps if the request didn't contain the ?all=1 arg.
346    if not req_vars.get('all') and not re.match(_APP_PROCESS_RE, process.name):
347      continue
348    stats = process.GetStats()
349    resp['rows'] += [{'c': [
350        {'v': process.pid, 'f': None},
351        {'v': process.name, 'f': None},
352        {'v': stats.cpu_usage, 'f': None},
353        {'v': stats.vm_rss, 'f': None},
354        {'v': stats.threads, 'f': None},
355    ]}]
356  return _HTTP_OK, [], resp
357
358
359@AjaxHandler(r'/ajax/stats/(\w+)/(\w+)$')  # /ajax/stats/Android/a0b1c2
360def _GetDeviceStats(args, req_vars):  # pylint: disable=W0613
361  """Lists device CPU / mem stats.
362
363  The response is formatted according to the Google Charts DataTable format.
364  """
365  device = _GetDevice(args)
366  if not device:
367    return _HTTP_GONE, [], 'Device not found'
368  device_stats = device.GetStats()
369
370  cpu_stats = {
371      'cols': [
372          {'label': 'CPU', 'type':'string'},
373          {'label': 'Usr %', 'type':'number'},
374          {'label': 'Sys %', 'type':'number'},
375          {'label': 'Idle %', 'type':'number'},
376        ],
377      'rows': []}
378
379  for cpu_idx in xrange(len(device_stats.cpu_times)):
380    cpu = device_stats.cpu_times[cpu_idx]
381    cpu_stats['rows'] += [{'c': [
382        {'v': '# %d' % cpu_idx, 'f': None},
383        {'v': cpu['usr'], 'f': None},
384        {'v': cpu['sys'], 'f': None},
385        {'v': cpu['idle'], 'f': None},
386    ]}]
387
388  mem_stats = {
389      'cols': [
390          {'label': 'Section', 'type':'string'},
391          {'label': 'MB', 'type':'number',  'pattern': ''},
392        ],
393      'rows': []}
394
395  for key, value in device_stats.memory_stats.iteritems():
396    mem_stats['rows'] += [{'c': [
397        {'v': key, 'f': None},
398        {'v': value / 1024, 'f': None}
399    ]}]
400
401  return _HTTP_OK, [], {'cpu': cpu_stats, 'mem': mem_stats}
402
403
404@AjaxHandler(r'/ajax/stats/(\w+)/(\w+)/(\d+)$')  # /ajax/stats/Android/a0b1c2/42
405def _GetProcessStats(args, req_vars):  # pylint: disable=W0613
406  """Lists CPU / mem stats for a given process (and keeps history).
407
408  The response is formatted according to the Google Charts DataTable format.
409  """
410  process = _GetProcess(args)
411  if not process:
412    return _HTTP_GONE, [], 'Device not found'
413
414  proc_uri = '/'.join(args)
415  cur_stats = process.GetStats()
416  if proc_uri not in _proc_stats_history:
417    _proc_stats_history[proc_uri] = collections.deque(maxlen=_STATS_HIST_SIZE)
418  history = _proc_stats_history[proc_uri]
419  history.append(cur_stats)
420
421  cpu_stats = {
422      'cols': [
423          {'label': 'T', 'type':'string'},
424          {'label': 'CPU %', 'type':'number'},
425          {'label': '# Threads', 'type':'number'},
426        ],
427      'rows': []
428  }
429
430  mem_stats = {
431      'cols': [
432          {'label': 'T', 'type':'string'},
433          {'label': 'Mem RSS Kb', 'type':'number'},
434          {'label': 'Page faults', 'type':'number'},
435        ],
436      'rows': []
437  }
438
439  for stats in history:
440    cpu_stats['rows'] += [{'c': [
441          {'v': str(datetime.timedelta(seconds=stats.run_time)), 'f': None},
442          {'v': stats.cpu_usage, 'f': None},
443          {'v': stats.threads, 'f': None},
444    ]}]
445    mem_stats['rows'] += [{'c': [
446          {'v': str(datetime.timedelta(seconds=stats.run_time)), 'f': None},
447          {'v': stats.vm_rss, 'f': None},
448          {'v': stats.page_faults, 'f': None},
449    ]}]
450
451  return _HTTP_OK, [], {'cpu': cpu_stats, 'mem': mem_stats}
452
453
454@AjaxHandler(r'/ajax/settings/(\w+)/?(\w+)?$')  # /ajax/settings/Android[/id]
455def _GetDeviceOrBackendSettings(args, req_vars):  # pylint: disable=W0613
456  backend = backends.GetBackend(args[0])
457  if not backend:
458    return _HTTP_GONE, [], 'Backend not found'
459  if args[1]:
460    device = _GetDevice(args)
461    if not device:
462      return _HTTP_GONE, [], 'Device not found'
463    settings = device.settings
464  else:
465    settings = backend.settings
466
467  assert(isinstance(settings, backends.Settings))
468  resp = {}
469  for key  in settings.expected_keys:
470    resp[key] = {'description': settings.expected_keys[key],
471                 'value': settings.values[key]}
472  return _HTTP_OK, [], resp
473
474
475@AjaxHandler(r'/ajax/settings/(\w+)/?(\w+)?$', 'POST')
476def _SetDeviceOrBackendSettings(args, req_vars):  # pylint: disable=W0613
477  backend = backends.GetBackend(args[0])
478  if not backend:
479    return _HTTP_GONE, [], 'Backend not found'
480  if args[1]:
481    device = _GetDevice(args)
482    if not device:
483      return _HTTP_GONE, [], 'Device not found'
484    settings = device.settings
485    storage_name = device.id
486  else:
487    settings = backend.settings
488    storage_name = backend.name
489
490  for key in req_vars.iterkeys():
491    settings[key] = req_vars[key]
492  _persistent_storage.StoreSettings(storage_name, settings.values)
493  return _HTTP_OK, [], ''
494
495
496@AjaxHandler(r'/ajax/storage/list')
497def _ListStorage(args, req_vars):  # pylint: disable=W0613
498  resp = {
499      'cols': [
500          {'label': 'Archive', 'type':'string'},
501          {'label': 'Snapshot', 'type':'string'},
502          {'label': 'Mem maps', 'type':'boolean'},
503          {'label': 'N. Heap', 'type':'boolean'},
504        ],
505      'rows': []}
506  for archive_name in _persistent_storage.ListArchives():
507    archive = _persistent_storage.OpenArchive(archive_name)
508    first_timestamp = None
509    for timestamp in archive.ListSnapshots():
510      first_timestamp = timestamp if not first_timestamp else first_timestamp
511      time_delta = '%d s.' % (timestamp - first_timestamp).total_seconds()
512      resp['rows'] += [{'c': [
513          {'v': archive_name, 'f': None},
514          {'v': timestamp.isoformat(), 'f': time_delta},
515          {'v': archive.HasMemMaps(timestamp), 'f': None},
516          {'v': archive.HasNativeHeap(timestamp), 'f': None},
517      ]}]
518  return _HTTP_OK, [], resp
519
520
521@AjaxHandler(r'/ajax/storage/(.+)/(.+)/mmaps')
522def _LoadMmapsFromStorage(args, req_vars):  # pylint: disable=W0613
523  archive = _persistent_storage.OpenArchive(args[0])
524  if not archive:
525    return _HTTP_GONE, [], 'Cannot open archive %s' % req_vars['archive']
526
527  timestamp = dateutil.parser.parse(args[1])
528  if not archive.HasMemMaps(timestamp):
529    return _HTTP_GONE, [], 'No mmaps for snapshot %s' % timestamp
530  mmap = archive.LoadMemMaps(timestamp)
531  return _HTTP_OK, [], {'table': _ConvertMmapToGTable(mmap)}
532
533
534# /ajax/tracer/start/Android/device-id/pid
535@AjaxHandler(r'/ajax/tracer/start/(\w+)/(\w+)/(\d+)', 'POST')
536def _StartTracer(args, req_vars):
537  for arg in 'interval', 'count', 'traceNativeHeap':
538    assert(arg in req_vars), 'Expecting %s argument in POST data' % arg
539  process = _GetProcess(args)
540  if not process:
541    return _HTTP_GONE, [], 'Device not found or process died'
542  task_id = background_tasks.StartTracer(
543      storage_path=_PERSISTENT_STORAGE_PATH,
544      process=process,
545      interval=int(req_vars['interval']),
546      count=int(req_vars['count']),
547      trace_native_heap=req_vars['traceNativeHeap'])
548  return _HTTP_OK, [], task_id
549
550
551@AjaxHandler(r'/ajax/tracer/status/(\d+)')  # /ajax/tracer/status/{task_id}
552def _GetTracerStatus(args, req_vars):  # pylint: disable=W0613
553  task = background_tasks.Get(int(args[0]))
554  if not task:
555    return _HTTP_GONE, [], 'Task not found'
556  return _HTTP_OK, [], task.GetProgress()
557
558
559@UriHandler(r'^(?!/ajax)/(.*)$')
560def _StaticContent(args, req_vars):  # pylint: disable=W0613
561  # Give the browser a 1-day TTL cache to minimize the start-up time.
562  cache_headers = [('Cache-Control', 'max-age=86400, public')]
563  req_path = args[0] if args[0] else 'index.html'
564  file_path = os.path.abspath(os.path.join(_CONTENT_DIR, req_path))
565  if (os.path.isfile(file_path) and
566      os.path.commonprefix([file_path, _CONTENT_DIR]) == _CONTENT_DIR):
567    mtype = 'text/plain'
568    guessed_mime = mimetypes.guess_type(file_path)
569    if guessed_mime and guessed_mime[0]:
570      mtype = guessed_mime[0]
571    with open(file_path, 'rb') as f:
572      body = f.read()
573    return _HTTP_OK, cache_headers + [('Content-Type', mtype)], body
574  return _HTTP_NOT_FOUND, cache_headers,  file_path + ' not found'
575
576
577def _GetDevice(args):
578  """Returns a |backends.Device| instance from a /backend/device URI."""
579  assert(len(args) >= 2), 'Malformed request. Expecting /backend/device'
580  return backends.GetDevice(backend_name=args[0], device_id=args[1])
581
582
583def _GetProcess(args):
584  """Returns a |backends.Process| instance from a /backend/device/pid URI."""
585  assert(len(args) >= 3 and args[2].isdigit()), (
586      'Malformed request. Expecting /backend/device/pid')
587  device = _GetDevice(args)
588  if not device:
589    return None
590  return device.GetProcess(int(args[2]))
591
592def _ConvertMmapToGTable(mmap):
593  """Returns a Google Charts DataTable dictionary for the given mmap."""
594  assert(isinstance(mmap, memory_map.Map))
595  table = {
596      'cols': [
597          {'label': 'Start', 'type':'string'},
598          {'label': 'End', 'type':'string'},
599          {'label': 'Length Kb', 'type':'number'},
600          {'label': 'Prot', 'type':'string'},
601          {'label': 'Priv. Dirty Kb', 'type':'number'},
602          {'label': 'Priv. Clean Kb', 'type':'number'},
603          {'label': 'Shared Dirty Kb', 'type':'number'},
604          {'label': 'Shared Clean Kb', 'type':'number'},
605          {'label': 'File', 'type':'string'},
606          {'label': 'Offset', 'type':'number'},
607          {'label': 'Resident Pages', 'type':'string'},
608        ],
609      'rows': []}
610  for entry in mmap.entries:
611    table['rows'] += [{'c': [
612        {'v': '%08x' % entry.start, 'f': None},
613        {'v': '%08x' % entry.end, 'f': None},
614        {'v': entry.len / 1024, 'f': None},
615        {'v': entry.prot_flags, 'f': None},
616        {'v': entry.priv_dirty_bytes / 1024, 'f': None},
617        {'v': entry.priv_clean_bytes / 1024, 'f': None},
618        {'v': entry.shared_dirty_bytes / 1024, 'f': None},
619        {'v': entry.shared_clean_bytes / 1024, 'f': None},
620        {'v': entry.mapped_file, 'f': None},
621        {'v': entry.mapped_offset, 'f': None},
622        {'v': '[%s]' % (','.join(map(str, entry.resident_pages))), 'f': None},
623    ]}]
624  return table
625
626def _CacheObject(obj_to_store):
627  """Stores an object in the server-side cache and returns its unique id."""
628  if len(_cached_objs) >= _CACHE_LEN:
629    _cached_objs.popitem(last=False)
630  obj_id = uuid.uuid4().hex
631  _cached_objs[obj_id] = obj_to_store
632  return str(obj_id)
633
634
635def _GetCacheObject(obj_id):
636  """Retrieves an object in the server-side cache by its id."""
637  return _cached_objs.get(obj_id)
638
639
640def _StrMem(nbytes):
641  """Converts a number (of bytes) into a human readable string (kb, mb)."""
642  if nbytes < 2**10:
643    return '%d B' % nbytes
644  if nbytes < 2**20:
645    return '%.1f KB' % round(nbytes / 1024.0)
646  return '%.1f MB' % (nbytes / 1048576.0)
647
648
649def _HttpRequestHandler(environ, start_response):
650  """Parses a single HTTP request and delegates the handling through UriHandler.
651
652  This essentially wires up wsgiref.simple_server with our @UriHandler(s).
653  """
654  path = environ['PATH_INFO']
655  method = environ['REQUEST_METHOD']
656  if method == 'POST':
657    req_body_size = int(environ.get('CONTENT_LENGTH', 0))
658    req_body = environ['wsgi.input'].read(req_body_size)
659    req_vars = json.loads(req_body)
660  else:
661    req_vars = urlparse.parse_qs(environ['QUERY_STRING'])
662  (http_code, headers, body) = UriHandler.Handle(method, path, req_vars)
663  start_response(http_code, headers)
664  return [body]
665
666
667def Start(http_port):
668  # Load the saved backends' settings (some of them might be needed to bootstrap
669  # as, for instance, the adb path for the Android backend).
670  for backend in backends.ListBackends():
671    for k, v in _persistent_storage.LoadSettings(backend.name).iteritems():
672      backend.settings[k] = v
673
674  httpd = wsgiref.simple_server.make_server('', http_port, _HttpRequestHandler)
675  try:
676    httpd.serve_forever()
677  except KeyboardInterrupt:
678    pass  # Don't print useless stack traces when the user hits CTRL-C.
679  background_tasks.TerminateAll()