mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 10:49:24 +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) {
|
if (scrollTop < bodyRef.scrollHeight - bodyRef.clientHeight && bodyRef.scrollTop < scrollTop) {
|
||||||
setAutoScroll(false);
|
setAutoScroll(false);
|
||||||
}
|
}
|
||||||
} else if (bodyRef.scrollTop + bodyRef.offsetHeight >= bodyRef.scrollHeight - 50) {
|
} else if (bodyRef.scrollTop + bodyRef.offsetHeight >= bodyRef.scrollHeight - 10) {
|
||||||
setAutoScroll(true);
|
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 { 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 { AbortController, ReadableStream, WritableStream } from '@yume-chan/stream-extra';
|
||||||
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { FormEvent, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { CommandBar, Grid, GridColumn, GridHeaderProps, GridRowProps } from "../components";
|
import { CommandBar, Grid, GridColumn, GridHeaderProps, GridRowProps } from "../components";
|
||||||
import { GlobalState } from "../state";
|
import { GlobalState } from "../state";
|
||||||
|
@ -55,6 +57,7 @@ const state = makeAutoObservable({
|
||||||
buffer: [] as LogRow[],
|
buffer: [] as LogRow[],
|
||||||
flushRequested: false,
|
flushRequested: false,
|
||||||
list: [] as LogRow[],
|
list: [] as LogRow[],
|
||||||
|
selection: new BTree(6),
|
||||||
count: 0,
|
count: 0,
|
||||||
stream: undefined as ReadableStream<AndroidLogEntry> | undefined,
|
stream: undefined as ReadableStream<AndroidLogEntry> | undefined,
|
||||||
stopSignal: undefined as AbortController | undefined,
|
stopSignal: undefined as AbortController | undefined,
|
||||||
|
@ -98,6 +101,7 @@ const state = makeAutoObservable({
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
this.list = [];
|
this.list = [];
|
||||||
|
this.selection.clear();
|
||||||
this.selectedCount = 0;
|
this.selectedCount = 0;
|
||||||
},
|
},
|
||||||
get empty() {
|
get empty() {
|
||||||
|
@ -130,7 +134,16 @@ const state = makeAutoObservable({
|
||||||
disabled: this.selectedCount === 0,
|
disabled: this.selectedCount === 0,
|
||||||
iconProps: { iconName: Icons.Copy },
|
iconProps: { iconName: Icons.Copy },
|
||||||
onClick: () => {
|
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,
|
disabled: this.selectedCount === 0,
|
||||||
iconProps: { iconName: Icons.Copy },
|
iconProps: { iconName: Icons.Copy },
|
||||||
onClick: () => {
|
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[] {
|
get columns(): Column[] {
|
||||||
return [
|
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,
|
width: 200,
|
||||||
title: 'Time',
|
title: 'Time',
|
||||||
|
|
|
@ -50,6 +50,7 @@ export function register() {
|
||||||
WindowConsole: <WindowConsoleRegular style={STYLE} />,
|
WindowConsole: <WindowConsoleRegular style={STYLE} />,
|
||||||
|
|
||||||
// Required by @fluentui/react
|
// Required by @fluentui/react
|
||||||
|
Checkmark: <CheckmarkRegular style={STYLE} />,
|
||||||
StatusCircleCheckmark: <CheckmarkRegular style={STYLE} />,
|
StatusCircleCheckmark: <CheckmarkRegular style={STYLE} />,
|
||||||
ChevronUpSmall: <ChevronUpRegular style={STYLE} />,
|
ChevronUpSmall: <ChevronUpRegular style={STYLE} />,
|
||||||
ChevronDownSmall: <ChevronDownRegular style={STYLE} />,
|
ChevronDownSmall: <ChevronDownRegular style={STYLE} />,
|
||||||
|
|
|
@ -25,6 +25,7 @@ export enum AndroidLogPriority {
|
||||||
Unknown,
|
Unknown,
|
||||||
Default,
|
Default,
|
||||||
Verbose,
|
Verbose,
|
||||||
|
Debug,
|
||||||
Info,
|
Info,
|
||||||
Warn,
|
Warn,
|
||||||
Error,
|
Error,
|
||||||
|
@ -32,6 +33,41 @@ export enum AndroidLogPriority {
|
||||||
Silent,
|
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 {
|
export interface LogcatOptions {
|
||||||
pid?: number;
|
pid?: number;
|
||||||
ids?: LogId[];
|
ids?: LogId[];
|
||||||
|
@ -65,6 +101,21 @@ export interface AndroidLogEntry extends LoggerEntry {
|
||||||
message: string;
|
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) {
|
function findTagEnd(payload: Uint8Array) {
|
||||||
for (const separator of [0, ' '.charCodeAt(0), ':'.charCodeAt(0)]) {
|
for (const separator of [0, ' '.charCodeAt(0), ':'.charCodeAt(0)]) {
|
||||||
const index = payload.indexOf(separator);
|
const index = payload.indexOf(separator);
|
||||||
|
@ -200,6 +251,6 @@ export class Logcat extends AdbCommandBase {
|
||||||
return stdout;
|
return stdout;
|
||||||
}).pipeThrough(new BufferedTransformStream(stream => {
|
}).pipeThrough(new BufferedTransformStream(stream => {
|
||||||
return deserializeAndroidLogEntry(stream);
|
return deserializeAndroidLogEntry(stream);
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,13 +55,13 @@ describe('BTree', () => {
|
||||||
|
|
||||||
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
|
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
|
||||||
for (let value of values) {
|
for (let value of values) {
|
||||||
tree.insert(value);
|
tree.add(value);
|
||||||
validateTree(tree);
|
validateTree(tree);
|
||||||
expect(tree.has(value)).toBe(true);
|
expect(tree.has(value)).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let value of values) {
|
for (let value of values) {
|
||||||
tree.remove(value);
|
tree.delete(value);
|
||||||
validateTree(tree);
|
validateTree(tree);
|
||||||
expect(tree.has(value)).toBe(false);
|
expect(tree.has(value)).toBe(false);
|
||||||
}
|
}
|
||||||
|
@ -73,14 +73,14 @@ describe('BTree', () => {
|
||||||
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
|
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
|
||||||
shuffle(values);
|
shuffle(values);
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
tree.insert(value);
|
tree.add(value);
|
||||||
validateTree(tree);
|
validateTree(tree);
|
||||||
expect(tree.has(value)).toBe(true);
|
expect(tree.has(value)).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
shuffle(values);
|
shuffle(values);
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
tree.remove(value);
|
tree.delete(value);
|
||||||
validateTree(tree);
|
validateTree(tree);
|
||||||
expect(tree.has(value)).toBe(false);
|
expect(tree.has(value)).toBe(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ export class BTreeNode {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public insert(value: number): BTreeInsertionResult | boolean {
|
public add(value: number): BTreeInsertionResult | boolean {
|
||||||
let index = this.search(value);
|
let index = this.search(value);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -155,7 +155,7 @@ export class BTreeNode {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const split = this.children[index]!.insert(value);
|
const split = this.children[index]!.add(value);
|
||||||
if (typeof split === 'object') {
|
if (typeof split === 'object') {
|
||||||
if (this.keyCount === this.order - 1) {
|
if (this.keyCount === this.order - 1) {
|
||||||
return this.split(split.key, index, split.child);
|
return this.split(split.key, index, split.child);
|
||||||
|
@ -170,20 +170,20 @@ export class BTreeNode {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove(value: number): boolean {
|
public delete(value: number): boolean {
|
||||||
let index = this.search(value);
|
let index = this.search(value);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.removeAt(index);
|
this.deleteAt(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.height > 0) {
|
if (this.height > 0) {
|
||||||
index = ~index;
|
index = ~index;
|
||||||
const removed = this.children[index]!.remove(value);
|
const deleted = this.children[index]!.delete(value);
|
||||||
if (removed) {
|
if (deleted) {
|
||||||
this.balance(index);
|
this.balance(index);
|
||||||
}
|
}
|
||||||
return removed;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -265,18 +265,18 @@ export class BTreeNode {
|
||||||
this.keyCount -= 1;
|
this.keyCount -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected removeMax(): void {
|
protected deleteMax(): void {
|
||||||
if (this.height === 0) {
|
if (this.height === 0) {
|
||||||
this.keyCount -= 1;
|
this.keyCount -= 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = this.children[this.keyCount]!;
|
const child = this.children[this.keyCount]!;
|
||||||
child.removeMax();
|
child.deleteMax();
|
||||||
this.balance(this.keyCount);
|
this.balance(this.keyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected removeAt(index: number) {
|
protected deleteAt(index: number) {
|
||||||
if (this.height === 0) {
|
if (this.height === 0) {
|
||||||
remove(this.keys, this.keyCount, index);
|
remove(this.keys, this.keyCount, index);
|
||||||
this.keyCount -= 1;
|
this.keyCount -= 1;
|
||||||
|
@ -285,9 +285,23 @@ export class BTreeNode {
|
||||||
|
|
||||||
const max = this.children[index]!.max();
|
const max = this.children[index]!.max();
|
||||||
this.keys[index] = max;
|
this.keys[index] = max;
|
||||||
this.children[index]!.removeMax();
|
this.children[index]!.deleteMax();
|
||||||
this.balance(index);
|
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 {
|
export class BTree {
|
||||||
|
@ -323,8 +337,8 @@ export class BTree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public insert(value: number) {
|
public add(value: number) {
|
||||||
const split = this.root.insert(value);
|
const split = this.root.add(value);
|
||||||
if (typeof split === 'object') {
|
if (typeof split === 'object') {
|
||||||
const keys = new Int32Array(this.order - 1);
|
const keys = new Int32Array(this.order - 1);
|
||||||
keys[0] = split.key;
|
keys[0] = split.key;
|
||||||
|
@ -347,14 +361,26 @@ export class BTree {
|
||||||
return !!split;
|
return !!split;
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove(value: number) {
|
public delete(value: number) {
|
||||||
const removed = this.root.remove(value);
|
const deleted = this.root.delete(value);
|
||||||
if (removed) {
|
if (deleted) {
|
||||||
if (this.root.height > 0 && this.root.keyCount === 0) {
|
if (this.root.height > 0 && this.root.keyCount === 0) {
|
||||||
this.root = this.root.children[0]!;
|
this.root = this.root.children[0]!;
|
||||||
}
|
}
|
||||||
this.size -= 1;
|
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