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()