blob: ffc23b87048bc0500ad454ac3a6975e705268e79 [file] [log] [blame]
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import subprocess
import threading
import time
import uuid
from devil.utils import reraiser_thread
from pylib import constants
_MINIUMUM_TIMEOUT = 3.0
_PER_LINE_TIMEOUT = .002 # Should be able to process 500 lines per second.
_PROCESS_START_TIMEOUT = 10.0
_MAX_RESTARTS = 10 # Should be plenty unless tool is crashing on start-up.
class Deobfuscator(object):
def __init__(self, mapping_path):
script_path = os.path.join(constants.DIR_SOURCE_ROOT, 'build', 'android',
'stacktrace', 'java_deobfuscate.py')
cmd = [script_path, mapping_path]
# Allow only one thread to call TransformLines() at a time.
self._lock = threading.Lock()
# Ensure that only one thread attempts to kill self._proc in Close().
self._close_lock = threading.Lock()
self._closed_called = False
# Assign to None so that attribute exists if Popen() throws.
self._proc = None
# Start process eagerly to hide start-up latency.
self._proc_start_time = time.time()
self._proc = subprocess.Popen(
cmd, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
close_fds=True)
def IsClosed(self):
return self._closed_called or self._proc.returncode is not None
def IsBusy(self):
return self._lock.locked()
def IsReady(self):
return not self.IsClosed() and not self.IsBusy()
def TransformLines(self, lines):
"""Deobfuscates obfuscated names found in the given lines.
If anything goes wrong (process crashes, timeout, etc), returns |lines|.
Args:
lines: A list of strings without trailing newlines.
Returns:
A list of strings without trailing newlines.
"""
if not lines:
return []
# Deobfuscated stacks contain more frames than obfuscated ones when method
# inlining occurs. To account for the extra output lines, keep reading until
# this eof_line token is reached.
eof_line = uuid.uuid4().hex
out_lines = []
def deobfuscate_reader():
while True:
line = self._proc.stdout.readline()
# Return an empty string at EOF (when stdin is closed).
if not line:
break
line = line[:-1]
if line == eof_line:
break
out_lines.append(line)
if self.IsBusy():
logging.warning('deobfuscator: Having to wait for Java deobfuscation.')
# Allow only one thread to operate at a time.
with self._lock:
if self.IsClosed():
if not self._closed_called:
logging.warning('deobfuscator: Process exited with code=%d.',
self._proc.returncode)
self.Close()
return lines
# TODO(agrieve): Can probably speed this up by only sending lines through
# that might contain an obfuscated name.
reader_thread = reraiser_thread.ReraiserThread(deobfuscate_reader)
reader_thread.start()
try:
self._proc.stdin.write('\n'.join(lines))
self._proc.stdin.write('\n{}\n'.format(eof_line))
self._proc.stdin.flush()
time_since_proc_start = time.time() - self._proc_start_time
timeout = (max(0, _PROCESS_START_TIMEOUT - time_since_proc_start) +
max(_MINIUMUM_TIMEOUT, len(lines) * _PER_LINE_TIMEOUT))
reader_thread.join(timeout)
if self.IsClosed():
logging.warning(
'deobfuscator: Close() called by another thread during join().')
return lines
if reader_thread.is_alive():
logging.error('deobfuscator: Timed out.')
self.Close()
return lines
return out_lines
except IOError:
logging.exception('deobfuscator: Exception during java_deobfuscate')
self.Close()
return lines
def Close(self):
with self._close_lock:
needs_closing = not self.IsClosed()
self._closed_called = True
if needs_closing:
self._proc.stdin.close()
self._proc.kill()
self._proc.wait()
def __del__(self):
# self._proc is None when Popen() fails.
if not self._closed_called and self._proc:
logging.error('deobfuscator: Forgot to Close()')
self.Close()
class DeobfuscatorPool(object):
# As of Sep 2017, each instance requires about 500MB of RAM, as measured by:
# /usr/bin/time -v build/android/stacktrace/java_deobfuscate.py \
# out/Release/apks/ChromePublic.apk.mapping
def __init__(self, mapping_path, pool_size=4):
self._mapping_path = mapping_path
self._pool = [Deobfuscator(mapping_path) for _ in xrange(pool_size)]
# Allow only one thread to select from the pool at a time.
self._lock = threading.Lock()
self._num_restarts = 0
def TransformLines(self, lines):
with self._lock:
assert self._pool, 'TransformLines() called on a closed DeobfuscatorPool.'
# De-obfuscation is broken.
if self._num_restarts == _MAX_RESTARTS:
raise Exception('Deobfuscation seems broken.')
# Restart any closed Deobfuscators.
for i, d in enumerate(self._pool):
if d.IsClosed():
logging.warning('deobfuscator: Restarting closed instance.')
self._pool[i] = Deobfuscator(self._mapping_path)
self._num_restarts += 1
if self._num_restarts == _MAX_RESTARTS:
logging.warning('deobfuscator: MAX_RESTARTS reached.')
selected = next((x for x in self._pool if x.IsReady()), self._pool[0])
# Rotate the order so that next caller will not choose the same one.
self._pool.remove(selected)
self._pool.append(selected)
return selected.TransformLines(lines)
def Close(self):
with self._lock:
for d in self._pool:
d.Close()
self._pool = None