1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
|
/* jshint esnext: true */
/* global DataView */
"use strict";
this.EXPORTED_SYMBOLS = ["LogParser"];
/**
* Parse an array read from a /dev/log/ file. Format taken from
* kernel/drivers/staging/android/logger.h and system/core/logcat/logcat.cpp
*
* @param array {Uint8Array} Array read from /dev/log/ file
* @return {Array} List of log messages
*/
function parseLogArray(array) {
let data = new DataView(array.buffer);
let byteString = String.fromCharCode.apply(null, array);
// Length of bytes that precede the payload of a log message
// From the 5 Uint32 and 1 Uint8 reads
const HEADER_LENGTH = 21;
let logMessages = [];
let pos = 0;
while (pos + HEADER_LENGTH < byteString.length) {
// Parse a single log entry
// Track current offset from global position
let offset = 0;
// Length of the entry, discarded
let length = data.getUint32(pos + offset, true);
offset += 4;
// Id of the process which generated the message
let processId = data.getUint32(pos + offset, true);
offset += 4;
// Id of the thread which generated the message
let threadId = data.getUint32(pos + offset, true);
offset += 4;
// Seconds since epoch when this message was logged
let seconds = data.getUint32(pos + offset, true);
offset += 4;
// Nanoseconds since the last second
let nanoseconds = data.getUint32(pos + offset, true);
offset += 4;
// Priority in terms of the ANDROID_LOG_* constants (see below)
// This is where the length field begins counting
let priority = data.getUint8(pos + offset);
// Reset pos and offset to count from here
pos += offset;
offset = 0;
offset += 1;
// Read the tag and message, represented as null-terminated c-style strings
let tag = "";
while (byteString[pos + offset] != "\0") {
tag += byteString[pos + offset];
offset ++;
}
offset ++;
let message = "";
// The kernel log driver may have cut off the null byte (logprint.c)
while (byteString[pos + offset] != "\0" && offset < length) {
message += byteString[pos + offset];
offset ++;
}
// Un-skip the missing null terminator
if (offset === length) {
offset --;
}
offset ++;
pos += offset;
// Log messages are occasionally delimited by newlines, but are also
// sometimes followed by newlines as well
if (message.charAt(message.length - 1) === "\n") {
message = message.substring(0, message.length - 1);
}
// Add an aditional time property to mimic the milliseconds since UTC
// expected by Date
let time = seconds * 1000.0 + nanoseconds/1000000.0;
// Log messages with interleaved newlines are considered to be separate log
// messages by logcat
for (let lineMessage of message.split("\n")) {
logMessages.push({
processId: processId,
threadId: threadId,
seconds: seconds,
nanoseconds: nanoseconds,
time: time,
priority: priority,
tag: tag,
message: lineMessage + "\n"
});
}
}
return logMessages;
}
/**
* Get a thread-time style formatted string from time
* @param time {Number} Milliseconds since epoch
* @return {String} Formatted time string
*/
function getTimeString(time) {
let date = new Date(time);
function pad(number) {
if ( number < 10 ) {
return "0" + number;
}
return number;
}
return pad( date.getMonth() + 1 ) +
"-" + pad( date.getDate() ) +
" " + pad( date.getHours() ) +
":" + pad( date.getMinutes() ) +
":" + pad( date.getSeconds() ) +
"." + (date.getMilliseconds() / 1000).toFixed(3).slice(2, 5);
}
/**
* Pad a string using spaces on the left
* @param str {String} String to pad
* @param width {Number} Desired string length
*/
function padLeft(str, width) {
while (str.length < width) {
str = " " + str;
}
return str;
}
/**
* Pad a string using spaces on the right
* @param str {String} String to pad
* @param width {Number} Desired string length
*/
function padRight(str, width) {
while (str.length < width) {
str = str + " ";
}
return str;
}
/** Constant values taken from system/core/liblog */
const ANDROID_LOG_UNKNOWN = 0;
const ANDROID_LOG_DEFAULT = 1;
const ANDROID_LOG_VERBOSE = 2;
const ANDROID_LOG_DEBUG = 3;
const ANDROID_LOG_INFO = 4;
const ANDROID_LOG_WARN = 5;
const ANDROID_LOG_ERROR = 6;
const ANDROID_LOG_FATAL = 7;
const ANDROID_LOG_SILENT = 8;
/**
* Map a priority number to its abbreviated string equivalent
* @param priorityNumber {Number} Log-provided priority number
* @return {String} Priority number's abbreviation
*/
function getPriorityString(priorityNumber) {
switch (priorityNumber) {
case ANDROID_LOG_VERBOSE:
return "V";
case ANDROID_LOG_DEBUG:
return "D";
case ANDROID_LOG_INFO:
return "I";
case ANDROID_LOG_WARN:
return "W";
case ANDROID_LOG_ERROR:
return "E";
case ANDROID_LOG_FATAL:
return "F";
case ANDROID_LOG_SILENT:
return "S";
default:
return "?";
}
}
/**
* Mimic the logcat "threadtime" format, generating a formatted string from a
* log message object.
* @param logMessage {Object} A log message from the list returned by parseLogArray
* @return {String} threadtime formatted summary of the message
*/
function formatLogMessage(logMessage) {
// MM-DD HH:MM:SS.ms pid tid priority tag: message
// from system/core/liblog/logprint.c:
return getTimeString(logMessage.time) +
" " + padLeft(""+logMessage.processId, 5) +
" " + padLeft(""+logMessage.threadId, 5) +
" " + getPriorityString(logMessage.priority) +
" " + padRight(logMessage.tag, 8) +
": " + logMessage.message;
}
/**
* Convert a string to a utf-8 Uint8Array
* @param {String} str
* @return {Uint8Array}
*/
function textEncode(str) {
return new TextEncoder("utf-8").encode(str);
}
/**
* Pretty-print an array of bytes read from a log file by parsing then
* threadtime formatting its entries.
* @param array {Uint8Array} Array of a log file's bytes
* @return {Uint8Array} Pretty-printed log
*/
function prettyPrintLogArray(array) {
let logMessages = parseLogArray(array);
return textEncode(logMessages.map(formatLogMessage).join(""));
}
/**
* Pretty-print an array read from the list of propreties.
* @param {Object} Object representing the properties
* @return {Uint8Array} Human-readable string of property name: property value
*/
function prettyPrintPropertiesArray(properties) {
let propertiesString = "";
for(let propName in properties) {
propertiesString += "[" + propName + "]: [" + properties[propName] + "]\n";
}
return textEncode(propertiesString);
}
/**
* Pretty-print a normal array. Does nothing.
* @param array {Uint8Array} Input array
* @return {Uint8Array} The same array
*/
function prettyPrintArray(array) {
return array;
}
this.LogParser = {
parseLogArray: parseLogArray,
prettyPrintArray: prettyPrintArray,
prettyPrintLogArray: prettyPrintLogArray,
prettyPrintPropertiesArray: prettyPrintPropertiesArray
};
|