1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ddmlib.log;
18
19import com.android.ddmlib.IDevice;
20import com.android.ddmlib.Log;
21import com.android.ddmlib.MultiLineReceiver;
22import com.android.ddmlib.log.EventContainer.EventValueType;
23import com.android.ddmlib.log.EventValueDescription.ValueType;
24import com.android.ddmlib.log.LogReceiver.LogEntry;
25import com.android.ddmlib.utils.ArrayHelper;
26
27import java.io.BufferedReader;
28import java.io.File;
29import java.io.FileOutputStream;
30import java.io.FileReader;
31import java.io.IOException;
32import java.io.UnsupportedEncodingException;
33import java.util.ArrayList;
34import java.util.Calendar;
35import java.util.Map;
36import java.util.Map.Entry;
37import java.util.Set;
38import java.util.TreeMap;
39import java.util.regex.Matcher;
40import java.util.regex.Pattern;
41
42/**
43 * Parser for the "event" log.
44 */
45public final class EventLogParser {
46
47    /** Location of the tag map file on the device */
48    private final static String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$
49
50    /**
51     * Event log entry types.  These must match up with the declarations in
52     * java/android/android/util/EventLog.java.
53     */
54    private final static int EVENT_TYPE_INT      = 0;
55    private final static int EVENT_TYPE_LONG     = 1;
56    private final static int EVENT_TYPE_STRING   = 2;
57    private final static int EVENT_TYPE_LIST     = 3;
58
59    private final static Pattern PATTERN_SIMPLE_TAG = Pattern.compile(
60    "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$
61    private final static Pattern PATTERN_TAG_WITH_DESC = Pattern.compile(
62            "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$
63    private final static Pattern PATTERN_DESCRIPTION = Pattern.compile(
64            "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$
65
66    private final static Pattern TEXT_LOG_LINE = Pattern.compile(
67            "(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$
68
69    private final TreeMap<Integer, String> mTagMap = new TreeMap<Integer, String>();
70
71    private final TreeMap<Integer, EventValueDescription[]> mValueDescriptionMap =
72        new TreeMap<Integer, EventValueDescription[]>();
73
74    public EventLogParser() {
75    }
76
77    /**
78     * Inits the parser for a specific Device.
79     * <p/>
80     * This methods reads the event-log-tags located on the device to find out
81     * what tags are being written to the event log and what their format is.
82     * @param device The device.
83     * @return <code>true</code> if success, <code>false</code> if failure or cancellation.
84     */
85    public boolean init(IDevice device) {
86        // read the event tag map file on the device.
87        try {
88            device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$
89                    new MultiLineReceiver() {
90                @Override
91                public void processNewLines(String[] lines) {
92                    for (String line : lines) {
93                        processTagLine(line);
94                    }
95                }
96                @Override
97                public boolean isCancelled() {
98                    return false;
99                }
100            });
101        } catch (Exception e) {
102            // catch all possible exceptions and return false.
103            return false;
104        }
105
106        return true;
107    }
108
109    /**
110     * Inits the parser with the content of a tag file.
111     * @param tagFileContent the lines of a tag file.
112     * @return <code>true</code> if success, <code>false</code> if failure.
113     */
114    public boolean init(String[] tagFileContent) {
115        for (String line : tagFileContent) {
116            processTagLine(line);
117        }
118        return true;
119    }
120
121    /**
122     * Inits the parser with a specified event-log-tags file.
123     * @param filePath
124     * @return <code>true</code> if success, <code>false</code> if failure.
125     */
126    public boolean init(String filePath)  {
127        BufferedReader reader = null;
128        try {
129            reader = new BufferedReader(new FileReader(filePath));
130
131            String line = null;
132            do {
133                line = reader.readLine();
134                if (line != null) {
135                    processTagLine(line);
136                }
137            } while (line != null);
138
139            return true;
140        } catch (IOException e) {
141            return false;
142        } finally {
143            try {
144                if (reader != null) {
145                    reader.close();
146                }
147            } catch (IOException e) {
148                // ignore
149            }
150        }
151    }
152
153    /**
154     * Processes a line from the event-log-tags file.
155     * @param line the line to process
156     */
157    private void processTagLine(String line) {
158        // ignore empty lines and comment lines
159        if (line.length() > 0 && line.charAt(0) != '#') {
160            Matcher m = PATTERN_TAG_WITH_DESC.matcher(line);
161            if (m.matches()) {
162                try {
163                    int value = Integer.parseInt(m.group(1));
164                    String name = m.group(2);
165                    if (name != null && mTagMap.get(value) == null) {
166                        mTagMap.put(value, name);
167                    }
168
169                    // special case for the GC tag. We ignore what is in the file,
170                    // and take what the custom GcEventContainer class tells us.
171                    // This is due to the event encoding several values on 2 longs.
172                    // @see GcEventContainer
173                    if (value == GcEventContainer.GC_EVENT_TAG) {
174                        mValueDescriptionMap.put(value,
175                            GcEventContainer.getValueDescriptions());
176                    } else {
177
178                        String description = m.group(3);
179                        if (description != null && description.length() > 0) {
180                            EventValueDescription[] desc =
181                                processDescription(description);
182
183                            if (desc != null) {
184                                mValueDescriptionMap.put(value, desc);
185                            }
186                        }
187                    }
188                } catch (NumberFormatException e) {
189                    // failed to convert the number into a string. just ignore it.
190                }
191            } else {
192                m = PATTERN_SIMPLE_TAG.matcher(line);
193                if (m.matches()) {
194                    int value = Integer.parseInt(m.group(1));
195                    String name = m.group(2);
196                    if (name != null && mTagMap.get(value) == null) {
197                        mTagMap.put(value, name);
198                    }
199                }
200            }
201        }
202    }
203
204    private EventValueDescription[] processDescription(String description) {
205        String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$
206
207        ArrayList<EventValueDescription> list = new ArrayList<EventValueDescription>();
208
209        for (String desc : descriptions) {
210            Matcher m = PATTERN_DESCRIPTION.matcher(desc);
211            if (m.matches()) {
212                try {
213                    String name = m.group(1);
214
215                    String typeString = m.group(2);
216                    int typeValue = Integer.parseInt(typeString);
217                    EventValueType eventValueType = EventValueType.getEventValueType(typeValue);
218                    if (eventValueType == null) {
219                        // just ignore this description if the value is not recognized.
220                        // TODO: log the error.
221                    }
222
223                    typeString = m.group(3);
224                    if (typeString != null && typeString.length() > 0) {
225                        //skip the |
226                        typeString = typeString.substring(1);
227
228                        typeValue = Integer.parseInt(typeString);
229                        ValueType valueType = ValueType.getValueType(typeValue);
230
231                        list.add(new EventValueDescription(name, eventValueType, valueType));
232                    } else {
233                        list.add(new EventValueDescription(name, eventValueType));
234                    }
235                } catch (NumberFormatException nfe) {
236                    // just ignore this description if one number is malformed.
237                    // TODO: log the error.
238                } catch (InvalidValueTypeException e) {
239                    // just ignore this description if data type and data unit don't match
240                    // TODO: log the error.
241                }
242            } else {
243                Log.e("EventLogParser",  //$NON-NLS-1$
244                    String.format("Can't parse %1$s", description));  //$NON-NLS-1$
245            }
246        }
247
248        if (list.size() == 0) {
249            return null;
250        }
251
252        return list.toArray(new EventValueDescription[list.size()]);
253
254    }
255
256    public EventContainer parse(LogEntry entry) {
257        if (entry.len < 4) {
258            return null;
259        }
260
261        int inOffset = 0;
262
263        int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset);
264        inOffset += 4;
265
266        String tag = mTagMap.get(tagValue);
267        if (tag == null) {
268            Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue));
269        }
270
271        ArrayList<Object> list = new ArrayList<Object>();
272        if (parseBinaryEvent(entry.data, inOffset, list) == -1) {
273            return null;
274        }
275
276        Object data;
277        if (list.size() == 1) {
278            data = list.get(0);
279        } else{
280            data = list.toArray();
281        }
282
283        EventContainer event = null;
284        if (tagValue == GcEventContainer.GC_EVENT_TAG) {
285            event = new GcEventContainer(entry, tagValue, data);
286        } else {
287            event = new EventContainer(entry, tagValue, data);
288        }
289
290        return event;
291    }
292
293    public EventContainer parse(String textLogLine) {
294        // line will look like
295        // 04-29 23:16:16.691 I/dvm_gc_info(  427): <data>
296        // where <data> is either
297        // [value1,value2...]
298        // or
299        // value
300        if (textLogLine.length() == 0) {
301            return null;
302        }
303
304        // parse the header first
305        Matcher m = TEXT_LOG_LINE.matcher(textLogLine);
306        if (m.matches()) {
307            try {
308                int month = Integer.parseInt(m.group(1));
309                int day = Integer.parseInt(m.group(2));
310                int hours = Integer.parseInt(m.group(3));
311                int minutes = Integer.parseInt(m.group(4));
312                int seconds = Integer.parseInt(m.group(5));
313                int milliseconds = Integer.parseInt(m.group(6));
314
315                // convert into seconds since epoch and nano-seconds.
316                Calendar cal = Calendar.getInstance();
317                cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds);
318                int sec = (int)Math.floor(cal.getTimeInMillis()/1000);
319                int nsec = milliseconds * 1000000;
320
321                String tag = m.group(7);
322
323                // get the numerical tag value
324                int tagValue = -1;
325                Set<Entry<Integer, String>> tagSet = mTagMap.entrySet();
326                for (Entry<Integer, String> entry : tagSet) {
327                    if (tag.equals(entry.getValue())) {
328                        tagValue = entry.getKey();
329                        break;
330                    }
331                }
332
333                if (tagValue == -1) {
334                    return null;
335                }
336
337                int pid = Integer.parseInt(m.group(8));
338
339                Object data = parseTextData(m.group(9), tagValue);
340                if (data == null) {
341                    return null;
342                }
343
344                // now we can allocate and return the EventContainer
345                EventContainer event = null;
346                if (tagValue == GcEventContainer.GC_EVENT_TAG) {
347                    event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
348                } else {
349                    event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
350                }
351
352                return event;
353            } catch (NumberFormatException e) {
354                return null;
355            }
356        }
357
358        return null;
359    }
360
361    public Map<Integer, String> getTagMap() {
362        return mTagMap;
363    }
364
365    public Map<Integer, EventValueDescription[]> getEventInfoMap() {
366        return mValueDescriptionMap;
367    }
368
369    /**
370     * Recursively convert binary log data to printable form.
371     *
372     * This needs to be recursive because you can have lists of lists.
373     *
374     * If we run out of room, we stop processing immediately.  It's important
375     * for us to check for space on every output element to avoid producing
376     * garbled output.
377     *
378     * Returns the amount read on success, -1 on failure.
379     */
380    private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList<Object> list) {
381
382        if (eventData.length - dataOffset < 1)
383            return -1;
384
385        int offset = dataOffset;
386
387        int type = eventData[offset++];
388
389        //fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen);
390
391        switch (type) {
392        case EVENT_TYPE_INT: { /* 32-bit signed int */
393                int ival;
394
395                if (eventData.length - offset < 4)
396                    return -1;
397                ival = ArrayHelper.swap32bitFromArray(eventData, offset);
398                offset += 4;
399
400                list.add(new Integer(ival));
401            }
402            break;
403        case EVENT_TYPE_LONG: { /* 64-bit signed long */
404                long lval;
405
406                if (eventData.length - offset < 8)
407                    return -1;
408                lval = ArrayHelper.swap64bitFromArray(eventData, offset);
409                offset += 8;
410
411                list.add(new Long(lval));
412            }
413            break;
414        case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */
415                int strLen;
416
417                if (eventData.length - offset < 4)
418                    return -1;
419                strLen = ArrayHelper.swap32bitFromArray(eventData, offset);
420                offset += 4;
421
422                if (eventData.length - offset < strLen)
423                    return -1;
424
425                // get the string
426                try {
427                    String str = new String(eventData, offset, strLen, "UTF-8"); //$NON-NLS-1$
428                    list.add(str);
429                } catch (UnsupportedEncodingException e) {
430                }
431                offset += strLen;
432                break;
433            }
434        case EVENT_TYPE_LIST: { /* N items, all different types */
435
436                if (eventData.length - offset < 1)
437                    return -1;
438
439                int count = eventData[offset++];
440
441                // make a new temp list
442                ArrayList<Object> subList = new ArrayList<Object>();
443                for (int i = 0; i < count; i++) {
444                    int result = parseBinaryEvent(eventData, offset, subList);
445                    if (result == -1) {
446                        return result;
447                    }
448
449                    offset += result;
450                }
451
452                list.add(subList.toArray());
453            }
454            break;
455        default:
456            Log.e("EventLogParser",  //$NON-NLS-1$
457                    String.format("Unknown binary event type %1$d", type));  //$NON-NLS-1$
458            return -1;
459        }
460
461        return offset - dataOffset;
462    }
463
464    private Object parseTextData(String data, int tagValue) {
465        // first, get the description of what we're supposed to parse
466        EventValueDescription[] desc = mValueDescriptionMap.get(tagValue);
467
468        if (desc == null) {
469            // TODO parse and create string values.
470            return null;
471        }
472
473        if (desc.length == 1) {
474            return getObjectFromString(data, desc[0].getEventValueType());
475        } else if (data.startsWith("[") && data.endsWith("]")) {
476            data = data.substring(1, data.length() - 1);
477
478            // get each individual values as String
479            String[] values = data.split(",");
480
481            if (tagValue == GcEventContainer.GC_EVENT_TAG) {
482                // special case for the GC event!
483                Object[] objects = new Object[2];
484
485                objects[0] = getObjectFromString(values[0], EventValueType.LONG);
486                objects[1] = getObjectFromString(values[1], EventValueType.LONG);
487
488                return objects;
489            } else {
490                // must be the same number as the number of descriptors.
491                if (values.length != desc.length) {
492                    return null;
493                }
494
495                Object[] objects = new Object[values.length];
496
497                for (int i = 0 ; i < desc.length ; i++) {
498                    Object obj = getObjectFromString(values[i], desc[i].getEventValueType());
499                    if (obj == null) {
500                        return null;
501                    }
502                    objects[i] = obj;
503                }
504
505                return objects;
506            }
507        }
508
509        return null;
510    }
511
512
513    private Object getObjectFromString(String value, EventValueType type) {
514        try {
515            switch (type) {
516                case INT:
517                    return Integer.valueOf(value);
518                case LONG:
519                    return Long.valueOf(value);
520                case STRING:
521                    return value;
522            }
523        } catch (NumberFormatException e) {
524            // do nothing, we'll return null.
525        }
526
527        return null;
528    }
529
530    /**
531     * Recreates the event-log-tags at the specified file path.
532     * @param filePath the file path to write the file.
533     * @throws IOException
534     */
535    public void saveTags(String filePath) throws IOException {
536        File destFile = new File(filePath);
537        destFile.createNewFile();
538        FileOutputStream fos = null;
539
540        try {
541
542            fos = new FileOutputStream(destFile);
543
544            for (Integer key : mTagMap.keySet()) {
545                // get the tag name
546                String tagName = mTagMap.get(key);
547
548                // get the value descriptions
549                EventValueDescription[] descriptors = mValueDescriptionMap.get(key);
550
551                String line = null;
552                if (descriptors != null) {
553                    StringBuilder sb = new StringBuilder();
554                    sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$
555                    boolean first = true;
556                    for (EventValueDescription evd : descriptors) {
557                        if (first) {
558                            sb.append(" ("); //$NON-NLS-1$
559                            first = false;
560                        } else {
561                            sb.append(",("); //$NON-NLS-1$
562                        }
563                        sb.append(evd.getName());
564                        sb.append("|"); //$NON-NLS-1$
565                        sb.append(evd.getEventValueType().getValue());
566                        sb.append("|"); //$NON-NLS-1$
567                        sb.append(evd.getValueType().getValue());
568                        sb.append("|)"); //$NON-NLS-1$
569                    }
570                    sb.append("\n"); //$NON-NLS-1$
571
572                    line = sb.toString();
573                } else {
574                    line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$
575                }
576
577                byte[] buffer = line.getBytes();
578                fos.write(buffer);
579            }
580        } finally {
581            if (fos != null) {
582                fos.close();
583            }
584        }
585    }
586
587
588}
589