/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ddmuilib;
import com.android.ddmlib.*;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.HashMap;
/**
* Represents an addr2line process to get filename/method information from a
* memory address.<br>
* Each process can only handle one library, which should be provided when
* creating a new process.<br>
* <br>
* The processes take some time to load as they need to parse the library files.
* For this reason, processes cannot be manually started. Instead the class
* keeps an internal list of processes and one asks for a process for a specific
* library, using <code>getProcess(String library)<code>.<br></br>
* Internally, the processes are started in pipe mode to be able to query them
* with multiple addresses.
*/
public class Addr2Line {
/**
* Loaded processes list. This is also used as a locking object for any
* methods dealing with starting/stopping/creating processes/querying for
* method.
*/
private static final HashMap<String, Addr2Line> sProcessCache =
new HashMap<String, Addr2Line>();
/**
* byte array representing a carriage return. Used to push addresses in the
* process pipes.
*/
private static final byte[] sCrLf = {
'\n'
};
/** Path to the library */
private String mLibrary;
/** the command line process */
private Process mProcess;
/** buffer to read the result of the command line process from */
private BufferedReader mResultReader;
/**
* output stream to provide new addresses to decode to the command line
* process
*/
private BufferedOutputStream mAddressWriter;
/**
* Returns the instance of a Addr2Line process for the specified library.
* <br>The library should be in a format that makes<br>
* <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
*
* @param library the library in which to look for addresses.
* @return a new Addr2Line object representing a started process, ready to
* be queried for addresses. If any error happened when launching a
* new process, <code>null</code> will be returned.
*/
public static Addr2Line getProcess(final String library) {
// synchronize around the hashmap object
if (library != null) {
synchronized (sProcessCache) {
// look for an existing process
Addr2Line process = sProcessCache.get(library);
// if we don't find one, we create it
if (process == null) {
process = new Addr2Line(library);
// then we start it
boolean status = process.start();
if (status) {
// if starting the process worked, then we add it to the
// list.
sProcessCache.put(library, process);
} else {
// otherwise we just drop the object, to return null
process = null;
}
}
// return the process
return process;
}
}
return null;
}
/**
* Construct the object with a library name.
* <br>The library should be in a format that makes<br>
* <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
*
* @param library the library in which to look for address.
*/
private Addr2Line(final String library) {
mLibrary = library;
}
/**
* Starts the command line process.
*
* @return true if the process was started, false if it failed to start, or
* if there was any other errors.
*/
private boolean start() {
// because this is only called from getProcess() we know we don't need
// to synchronize this code.
// get the output directory.
String symbols = DdmUiPreferences.getSymbolDirectory();
// build the command line
String[] command = new String[5];
command[0] = DdmUiPreferences.getAddr2Line();
command[1] = "-C";
command[2] = "-f";
command[3] = "-e";
command[4] = symbols + mLibrary.replaceAll("libc\\.so", "libc_debug\\.so");
try {
// attempt to start the process
mProcess = Runtime.getRuntime().exec(command);
if (mProcess != null) {
// get the result reader
InputStreamReader is = new InputStreamReader(mProcess
.getInputStream());
mResultReader = new BufferedReader(is);
// get the outstream to write the addresses
mAddressWriter = new BufferedOutputStream(mProcess
.getOutputStream());
// check our streams are here
if (mResultReader == null || mAddressWriter == null) {
// not here? stop the process and return false;
mProcess.destroy();
mProcess = null;
return false;
}
// return a success
return true;
}
} catch (IOException e) {
// log the error
String msg = String.format(
"Error while trying to start %1$s process for library %2$s",
DdmUiPreferences.getAddr2Line(), mLibrary);
Log.e("ddm-Addr2Line", msg);
// drop the process just in case
if (mProcess != null) {
mProcess.destroy();
mProcess = null;
}
}
// we can be here either cause the allocation of mProcess failed, or we
// caught an exception
return false;
}
/**
* Stops the command line process.
*/
public void stop() {
synchronized (sProcessCache) {
if (mProcess != null) {
// remove the process from the list
sProcessCache.remove(mLibrary);
// then stops the process
mProcess.destroy();
// set the reference to null.
// this allows to make sure another thread calling getAddress()
// will not query a stopped thread
mProcess = null;
}
}
}
/**
* Stops all current running processes.
*/
public static void stopAll() {
// because of concurrent access (and our use of HashMap.values()), we
// can't rely on the synchronized inside stop(). We need to put one
// around the whole loop.
synchronized (sProcessCache) {
// just a basic loop on all the values in the hashmap and call to
// stop();
Collection<Addr2Line> col = sProcessCache.values();
for (Addr2Line a2l : col) {
a2l.stop();
}
}
}
/**
* Looks up an address and returns method name, source file name, and line
* number.
*
* @param addr the address to look up
* @return a BacktraceInfo object containing the method/filename/linenumber
* or null if the process we stopped before the query could be
* processed, or if an IO exception happened.
*/
public NativeStackCallInfo getAddress(long addr) {
// even though we don't access the hashmap object, we need to
// synchronized on it to prevent
// another thread from stopping the process we're going to query.
synchronized (sProcessCache) {
// check the process is still alive/allocated
if (mProcess != null) {
// prepare to the write the address to the output buffer.
// first, conversion to a string containing the hex value.
String tmp = Long.toString(addr, 16);
try {
// write the address to the buffer
mAddressWriter.write(tmp.getBytes());
// add CR-LF
mAddressWriter.write(sCrLf);
// flush it all.
mAddressWriter.flush();
// read the result. We need to read 2 lines
String method = mResultReader.readLine();
String source = mResultReader.readLine();
// make the backtrace object and return it
if (method != null && source != null) {
return new NativeStackCallInfo(mLibrary, method, source);
}
} catch (IOException e) {
// log the error
Log.e("ddms",
"Error while trying to get information for addr: "
+ tmp + " in library: " + mLibrary);
// we'll return null later
}
}
}
return null;
}
}