mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 19:42:15 +02:00
feat(log): format log entry like native binary
This commit is contained in:
parent
2621f83ccb
commit
db079b4292
6 changed files with 157 additions and 27 deletions
|
@ -175,7 +175,7 @@ export const Grid = withDisplayName('Grid')(({
|
|||
if (scrollTop < bodyRef.scrollHeight - bodyRef.clientHeight && bodyRef.scrollTop < scrollTop) {
|
||||
setAutoScroll(false);
|
||||
}
|
||||
} else if (bodyRef.scrollTop + bodyRef.offsetHeight >= bodyRef.scrollHeight - 50) {
|
||||
} else if (bodyRef.scrollTop + bodyRef.offsetHeight >= bodyRef.scrollHeight - 10) {
|
||||
setAutoScroll(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { ICommandBarItemProps, Stack, StackItem } from "@fluentui/react";
|
||||
import { Checkbox, ICommandBarItemProps, Stack, StackItem } from "@fluentui/react";
|
||||
import { makeStyles, mergeClasses, shorthands } from "@griffel/react";
|
||||
import { AndroidLogEntry, AndroidLogPriority, Logcat } from '@yume-chan/android-bin';
|
||||
import { AndroidLogEntry, AndroidLogPriority, formatAndroidLogEntry, Logcat, LogcatFormat } from '@yume-chan/android-bin';
|
||||
import { BTree } from '@yume-chan/b-tree';
|
||||
import { AbortController, ReadableStream, WritableStream } from '@yume-chan/stream-extra';
|
||||
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
import { CommandBar, Grid, GridColumn, GridHeaderProps, GridRowProps } from "../components";
|
||||
import { GlobalState } from "../state";
|
||||
|
@ -55,6 +57,7 @@ const state = makeAutoObservable({
|
|||
buffer: [] as LogRow[],
|
||||
flushRequested: false,
|
||||
list: [] as LogRow[],
|
||||
selection: new BTree(6),
|
||||
count: 0,
|
||||
stream: undefined as ReadableStream<AndroidLogEntry> | undefined,
|
||||
stopSignal: undefined as AbortController | undefined,
|
||||
|
@ -98,6 +101,7 @@ const state = makeAutoObservable({
|
|||
},
|
||||
clear() {
|
||||
this.list = [];
|
||||
this.selection.clear();
|
||||
this.selectedCount = 0;
|
||||
},
|
||||
get empty() {
|
||||
|
@ -130,7 +134,16 @@ const state = makeAutoObservable({
|
|||
disabled: this.selectedCount === 0,
|
||||
iconProps: { iconName: Icons.Copy },
|
||||
onClick: () => {
|
||||
|
||||
let text = '';
|
||||
for (const index of this.selection) {
|
||||
text += formatAndroidLogEntry(
|
||||
this.list[index],
|
||||
LogcatFormat.Brief
|
||||
) + '\n';
|
||||
}
|
||||
// Chrome on Windows can't copy null characters
|
||||
text = text.replace(/\u0000/g, '');
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -139,13 +152,52 @@ const state = makeAutoObservable({
|
|||
disabled: this.selectedCount === 0,
|
||||
iconProps: { iconName: Icons.Copy },
|
||||
onClick: () => {
|
||||
|
||||
let text = '';
|
||||
for (const index of this.selection) {
|
||||
text += this.list[index].message + '\n';
|
||||
}
|
||||
// Chrome on Windows can't copy null characters
|
||||
text = text.replace(/\u0000/g, '');
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
get columns(): Column[] {
|
||||
return [
|
||||
{
|
||||
width: 40,
|
||||
title: '',
|
||||
CellComponent: ({ rowIndex, columnIndex, className, ...rest }) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
useEffect(() => {
|
||||
setChecked(this.selection.has(rowIndex));
|
||||
}, [rowIndex]);
|
||||
|
||||
const handleChange = useStableCallback((e?: FormEvent<EventTarget>, checked?: boolean) => {
|
||||
if (checked === undefined) {
|
||||
return;
|
||||
}
|
||||
if (checked) {
|
||||
this.selection.add(rowIndex);
|
||||
setChecked(true);
|
||||
} else {
|
||||
this.selection.delete(rowIndex);
|
||||
setChecked(false);
|
||||
}
|
||||
runInAction(() => {
|
||||
// Trigger mobx
|
||||
this.selectedCount = this.selection.size;
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack className={className} verticalAlign='center' horizontalAlign='center' {...rest}>
|
||||
<Checkbox checked={checked} onChange={handleChange} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
width: 200,
|
||||
title: 'Time',
|
||||
|
|
|
@ -50,6 +50,7 @@ export function register() {
|
|||
WindowConsole: <WindowConsoleRegular style={STYLE} />,
|
||||
|
||||
// Required by @fluentui/react
|
||||
Checkmark: <CheckmarkRegular style={STYLE} />,
|
||||
StatusCircleCheckmark: <CheckmarkRegular style={STYLE} />,
|
||||
ChevronUpSmall: <ChevronUpRegular style={STYLE} />,
|
||||
ChevronDownSmall: <ChevronDownRegular style={STYLE} />,
|
||||
|
|
|
@ -25,6 +25,7 @@ export enum AndroidLogPriority {
|
|||
Unknown,
|
||||
Default,
|
||||
Verbose,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
|
@ -32,6 +33,41 @@ export enum AndroidLogPriority {
|
|||
Silent,
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=140;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
|
||||
export const AndroidLogPriorityToCharacter: Record<AndroidLogPriority, string> = {
|
||||
[AndroidLogPriority.Unknown]: '?',
|
||||
[AndroidLogPriority.Default]: '?',
|
||||
[AndroidLogPriority.Verbose]: 'V',
|
||||
[AndroidLogPriority.Debug]: 'D',
|
||||
[AndroidLogPriority.Info]: 'I',
|
||||
[AndroidLogPriority.Warn]: 'W',
|
||||
[AndroidLogPriority.Error]: 'E',
|
||||
[AndroidLogPriority.Fatal]: 'F',
|
||||
[AndroidLogPriority.Silent]: 'S',
|
||||
};
|
||||
|
||||
export enum LogcatFormat {
|
||||
Brief,
|
||||
Process,
|
||||
Tag,
|
||||
Thread,
|
||||
Raw,
|
||||
Time,
|
||||
ThreadTime,
|
||||
Long
|
||||
}
|
||||
|
||||
export interface LogcatFormatModifiers {
|
||||
usec?: boolean;
|
||||
printable?: boolean;
|
||||
year?: boolean;
|
||||
zone?: boolean;
|
||||
epoch?: boolean;
|
||||
monotonic?: boolean;
|
||||
uid?: boolean;
|
||||
descriptive?: boolean;
|
||||
}
|
||||
|
||||
export interface LogcatOptions {
|
||||
pid?: number;
|
||||
ids?: LogId[];
|
||||
|
@ -65,6 +101,21 @@ export interface AndroidLogEntry extends LoggerEntry {
|
|||
message: string;
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=1415;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
|
||||
export function formatAndroidLogEntry(
|
||||
entry: AndroidLogEntry,
|
||||
format: LogcatFormat = LogcatFormat.Brief,
|
||||
modifier?: LogcatFormatModifiers
|
||||
) {
|
||||
const uid = modifier?.uid ? `${entry.uid.toString().padStart(5)}:` : '';
|
||||
|
||||
switch (format) {
|
||||
// TODO: implement other formats
|
||||
default:
|
||||
return `${AndroidLogPriorityToCharacter[entry.priority]}/${entry.tag.padEnd(8)}(${uid}${entry.pid.toString().padStart(5)}): ${entry.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function findTagEnd(payload: Uint8Array) {
|
||||
for (const separator of [0, ' '.charCodeAt(0), ':'.charCodeAt(0)]) {
|
||||
const index = payload.indexOf(separator);
|
||||
|
@ -200,6 +251,6 @@ export class Logcat extends AdbCommandBase {
|
|||
return stdout;
|
||||
}).pipeThrough(new BufferedTransformStream(stream => {
|
||||
return deserializeAndroidLogEntry(stream);
|
||||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,13 +55,13 @@ describe('BTree', () => {
|
|||
|
||||
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
|
||||
for (let value of values) {
|
||||
tree.insert(value);
|
||||
tree.add(value);
|
||||
validateTree(tree);
|
||||
expect(tree.has(value)).toBe(true);
|
||||
}
|
||||
|
||||
for (let value of values) {
|
||||
tree.remove(value);
|
||||
tree.delete(value);
|
||||
validateTree(tree);
|
||||
expect(tree.has(value)).toBe(false);
|
||||
}
|
||||
|
@ -73,14 +73,14 @@ describe('BTree', () => {
|
|||
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
|
||||
shuffle(values);
|
||||
for (const value of values) {
|
||||
tree.insert(value);
|
||||
tree.add(value);
|
||||
validateTree(tree);
|
||||
expect(tree.has(value)).toBe(true);
|
||||
}
|
||||
|
||||
shuffle(values);
|
||||
for (const value of values) {
|
||||
tree.remove(value);
|
||||
tree.delete(value);
|
||||
validateTree(tree);
|
||||
expect(tree.has(value)).toBe(false);
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ export class BTreeNode {
|
|||
return false;
|
||||
}
|
||||
|
||||
public insert(value: number): BTreeInsertionResult | boolean {
|
||||
public add(value: number): BTreeInsertionResult | boolean {
|
||||
let index = this.search(value);
|
||||
if (index >= 0) {
|
||||
return false;
|
||||
|
@ -155,7 +155,7 @@ export class BTreeNode {
|
|||
return true;
|
||||
}
|
||||
|
||||
const split = this.children[index]!.insert(value);
|
||||
const split = this.children[index]!.add(value);
|
||||
if (typeof split === 'object') {
|
||||
if (this.keyCount === this.order - 1) {
|
||||
return this.split(split.key, index, split.child);
|
||||
|
@ -170,20 +170,20 @@ export class BTreeNode {
|
|||
return true;
|
||||
}
|
||||
|
||||
public remove(value: number): boolean {
|
||||
public delete(value: number): boolean {
|
||||
let index = this.search(value);
|
||||
if (index >= 0) {
|
||||
this.removeAt(index);
|
||||
this.deleteAt(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.height > 0) {
|
||||
index = ~index;
|
||||
const removed = this.children[index]!.remove(value);
|
||||
if (removed) {
|
||||
const deleted = this.children[index]!.delete(value);
|
||||
if (deleted) {
|
||||
this.balance(index);
|
||||
}
|
||||
return removed;
|
||||
return deleted;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -265,18 +265,18 @@ export class BTreeNode {
|
|||
this.keyCount -= 1;
|
||||
}
|
||||
|
||||
protected removeMax(): void {
|
||||
protected deleteMax(): void {
|
||||
if (this.height === 0) {
|
||||
this.keyCount -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const child = this.children[this.keyCount]!;
|
||||
child.removeMax();
|
||||
child.deleteMax();
|
||||
this.balance(this.keyCount);
|
||||
}
|
||||
|
||||
protected removeAt(index: number) {
|
||||
protected deleteAt(index: number) {
|
||||
if (this.height === 0) {
|
||||
remove(this.keys, this.keyCount, index);
|
||||
this.keyCount -= 1;
|
||||
|
@ -285,9 +285,23 @@ export class BTreeNode {
|
|||
|
||||
const max = this.children[index]!.max();
|
||||
this.keys[index] = max;
|
||||
this.children[index]!.removeMax();
|
||||
this.children[index]!.deleteMax();
|
||||
this.balance(index);
|
||||
}
|
||||
|
||||
public *[Symbol.iterator](): Generator<number, void, void> {
|
||||
if (this.height > 0) {
|
||||
for (let i = 0; i < this.keyCount; i += 1) {
|
||||
yield* this.children[i]!;
|
||||
yield this.keys[i]!;
|
||||
}
|
||||
yield* this.children[this.keyCount]!;
|
||||
} else {
|
||||
for (let i = 0; i < this.keyCount; i += 1) {
|
||||
yield this.keys[i]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BTree {
|
||||
|
@ -323,8 +337,8 @@ export class BTree {
|
|||
}
|
||||
}
|
||||
|
||||
public insert(value: number) {
|
||||
const split = this.root.insert(value);
|
||||
public add(value: number) {
|
||||
const split = this.root.add(value);
|
||||
if (typeof split === 'object') {
|
||||
const keys = new Int32Array(this.order - 1);
|
||||
keys[0] = split.key;
|
||||
|
@ -347,14 +361,26 @@ export class BTree {
|
|||
return !!split;
|
||||
}
|
||||
|
||||
public remove(value: number) {
|
||||
const removed = this.root.remove(value);
|
||||
if (removed) {
|
||||
public delete(value: number) {
|
||||
const deleted = this.root.delete(value);
|
||||
if (deleted) {
|
||||
if (this.root.height > 0 && this.root.keyCount === 0) {
|
||||
this.root = this.root.children[0]!;
|
||||
}
|
||||
this.size -= 1;
|
||||
}
|
||||
return removed;
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.root.keyCount = 0;
|
||||
this.root.height = 0;
|
||||
// immediately release all references
|
||||
this.root.children = new Array(this.order);
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
public [Symbol.iterator]() {
|
||||
return this.root[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue