/* 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 android.util.Log; import com.mobilyzer.Config; import com.mobilyzer.MeasurementDesc; import com.mobilyzer.MeasurementResult; import com.mobilyzer.MeasurementTask; 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; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.InvalidClassException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Date; import java.util.Map; /** * A callable that executes a ping task using one of three methods */ public class PingTask extends MeasurementTask{ // Type name for internal use public static final String TYPE = "ping"; // Human readable name for the task public static final String DESCRIPTOR = "ping"; /* 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_PING_TTL = 51; private long duration; private Process pingProc = null; private String PING_METHOD_CMD = "ping_cmd"; private String PING_METHOD_JAVA = "java_ping"; private String PING_METHOD_HTTP = "http"; private String targetIp = null; //Track data consumption for this task to avoid exceeding user's limit private long dataConsumed; /** * Encode ping specific parameters, along with common parameters inherited from MeasurmentDesc * @author wenjiezeng@google.com (Steve Zeng) * */ public static class PingDesc extends MeasurementDesc { public String pingExe = null; // Host address either in the numeric form or domain names public String target = null; // The payload size in bytes of the ICMP packet public int packetSizeByte = PingTask.DEFAULT_PING_PACKET_SIZE; public int pingTimeoutSec = PingTask.DEFAULT_PING_TIMEOUT; public int pingTimeToLive= PingTask.DEFAULT_PING_TTL; public double pingIcmpIntervalSec= Config.DEFAULT_INTERVAL_BETWEEN_ICMP_PACKET_SEC; public PingDesc(String key, Date startTime, Date endTime, double intervalSec, long count, long priority, int contextIntervalSec, Map<String, String> params) throws InvalidParameterException { super(PingTask.TYPE, key, startTime, endTime, intervalSec, count, priority, contextIntervalSec,params); initializeParams(params); if (this.target == null || this.target.length() == 0) { throw new InvalidParameterException("PingTask cannot be created due " + " to null target string"); } } @Override protected void initializeParams(Map<String, String> params) { if (params == null) { return; } this.target = params.get("target"); try { String val = null; if ((val = params.get("packet_size_byte")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.packetSizeByte = Integer.parseInt(val); } if ((val = params.get("ping_timeout_sec")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.pingTimeoutSec = Integer.parseInt(val); } if ((val = params.get("ttl")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { this.pingTimeToLive = Integer.parseInt(val); } if ((val = params.get("icmp_interval_sec")) != null && val.length() > 0 && Double.parseDouble(val) > 0) { this.pingIcmpIntervalSec = Double.parseDouble(val); } } catch (NumberFormatException e) { throw new InvalidParameterException("PingTask cannot be created due to invalid params"); } } @Override public String getType() { return PingTask.TYPE; } protected PingDesc(Parcel in) { super(in); pingExe = in.readString(); target = in.readString(); packetSizeByte = in.readInt(); pingTimeoutSec = in.readInt(); pingTimeToLive = in.readInt(); pingIcmpIntervalSec = in.readDouble(); } public static final Parcelable.Creator<PingDesc> CREATOR = new Parcelable.Creator<PingDesc>() { public PingDesc createFromParcel(Parcel in) { return new PingDesc(in); } public PingDesc[] newArray(int size) { return new PingDesc[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(pingExe); dest.writeString(target); dest.writeInt(packetSizeByte); dest.writeInt(pingTimeoutSec); dest.writeInt(pingTimeToLive); dest.writeDouble(pingIcmpIntervalSec); } } @SuppressWarnings("rawtypes") public static Class getDescClass() throws InvalidClassException { return PingDesc.class; } public PingTask(MeasurementDesc desc) { super(new PingDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); this.duration=Config.PING_COUNT_PER_MEASUREMENT*500; // this.taskProgress=TaskProgress.FAILED; // this.stopFlag=false; this.dataConsumed=0; } protected PingTask(Parcel in) { super(in); duration = in.readLong(); dataConsumed = in.readLong(); } public static final Parcelable.Creator<PingTask> CREATOR = new Parcelable.Creator<PingTask>() { public PingTask createFromParcel(Parcel in) { return new PingTask(in); } public PingTask[] newArray(int size) { return new PingTask[size]; } }; @Override public int describeContents() { return super.describeContents(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeLong(duration); dest.writeLong(dataConsumed); } /** * Returns a copy of the PingTask */ @Override public MeasurementTask clone() { MeasurementDesc desc = this.measurementDesc; PingDesc newDesc = new PingDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters); return new PingTask(newDesc); } /* We will use three methods to ping the requested resource in the order of PING_COMMAND, * JAVA_ICMP_PING, and HTTP_PING. If all fails, then we declare the resource unreachable */ @Override public MeasurementResult[] call() throws MeasurementError { MeasurementResult[] result = null; PingDesc desc = (PingDesc) measurementDesc; int ipByteLength; try { InetAddress addr = InetAddress.getByName(desc.target); // Get the address length ipByteLength = addr.getAddress().length; Logger.i("IP address length is " + ipByteLength); // All ping methods ping against targetIp rather than desc.target targetIp = addr.getHostAddress(); Logger.i("IP is " + targetIp); } catch (UnknownHostException e) { throw new MeasurementError("Unknown host " + desc.target); } result=new MeasurementResult[1]; try { Logger.i("running ping command"); // Prevents the phone from going to low-power mode where WiFi turns off result[0]=executePingCmdTask(ipByteLength); return result; } catch (MeasurementError e) { try { Logger.i("running java ping"); result[0]=executeJavaPingTask(); return result; } catch (MeasurementError ee) { Logger.i("running http ping"); result[0]=executeHttpPingTask(); return result; } } } @Override public String getType() { return PingTask.TYPE; } @Override public String getDescriptor() { return DESCRIPTOR; } private MeasurementResult constructResult(ArrayList<Double> rrtVals, double packetLoss, int packetsSent, String pingMethod) { double min = Double.MAX_VALUE; double max = Double.MIN_VALUE; double mdev, avg, filteredAvg; double total = 0; if (rrtVals.size() == 0) { return null; } for (double rrt : rrtVals) { if (rrt < min) { min = rrt; } if (rrt > max) { max = rrt; } total += rrt; } avg = total / rrtVals.size(); mdev = Util.getStandardDeviation(rrtVals, avg); filteredAvg = filterPingResults(rrtVals, avg); PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, phoneUtils.getDeviceProperty(this.getKey()), PingTask.TYPE, System.currentTimeMillis() * 1000, TaskProgress.COMPLETED, this.measurementDesc); result.addResult("target_ip", targetIp); result.addResult("mean_rtt_ms", avg); result.addResult("min_rtt_ms", min); result.addResult("max_rtt_ms", max); result.addResult("stddev_rtt_ms", mdev); if (filteredAvg != avg) { result.addResult("filtered_mean_rtt_ms", filteredAvg); } result.addResult("packet_loss", packetLoss); result.addResult("packets_sent", packetsSent); result.addResult("ping_method", pingMethod); Logger.i(MeasurementJsonConvertor.toJsonString(result)); return result; } private void cleanUp(Process proc) { try { if (proc != null) { proc.destroy(); } } catch (Exception e) { Logger.w("Unable to kill ping process" + e.getMessage()); } } /** * Compute the average of the filtered rtts. * The first several ping results are usually extremely large as the device * needs to activate the wireless interface and resolve domain names. * Such distorted measurements are filtered out */ private double filterPingResults(final ArrayList<Double> rrts, double avg) { double rrtAvg = avg; // Our # of results should be less than the # of times we ping try { ArrayList<Double> filteredResults = Util.applyInnerBandFilter(rrts, Double.MIN_VALUE, rrtAvg * Config.PING_FILTER_THRES); // Now we compute the average again based on the filtered results if (filteredResults != null && filteredResults.size() > 0) { rrtAvg = Util.getSum(filteredResults) / filteredResults.size(); } } catch (InvalidParameterException e) { Log.wtf("", "This should never happen because rrts is never empty"); } return rrtAvg; } // Runs when SystemState is IDLE private MeasurementResult executePingCmdTask(int ipByteLen) throws MeasurementError { Logger.i("Starting executePingCmdTask"); PingDesc pingTask = (PingDesc) this.measurementDesc; String errorMsg = ""; MeasurementResult measurementResult = null; // TODO(Wenjie): Add a exhaustive list of ping locations for different // Android phones pingTask.pingExe = Util.pingExecutableBasedOnIPType(ipByteLen); Logger.i("Ping executable is " + pingTask.pingExe); if (pingTask.pingExe == null) { Logger.e("Ping executable not found"); throw new MeasurementError("Ping executable not found"); } try { String command = Util.constructCommand(pingTask.pingExe, "-i", pingTask.pingIcmpIntervalSec, "-s", pingTask.packetSizeByte, "-w", pingTask.pingTimeoutSec, "-c", Config.PING_COUNT_PER_MEASUREMENT, "-t" ,pingTask.pingTimeToLive, targetIp); Logger.i("Running: " + command); pingProc = Runtime.getRuntime().exec(command); dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; // Grab the output of the process that runs the ping command InputStream is = pingProc.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line = null; int lineCnt = 0; ArrayList<Double> rrts = new ArrayList<Double>(); ArrayList<Integer> receivedIcmpSeq = new ArrayList<Integer>(); double packetLoss = Double.MIN_VALUE; int packetsSent = Config.PING_COUNT_PER_MEASUREMENT; // Process each line of the ping output and store the rrt in array rrts. while ((line = br.readLine()) != null) { // Ping prints a number of 'param=value' pairs, among which we only need // the 'time=rrt_val' pair String[] extractedValues = Util.extractInfoFromPingOutput(line); if (extractedValues != null) { int curIcmpSeq = Integer.parseInt(extractedValues[0]); double rrtVal = Double.parseDouble(extractedValues[1]); // ICMP responses from the system ping command could be duplicate // and out of order if (!receivedIcmpSeq.contains(curIcmpSeq)) { rrts.add(rrtVal); receivedIcmpSeq.add(curIcmpSeq); } } // Get the number of sent/received pings from the ping command output int[] packetLossInfo = Util.extractPacketLossInfoFromPingOutput(line); if (packetLossInfo != null) { packetsSent = packetLossInfo[0]; int packetsReceived = packetLossInfo[1]; packetLoss = 1 - ((double) packetsReceived / (double) packetsSent); } Logger.i(line); } // Use the output from the ping command to compute packet loss. If that's not // available, use an estimation. if (packetLoss == Double.MIN_VALUE) { packetLoss = 1 - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); } measurementResult = constructResult(rrts, packetLoss, packetsSent, PING_METHOD_CMD); } catch (IOException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } catch (SecurityException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } catch (NumberFormatException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } catch (InvalidParameterException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } finally { // All associated streams with the process will be closed upon destroy() cleanUp(pingProc); } if (measurementResult == null) { Logger.e("Error running ping: " + errorMsg); throw new MeasurementError(errorMsg); } return measurementResult; } // Runs when the ping command fails private MeasurementResult executeJavaPingTask() throws MeasurementError { PingDesc pingTask = (PingDesc) this.measurementDesc; long pingStartTime = 0; long pingEndTime = 0; ArrayList<Double> rrts = new ArrayList<Double>(); String errorMsg = ""; MeasurementResult result = null; try { int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec / Config.PING_COUNT_PER_MEASUREMENT); int successfulPingCnt = 0; long totalPingDelay = 0; for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) { pingStartTime = System.currentTimeMillis(); boolean status = InetAddress.getByName(targetIp).isReachable(timeOut); pingEndTime = System.currentTimeMillis(); long rrtVal = pingEndTime - pingStartTime; if (status) { totalPingDelay += rrtVal; rrts.add((double) rrtVal); } } Logger.i("java ping succeeds"); double packetLoss = 1 - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; result = constructResult(rrts, packetLoss, Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_JAVA); } catch (IllegalArgumentException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } catch (IOException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } if (result != null) { return result; } else { Logger.i("java ping fails"); throw new MeasurementError(errorMsg); } } /** * Use the HTTP Head method to emulate ping. The measurement from this method * can be substantially (2x) greater than the first two methods and inaccurate. * This is because, depending on the implementing of the destination web * server, either a quick HTTP response is replied or some actual heavy * lifting will be done in preparing the response * */ private MeasurementResult executeHttpPingTask() throws MeasurementError { long pingStartTime = 0; long pingEndTime = 0; ArrayList<Double> rrts = new ArrayList<Double>(); PingDesc pingTask = (PingDesc) this.measurementDesc; String errorMsg = ""; MeasurementResult result = null; try { long totalPingDelay = 0; URL url = new URL("http://"+ pingTask.target); int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec / Config.PING_COUNT_PER_MEASUREMENT); for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) { pingStartTime = System.currentTimeMillis(); HttpURLConnection httpClient = (HttpURLConnection) url.openConnection(); httpClient.setRequestProperty("Connection", "close"); httpClient.setRequestMethod("HEAD"); httpClient.setReadTimeout(timeOut); httpClient.setConnectTimeout(timeOut); httpClient.connect(); pingEndTime = System.currentTimeMillis(); httpClient.disconnect(); rrts.add((double) (pingEndTime - pingStartTime)); } Logger.i("HTTP get ping succeeds"); Logger.i("RTT is " + rrts.toString()); double packetLoss = 1 - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; result = constructResult(rrts, packetLoss, Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_HTTP); } catch (MalformedURLException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } catch (IOException e) { Logger.e(e.getMessage()); errorMsg += e.getMessage() + "\n"; } if (result != null) { return result; } else { Logger.i("HTTP get ping fails"); throw new MeasurementError(errorMsg); } } @Override public String toString() { PingDesc desc = (PingDesc) measurementDesc; return "[Ping]\n Target: " + desc.target + "\n Interval (sec): " + desc.intervalSec + "\n Next run: " + desc.startTime; } @Override public boolean stop() { return false; } @Override public long getDuration() { return this.duration; } @Override public void setDuration(long newDuration) { if(newDuration<0){ this.duration=0; }else{ this.duration=newDuration; } } /** * Data sent so far by this task. * * We count packets sent directly to calculate the data sent */ @Override public long getDataConsumed() { return dataConsumed; } }