logcat.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // cspell: ignore logcat
  2. import { AdbCommandBase, AdbSubprocessNoneProtocol } from '@yume-chan/adb';
  3. import { BufferedTransformStream, DecodeUtf8Stream, SplitStringStream, WrapReadableStream, WritableStream, type ReadableStream } from '@yume-chan/stream-extra';
  4. import Struct, { decodeUtf8, StructAsyncDeserializeStream } from '@yume-chan/struct';
  5. // `adb logcat` is an alias to `adb shell logcat`
  6. // so instead of adding to core library, it's implemented here
  7. // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/android/log.h;l=141;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
  8. export enum LogId {
  9. All = -1,
  10. Main,
  11. Radio,
  12. Events,
  13. System,
  14. Crash,
  15. Stats,
  16. Security,
  17. Kernel,
  18. }
  19. // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/android/log.h;l=73;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
  20. export enum AndroidLogPriority {
  21. Unknown,
  22. Default,
  23. Verbose,
  24. Debug,
  25. Info,
  26. Warn,
  27. Error,
  28. Fatal,
  29. Silent,
  30. }
  31. // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=140;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
  32. export const AndroidLogPriorityToCharacter: Record<AndroidLogPriority, string> = {
  33. [AndroidLogPriority.Unknown]: '?',
  34. [AndroidLogPriority.Default]: '?',
  35. [AndroidLogPriority.Verbose]: 'V',
  36. [AndroidLogPriority.Debug]: 'D',
  37. [AndroidLogPriority.Info]: 'I',
  38. [AndroidLogPriority.Warn]: 'W',
  39. [AndroidLogPriority.Error]: 'E',
  40. [AndroidLogPriority.Fatal]: 'F',
  41. [AndroidLogPriority.Silent]: 'S',
  42. };
  43. export enum LogcatFormat {
  44. Brief,
  45. Process,
  46. Tag,
  47. Thread,
  48. Raw,
  49. Time,
  50. ThreadTime,
  51. Long
  52. }
  53. export interface LogcatFormatModifiers {
  54. usec?: boolean;
  55. printable?: boolean;
  56. year?: boolean;
  57. zone?: boolean;
  58. epoch?: boolean;
  59. monotonic?: boolean;
  60. uid?: boolean;
  61. descriptive?: boolean;
  62. }
  63. export interface LogcatOptions {
  64. pid?: number;
  65. ids?: LogId[];
  66. }
  67. const NANOSECONDS_PER_SECOND = BigInt(1e9);
  68. // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/log/log_read.h;l=39;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
  69. export const LoggerEntry =
  70. new Struct({ littleEndian: true })
  71. .uint16('payloadSize')
  72. .uint16('headerSize')
  73. .int32('pid')
  74. .uint32('tid')
  75. .uint32('second')
  76. .uint32('nanoseconds')
  77. .uint32('logId')
  78. .uint32('uid')
  79. .extra({
  80. get timestamp() {
  81. return BigInt(this.second) * NANOSECONDS_PER_SECOND + BigInt(this.nanoseconds);
  82. },
  83. });
  84. export type LoggerEntry = typeof LoggerEntry['TDeserializeResult'];
  85. // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6;bpv=0
  86. export interface AndroidLogEntry extends LoggerEntry {
  87. priority: AndroidLogPriority;
  88. tag: string;
  89. message: string;
  90. }
  91. // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=1415;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
  92. export function formatAndroidLogEntry(
  93. entry: AndroidLogEntry,
  94. format: LogcatFormat = LogcatFormat.Brief,
  95. modifier?: LogcatFormatModifiers
  96. ) {
  97. const uid = modifier?.uid ? `${entry.uid.toString().padStart(5)}:` : '';
  98. switch (format) {
  99. // TODO: implement other formats
  100. default:
  101. return `${AndroidLogPriorityToCharacter[entry.priority]}/${entry.tag.padEnd(8)}(${uid}${entry.pid.toString().padStart(5)}): ${entry.message}`;
  102. }
  103. }
  104. function findTagEnd(payload: Uint8Array) {
  105. for (const separator of [0, ' '.charCodeAt(0), ':'.charCodeAt(0)]) {
  106. const index = payload.indexOf(separator);
  107. if (index !== -1) {
  108. return index;
  109. }
  110. }
  111. const index = payload.findIndex(x => x >= 0x7f);
  112. if (index !== -1) {
  113. return index;
  114. }
  115. return payload.length;
  116. }
  117. export async function deserializeAndroidLogEntry(stream: StructAsyncDeserializeStream): Promise<AndroidLogEntry> {
  118. const entry = await LoggerEntry.deserialize(stream) as unknown as AndroidLogEntry;
  119. if (entry.headerSize !== LoggerEntry.size) {
  120. await stream.read(entry.headerSize - LoggerEntry.size);
  121. }
  122. let payload = await stream.read(entry.payloadSize);
  123. // https://cs.android.com/android/platform/superproject/+/master:system/logging/logcat/logcat.cpp;l=193-194;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6
  124. // TODO: payload for some log IDs are in binary format.
  125. entry.priority = payload[0] as AndroidLogPriority;
  126. payload = payload.subarray(1);
  127. const tagEnd = findTagEnd(payload);
  128. entry.tag = decodeUtf8(payload.subarray(0, tagEnd));
  129. entry.message = tagEnd < payload.length - 1 ? decodeUtf8(payload.subarray(tagEnd + 1)) : '';
  130. return entry;
  131. }
  132. export interface LogSize {
  133. id: LogId;
  134. size: number;
  135. readable?: number;
  136. consumed: number;
  137. maxEntrySize: number;
  138. maxPayloadSize: number;
  139. }
  140. export class Logcat extends AdbCommandBase {
  141. public static logIdToName(id: LogId): string {
  142. return LogId[id]!;
  143. }
  144. public static logNameToId(name: string): LogId {
  145. const key = name[0]!.toUpperCase() + name.substring(1);
  146. return (LogId as any)[key];
  147. }
  148. public static joinLogId(ids: LogId[]): string {
  149. return ids.map(id => Logcat.logIdToName(id)).join(',');
  150. }
  151. public static parseSize(value: number, multiplier: string): number {
  152. const MULTIPLIERS = ['', 'Ki', 'Mi', 'Gi'];
  153. return value * 1024 ** (MULTIPLIERS.indexOf(multiplier) || 0);
  154. }
  155. // TODO: logcat: Support output format before Android 10
  156. // ref https://android-review.googlesource.com/c/platform/system/core/+/748128
  157. public static readonly LOG_SIZE_REGEX_10 = /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed\), max entry is (.*) B, max payload is (.*) B/;
  158. // Android 11 added `readable` part
  159. // ref https://android-review.googlesource.com/c/platform/system/core/+/1390940
  160. public static readonly LOG_SIZE_REGEX_11 = /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed, (.*) (.*)B readable\), max entry is (.*) B, max payload is (.*) B/;
  161. public async getLogSize(ids?: LogId[]): Promise<LogSize[]> {
  162. const { stdout } = await this.adb.subprocess.spawn([
  163. 'logcat',
  164. '-g',
  165. ...(ids ? ['-b', Logcat.joinLogId(ids)] : [])
  166. ]);
  167. const result: LogSize[] = [];
  168. await stdout
  169. .pipeThrough(new DecodeUtf8Stream())
  170. .pipeThrough(new SplitStringStream('\n'))
  171. .pipeTo(new WritableStream({
  172. write(chunk) {
  173. let match = chunk.match(Logcat.LOG_SIZE_REGEX_11);
  174. if (match) {
  175. result.push({
  176. id: Logcat.logNameToId(match[1]!),
  177. size: Logcat.parseSize(Number.parseInt(match[2]!, 10), match[3]!),
  178. readable: Logcat.parseSize(Number.parseInt(match[6]!, 10), match[7]!),
  179. consumed: Logcat.parseSize(Number.parseInt(match[4]!, 10), match[5]!),
  180. maxEntrySize: parseInt(match[8]!, 10),
  181. maxPayloadSize: parseInt(match[9]!, 10),
  182. });
  183. }
  184. match = chunk.match(Logcat.LOG_SIZE_REGEX_10);
  185. if (match) {
  186. result.push({
  187. id: Logcat.logNameToId(match[1]!),
  188. size: Logcat.parseSize(Number.parseInt(match[2]!, 10), match[3]!),
  189. consumed: Logcat.parseSize(Number.parseInt(match[4]!, 10), match[5]!),
  190. maxEntrySize: parseInt(match[6]!, 10),
  191. maxPayloadSize: parseInt(match[7]!, 10),
  192. });
  193. }
  194. },
  195. }));
  196. return result;
  197. }
  198. public async clear(ids?: LogId[]) {
  199. await this.adb.subprocess.spawnAndWait([
  200. 'logcat',
  201. '-c',
  202. ...(ids ? ['-b', Logcat.joinLogId(ids)] : []),
  203. ]);
  204. }
  205. public binary(options?: LogcatOptions): ReadableStream<AndroidLogEntry> {
  206. return new WrapReadableStream(async () => {
  207. // TODO: make `spawn` return synchronously with streams pending
  208. // so it's easier to chain them.
  209. const { stdout } = await this.adb.subprocess.spawn([
  210. 'logcat',
  211. '-B',
  212. ...(options?.pid ? ['--pid', options.pid.toString()] : []),
  213. ...(options?.ids ? ['-b', Logcat.joinLogId(options.ids)] : [])
  214. ], {
  215. // PERF: None protocol is 150% faster then Shell protocol
  216. protocols: [AdbSubprocessNoneProtocol],
  217. });
  218. return stdout;
  219. }).pipeThrough(new BufferedTransformStream(stream => {
  220. return deserializeAndroidLogEntry(stream);
  221. }));
  222. }
  223. }