/*
* Copyright 2012 Google Inc.
*
* 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.mobilyzer.measurements;
import android.os.Parcel;
import android.os.Parcelable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.mobilyzer.Config;
import com.mobilyzer.MeasurementDesc;
import com.mobilyzer.MeasurementResult;
import com.mobilyzer.MeasurementTask;
import com.mobilyzer.PreemptibleMeasurementTask;
import com.mobilyzer.MeasurementResult.TaskProgress;
import com.mobilyzer.exceptions.MeasurementError;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.MeasurementJsonConvertor;
import com.mobilyzer.util.PhoneUtils;
import com.mobilyzer.util.Util;
/**
* A Callable task that handles Traceroute measurements
*/
public class TracerouteTask extends MeasurementTask implements PreemptibleMeasurementTask {
// Type name for internal use
public static final String TYPE = "traceroute";
// Human readable name for the task
public static final String DESCRIPTOR = "traceroute";
/*
* Default payload size of the ICMP packet, plus the 8-byte ICMP header resulting in a total of
* 64-byte ICMP packet
*/
public static final int DEFAULT_PING_PACKET_SIZE = 56;
public static final int DEFAULT_PING_TIMEOUT = 10;
public static final int DEFAULT_MAX_HOP_CNT = 30;
public static final int DEFAULT_PARALLEL_PROBE_NUM = 1;
// Used to compute progress for user
public static final int EXPECTED_HOP_CNT = 20;
public static final int DEFAULT_PINGS_PER_HOP = 3;
private long duration;
public ArrayList<Double> resultsArray;
private TaskProgress taskProgress;
private volatile boolean stopFlag;
private volatile boolean pauseFlag;
public ArrayList<HopInfo> hopHosts;// TODO: change it to private
private long totalRunningTime;
private int ttl;
private int maxHopCount;
// Track data consumption for this task to avoid exceeding user's limit
private long dataConsumed;
/**
* The description of the Traceroute measurement
*/
public static class TracerouteDesc extends MeasurementDesc {
// the host name or IP address to use as the target of the traceroute.
public String target;
// the packet per ICMP ping in the unit of bytes
private int packetSizeByte;
// the number of seconds we wait for a ping response.
private int pingTimeoutSec;
// the interval between successive pings in seconds
private double pingIntervalSec;
// the number of pings we use for each ttl value
private int pingsPerHop;
// the total number of pings will send before we declarethe traceroute fails
private int maxHopCount;
// the location of the ping binary. Only used internally
private String pingExe;
// TODO, this should be moved into MeasurementDesc if we want to have that for all measurement
// types
public String preCondition;
private int parallelProbeNum;
public TracerouteDesc(String key, Date startTime, Date endTime, double intervalSec, long count,
long priority, int contextIntervalSec, Map<String, String> params)
throws InvalidParameterException {
super(TracerouteTask.TYPE, key, startTime, endTime, intervalSec, count, priority,
contextIntervalSec, params);
initializeParams(params);
if (target == null || target.length() == 0) {
throw new InvalidParameterException("Target of traceroute cannot be null");
}
}
@Override
public String getType() {
return TracerouteTask.TYPE;
}
@Override
protected void initializeParams(Map<String, String> params) {
if (params == null) {
return;
}
// HTTP specific parameters according to the design document
this.target = params.get("target");
try {
String val;
if ((val = params.get("packet_size_byte")) != null && val.length() > 0
&& Integer.parseInt(val) > 0) {
this.packetSizeByte = Integer.parseInt(val);
} else {
this.packetSizeByte = TracerouteTask.DEFAULT_PING_PACKET_SIZE;
}
if ((val = params.get("ping_timeout_sec")) != null && val.length() > 0
&& Integer.parseInt(val) > 0) {
this.pingTimeoutSec = Integer.parseInt(val);
} else {
this.pingTimeoutSec = TracerouteTask.DEFAULT_PING_TIMEOUT;
}
if ((val = params.get("ping_interval_sec")) != null && val.length() > 0
&& Integer.parseInt(val) > 0) {
this.pingIntervalSec = Integer.parseInt(val);
} else {
this.pingIntervalSec = Config.DEFAULT_INTERVAL_BETWEEN_ICMP_PACKET_SEC;
}
if ((val = params.get("pings_per_hop")) != null && val.length() > 0
&& Integer.parseInt(val) > 0) {
this.pingsPerHop = Integer.parseInt(val);
} else {
this.pingsPerHop = TracerouteTask.DEFAULT_PINGS_PER_HOP;
}
if ((val = params.get("max_hop_count")) != null && val.length() > 0
&& Integer.parseInt(val) > 0) {
this.maxHopCount = Integer.parseInt(val);
} else {
this.maxHopCount = TracerouteTask.DEFAULT_MAX_HOP_CNT;
}
if ((val = params.get("precond")) != null && val.length() > 0) {
this.preCondition = params.get("precond");
} else {
this.preCondition = null;
}
if ((val = params.get("parallel_probe_num")) != null && val.length() > 0
&& Integer.parseInt(val) > 0) {
this.parallelProbeNum = Integer.parseInt(val);
} else {
this.parallelProbeNum = TracerouteTask.DEFAULT_PARALLEL_PROBE_NUM;
}
} catch (NumberFormatException e) {
throw new InvalidParameterException("PingTask cannot be created due " + "to invalid params");
}
}
protected TracerouteDesc(Parcel in) {
super(in);
target = in.readString();
packetSizeByte = in.readInt();
pingTimeoutSec = in.readInt();
pingIntervalSec = in.readDouble();
pingsPerHop = in.readInt();
maxHopCount = in.readInt();
pingExe = in.readString();
preCondition = in.readString();
parallelProbeNum = in.readInt();
}
public static final Parcelable.Creator<TracerouteDesc> CREATOR =
new Parcelable.Creator<TracerouteDesc>() {
public TracerouteDesc createFromParcel(Parcel in) {
return new TracerouteDesc(in);
}
public TracerouteDesc[] newArray(int size) {
return new TracerouteDesc[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(target);
dest.writeInt(packetSizeByte);
dest.writeInt(pingTimeoutSec);
dest.writeDouble(pingIntervalSec);
dest.writeInt(pingsPerHop);
dest.writeInt(maxHopCount);
dest.writeString(pingExe);
dest.writeString(preCondition);
dest.writeInt(parallelProbeNum);
}
}
public TracerouteTask(MeasurementDesc desc) {
super(new TracerouteDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count,
desc.priority, desc.contextIntervalSec, desc.parameters));
this.duration = Config.TRACEROUTE_TASK_DURATION;
this.taskProgress = TaskProgress.FAILED;
this.stopFlag = false;
this.pauseFlag = false;
this.hopHosts = new ArrayList<HopInfo>();
this.ttl = 1;
this.maxHopCount = ((TracerouteDesc) this.measurementDesc).maxHopCount;
this.totalRunningTime = 0;
this.dataConsumed = 0;
}
protected TracerouteTask(Parcel in) {
super(in);
duration = in.readLong();
taskProgress = (TaskProgress) in.readSerializable();
stopFlag = (in.readByte() != 0);
pauseFlag = (in.readByte() != 0);
hopHosts = new ArrayList<HopInfo>();
ttl = in.readInt();
maxHopCount = ((TracerouteDesc) this.measurementDesc).maxHopCount;
totalRunningTime = in.readLong();
dataConsumed = in.readLong();
}
public static final Parcelable.Creator<TracerouteTask> CREATOR =
new Parcelable.Creator<TracerouteTask>() {
public TracerouteTask createFromParcel(Parcel in) {
return new TracerouteTask(in);
}
public TracerouteTask[] newArray(int size) {
return new TracerouteTask[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(duration);
dest.writeSerializable(taskProgress);
dest.writeByte((byte) (stopFlag ? 1 : 0));
dest.writeByte((byte) (pauseFlag ? 1 : 0));
dest.writeInt(ttl);
dest.writeLong(totalRunningTime);
dest.writeLong(dataConsumed);
}
/**
* Returns a copy of the TracerouteTask
*/
@Override
public MeasurementTask clone() {
MeasurementDesc desc = this.measurementDesc;
TracerouteDesc newDesc =
new TracerouteDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count,
desc.priority, desc.contextIntervalSec, desc.parameters);
return new TracerouteTask(newDesc);
}
@Override
public MeasurementResult[] call() throws MeasurementError {
TracerouteDesc task = (TracerouteDesc) this.measurementDesc;
// int maxHopCount = task.maxHopCount;
// int ttl = 1;
String hostIp = null;
String target = task.target;
taskProgress = TaskProgress.FAILED;
stopFlag = false;
pauseFlag = false;
Logger.d("Starting traceroute on host " + task.target);
try {
InetAddress hostInetAddr = InetAddress.getByName(target);
hostIp = hostInetAddr.getHostAddress();
// add support for ipv6
int ipByteLen = hostInetAddr.getAddress().length;
Logger.i("IP address length is " + ipByteLen);
Logger.i("IP is " + hostIp);
task.pingExe = Util.pingExecutableBasedOnIPType(ipByteLen);
Logger.i("Ping executable is " + task.pingExe);
if (task.pingExe == null) {
Logger.e("Ping Executable not found");
throw new MeasurementError("Ping Executable not found");
}
} catch (UnknownHostException e) {
Logger.e("Cannont resolve host " + target);
throw new MeasurementError("target " + target + " cannot be resolved");
}
MeasurementResult result = null;
ExecutorService hopExecutorService = Executors.newFixedThreadPool(task.parallelProbeNum);
CompletionService<HopInfo> taskCompletionService =
new ExecutorCompletionService<HopInfo>(hopExecutorService);
/*
* Current traceroute implementation sends out three ICMP probes per TTL. One ping every 0.2s is
* the lower bound before some platforms requires root to run ping. We ping once every time to
* get a rough rtt as we cannot get the exact rtt from the output of the ping command with ttl
* being set
*/
boolean[] hopsStatus = new boolean[maxHopCount];
for (int i = maxHopCount; i > 0; i--) {
hopsStatus[i - 1] = false;
String command =
Util.constructCommand(task.pingExe, "-n", "-t", ttl, "-s", task.packetSizeByte, "-c 1",
target);
taskCompletionService.submit(new HopExecutor(task.pingsPerHop, command, hostIp, ttl));
ttl++;
}
for (int tasksHandled = 0; tasksHandled < maxHopCount; tasksHandled++) {
try {
Future<HopInfo> hopResult = taskCompletionService.take();
HopInfo hop = hopResult.get();
hopHosts.add(hop);
HashSet<String> hostsAtThisDistance = hop.hosts;
hopsStatus[hop.ttl - 1] = true;
for (String ip : hostsAtThisDistance) {
// If we have reached the final destination hostIp,
// print it out and clean up
boolean allHopsAreDone = true;
if (ip.compareTo(hostIp) == 0) {
for (int i = 0; i < hop.ttl; i++) {
if (!hopsStatus[hop.ttl - 1]) {
allHopsAreDone=false;
}
}
if(allHopsAreDone){
hopExecutorService.shutdownNow();
Logger.i(hop.ttl + ": " + hostIp);
Logger.i(" Finished! " + target + " reached in " + hop.ttl + " hops");
taskProgress = TaskProgress.COMPLETED;
PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
result =
new MeasurementResult(phoneUtils.getDeviceInfo().deviceId,
phoneUtils.getDeviceProperty(this.getKey()), TracerouteTask.TYPE,
System.currentTimeMillis() * 1000, taskProgress, this.measurementDesc);
result.addResult("num_hops", hop.ttl);
for (int i = 0; i < hopHosts.size(); i++) {
HopInfo hopInfo = hopHosts.get(i);
int hostIdx = 1;
for (String host : hopInfo.hosts) {
result.addResult("hop_" + hopInfo.ttl + "_addr_" + hostIdx++, host);
}
result.addResult("hop_" + hopInfo.ttl + "_rtt_ms", String.format("%.3f", hopInfo.rtt));
if (hopInfo.hosts.contains(hostIp)){
break;
}
}
Logger.i(MeasurementJsonConvertor.toJsonString(result));
MeasurementResult[] mrArray = new MeasurementResult[1];
mrArray[0] = result;
return mrArray;
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
Logger.e("cannot perform traceroute to " + task.target);
throw new MeasurementError("cannot perform traceroute to " + task.target);
}
@SuppressWarnings("rawtypes")
public static Class getDescClass() throws InvalidClassException {
return TracerouteDesc.class;
}
@Override
public String getType() {
return TracerouteTask.TYPE;
}
@Override
public String getDescriptor() {
return DESCRIPTOR;
}
private void cleanUp(Process proc) {
if (proc != null) {
// destroy() closes all open streams
proc.destroy();
}
}
private HashSet<String> processPingOutput(BufferedReader br, String hostIp) throws IOException {
HashSet<String> hostsAtThisDistance = new HashSet<String>();
String line = null;
while ((line = br.readLine()) != null) {
if (line.startsWith("From")) {
String ip = getHostIp(line);
if (ip != null && ip.compareTo(hostIp) != 0) {
Logger.d("IP: " + ip);
hostsAtThisDistance.add(ip);
}
} else if (line.contains("time=")) {
hostsAtThisDistance.add(hostIp);
}
}
return hostsAtThisDistance;
}
/*
* TODO(Wenjie): The current search for valid IPs assumes the IP string is not a proper substring
* of the space-separated tokens. For more robust searching in case different outputs from ping
* due to its different versions, we need to refine the search by testing weather any substring of
* the tokens contains a valid IP
*/
private String getHostIp(String line) {
String[] tokens = line.split(" ");
// In most cases, the second element in the array is the IP
String tempIp = tokens[1];
/**
* In Android 4.3 or above, the second token of the result is like "192.168.1.1:". So we should
* remove the last ":"
*/
if (tempIp.endsWith(":")) {
tempIp = tempIp.substring(0, tempIp.length() - 1);
}
if (isValidIpv4Addr(tempIp) || isValidIpv6Addr(tempIp)) {
return tempIp;
} else {
for (int i = 0; i < tokens.length; i++) {
if (i == 1) {
// Examined already
continue;
} else {
if (isValidIpv4Addr(tokens[i]) || isValidIpv6Addr(tokens[i])) {
return tokens[i];
}
}
}
}
return null;
}
// Tells whether the string is an valid IPv4 address
private boolean isValidIpv4Addr(String ip) {
String[] tokens = ip.split("\\.");
if (tokens.length == 4) {
for (int i = 0; i < 4; i++) {
try {
int val = Integer.parseInt(tokens[i]);
if (val < 0 || val > 255) {
return false;
}
} catch (NumberFormatException e) {
Logger.d(ip + " is not a valid IPv4 address");
return false;
}
}
return true;
}
return false;
}
// Tells whether the string is an valid IPv6 address
private boolean isValidIpv6Addr(String ip) {
int max = Integer.valueOf("FFFF", 16);
String[] tokens = ip.split("\\:");
if (tokens.length <= 8) {
for (int i = 0; i < tokens.length; i++) {
try {
// zeros might get grouped
if (tokens[i].isEmpty())
continue;
int val = Integer.parseInt(tokens[i], 16);
if (val < 0 || val > max) {
return false;
}
} catch (NumberFormatException e) {
Logger.d(ip + " is not a valid IPv6 address");
return false;
}
}
return true;
}
return false;
}
private class HopInfo {
// The hosts at a given hop distance
public HashSet<String> hosts;
// The average RRT for this hop distance
public double rtt;
public int ttl;
protected HopInfo(HashSet<String> hosts, double rtt, int ttl) {
this.hosts = hosts;
this.rtt = rtt;
this.ttl = ttl;
}
}
@Override
public String toString() {
TracerouteDesc desc = (TracerouteDesc) measurementDesc;
return "[Traceroute]\n Target: " + desc.target + "\n Interval (sec): " + desc.intervalSec
+ "\n Next run: " + desc.startTime;
}
// Measure the actual ping process execution time
private class ProcWrapper extends Thread {
public long duration = 0;
private final Process process;
private Integer exitStatus = null;
private ProcWrapper(Process process) {
this.process = process;
}
public void run() {
try {
long startTime = System.currentTimeMillis();
exitStatus = process.waitFor();
duration = System.currentTimeMillis() - startTime;
} catch (InterruptedException e) {
Logger.e("Traceroute thread gets interrupted");
}
}
}
class HopExecutor implements Callable {
private int pingsPerHop;
private String command;
private String hostIp;
private Process pingProc = null;
private int ttl;
public HopExecutor(int pingsPerHop, String command, String hostIp, int ttl) {
this.pingsPerHop = pingsPerHop;
this.command = command;
this.ttl = ttl;
this.hostIp = hostIp;
}
@Override
public HopInfo call() {
double rtt = 0;
HashSet<String> hostsAtThisDistance = new HashSet<String>();
try {
int effectiveTask = 0;
ExecutorService executor = Executors.newFixedThreadPool(pingsPerHop);
ArrayList<Runnable> workers = new ArrayList<Runnable>();
for (int i = 0; i < pingsPerHop; i++) {
// Actual packet is 28 bytes larger than the size specified.
// Three packets are sent in each direction
// dataConsumed += (task.packetSizeByte + 28) * 2 * 3;//TODO
pingProc = Runtime.getRuntime().exec(command);
Runnable worker = new PingExecutor(pingProc, hostIp);
executor.execute(worker);
workers.add(worker);
}
executor.shutdown();
try {
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (Runnable w : workers) {
rtt += ((PingExecutor) w).getRtt();
if (((PingExecutor) w).getRtt() != 0) {
effectiveTask++;
for (String h : ((PingExecutor) w).getHosts()) {
hostsAtThisDistance.add(h);
}
}
}
rtt = (effectiveTask != 0) ? (rtt / effectiveTask) : -1;
if (rtt == -1) {
String Unreachablehost = "";
for (int i = 0; i < pingsPerHop; i++) {
Unreachablehost += "* ";
}
hostsAtThisDistance.add(Unreachablehost);
}
} catch (SecurityException e) {
Logger.e("Does not have the permission to run ping on this device");
} catch (IOException e) {
Logger.e("The ping program cannot be executed");
Logger.e(e.getMessage());
} finally {
cleanUp(pingProc);
}
return new HopInfo(hostsAtThisDistance, rtt, ttl);
}
}
class PingExecutor implements Runnable {
private Process proc;
private double rtt;
private String hostIp;
private HashSet<String> hosts;
public PingExecutor(Process proc, String hostIp) {
this.proc = proc;
rtt = 0;
this.hostIp = hostIp;
hosts = new HashSet<String>();
}
public double getRtt() {
return rtt;
}
public HashSet<String> getHosts() {
return hosts;
}
@Override
public void run() {
// Wait for process to finish
// Enforce thread timeout if pingProc doesn't respond
ProcWrapper procwrapper = new ProcWrapper(proc);
procwrapper.start();
try {
long pingThreadTimeout = 5000;
procwrapper.join(pingThreadTimeout);
if (procwrapper.exitStatus == null)
throw new TimeoutException();
} catch (InterruptedException ex) {
procwrapper.interrupt();
Thread.currentThread().interrupt();
Logger.e("Traceroute process gets interrupted");
cleanUp(proc);
return;
} catch (TimeoutException e) {
Logger.e("Traceroute process timeout");
cleanUp(proc);
return;
}
rtt += procwrapper.duration;
// Grab the output of the process that runs the ping command
InputStream is = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
/*
* Process each line of the ping output and extracts the intermediate hops into
* hostAtThisDistance
*/
try {
hosts = processPingOutput(br, hostIp);
} catch (IOException e) {
e.printStackTrace();
}
cleanUp(proc);
// try {
// Thread.sleep((long) (task.pingIntervalSec * 1000));
// } catch (InterruptedException e) {
// Logger.i("Sleep interrupted between ping intervals");
// }
}
}
@Override
public long getDuration() {
return this.duration - this.totalRunningTime;
}
@Override
public void setDuration(long newDuration) {
if (newDuration < 0) {
this.duration = 0;
} else {
this.duration = newDuration;
}
}
@Override
public boolean pause() {
pauseFlag = true;
return true;
}
@Override
public boolean stop() {
stopFlag = true;
// cleanUp(pingProc);TODO
return true;
}
@Override
public long getTotalRunningTime() {
return this.totalRunningTime;
}
@Override
public void updateTotalRunningTime(long duration) {
this.totalRunningTime += duration;
}
/**
* Based on counting the number of pings sent
*/
@Override
public long getDataConsumed() {
return dataConsumed;
}
}