1#!/usr/bin/env python
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Retrieves the counts of how many objects have a particular field null on all running processes.
19
20Prints a json map from pid -> (log-tag, field-name, null-count, total-count).
21"""
22
23
24import adb
25import argparse
26import concurrent.futures
27import itertools
28import json
29import logging
30import os
31import os.path
32import signal
33import subprocess
34import time
35
36def main():
37  parser = argparse.ArgumentParser(description="Get counts of null fields from a device.")
38  parser.add_argument("-S", "--serial", metavar="SERIAL", type=str,
39                      required=False,
40                      default=os.environ.get("ANDROID_SERIAL", None),
41                      help="Android serial to use. Defaults to ANDROID_SERIAL")
42  parser.add_argument("-p", "--pid", required=False,
43                      default=[], action="append",
44                      help="Specific pids to check. By default checks all running dalvik processes")
45  has_out = "OUT" in os.environ
46  def_32 = os.path.join(os.environ.get("OUT", ""), "system", "lib", "libfieldnull.so")
47  def_64 = os.path.join(os.environ.get("OUT", ""), "system", "lib64", "libfieldnull.so")
48  has_32 = has_out and os.path.exists(def_32)
49  has_64 = has_out and os.path.exists(def_64)
50  def pushable_lib(name):
51    if os.path.isfile(name):
52      return name
53    else:
54      raise argparse.ArgumentTypeError(name + " is not a file!")
55  parser.add_argument('--lib32', type=pushable_lib,
56                      required=not has_32,
57                      action='store',
58                      default=def_32,
59                      help="Location of 32 bit agent to push")
60  parser.add_argument('--lib64', type=pushable_lib,
61                      required=not has_64,
62                      action='store',
63                      default=def_64 if has_64 else None,
64                      help="Location of 64 bit agent to push")
65  parser.add_argument("fields", nargs="+",
66                      help="fields to check")
67
68  out = parser.parse_args()
69
70  device = adb.device.get_device(out.serial)
71  print("getting root")
72  device.root()
73
74  print("Disabling selinux")
75  device.shell("setenforce 0".split())
76
77  print("Pushing libraries")
78  lib32 = device.shell("mktemp".split())[0].strip()
79  lib64 = device.shell("mktemp".split())[0].strip()
80
81  print(out.lib32 + " -> " + lib32)
82  device.push(out.lib32, lib32)
83
84  print(out.lib64 + " -> " + lib64)
85  device.push(out.lib64, lib64)
86
87  cmd32 = "'{}={}'".format(lib32, ','.join(out.fields))
88  cmd64 = "'{}={}'".format(lib64, ','.join(out.fields))
89
90  if len(out.pid) == 0:
91    print("Getting jdwp pids")
92    new_env = dict(os.environ)
93    new_env["ANDROID_SERIAL"] = device.serial
94    p = subprocess.Popen([device.adb_path, "jdwp"], env=new_env, stdout=subprocess.PIPE)
95    # ADB jdwp doesn't ever exit so just kill it after 1 second to get a list of pids.
96    with concurrent.futures.ProcessPoolExecutor() as ppe:
97      ppe.submit(kill_it, p.pid).result()
98    out.pid = p.communicate()[0].strip().split()
99    p.wait()
100    print(out.pid)
101  print("Clearing logcat")
102  device.shell("logcat -c".split())
103  final = {}
104  print("Getting info from every process dumped to logcat")
105  for p in out.pid:
106    res = check_single_process(p, device, cmd32, cmd64);
107    if res is not None:
108      final[p] = res
109  device.shell('rm {}'.format(lib32).split())
110  device.shell('rm {}'.format(lib64).split())
111  print(json.dumps(final, indent=2))
112
113def kill_it(p):
114  time.sleep(1)
115  os.kill(p, signal.SIGINT)
116
117def check_single_process(pid, device, bit32, bit64):
118  try:
119    # Just try attaching both 32 and 64 bit. Wrong one will fail silently.
120    device.shell(['am', 'attach-agent', str(pid), bit32])
121    device.shell(['am', 'attach-agent', str(pid), bit64])
122    time.sleep(0.5)
123    device.shell('kill -3 {}'.format(pid).split())
124    time.sleep(0.5)
125    out = []
126    all_fields = []
127    lc_cmd = "logcat -d -b main --pid={} -e '^\\t.*\\t[0-9]*\\t[0-9]*$'".format(pid).split(' ')
128    for l in device.shell(lc_cmd)[0].strip().split('\n'):
129      # first 4 are just date and other useless data.
130      data = l.strip().split()[5:]
131      if len(data) < 4:
132        continue
133      # If we run multiple times many copies of the agent will be attached. Just choose one of any
134      # copies for each field.
135      field = data[1]
136      if field not in all_fields:
137        out.append((str(data[0]), str(data[1]), int(data[2]), int(data[3])))
138        all_fields.append(field)
139    if len(out) != 0:
140      print("pid: " + pid + " -> " + str(out))
141      return out
142    else:
143      return None
144  except adb.device.ShellError as e:
145    print("failed on pid " + repr(pid) + " because " + repr(e))
146    return None
147
148if __name__ == '__main__':
149  main()
150