diff options
-rw-r--r-- | src/background/background.ts | 2 | ||||
-rw-r--r-- | src/logging.ts | 186 | ||||
-rw-r--r-- | src/pages/logs.html | 36 | ||||
-rw-r--r-- | src/pages/logs.tsx | 76 | ||||
-rw-r--r-- | src/popup/popup.tsx | 3 | ||||
-rw-r--r-- | src/query.ts | 41 | ||||
-rw-r--r-- | src/wxBackend.ts | 6 | ||||
-rw-r--r-- | tsconfig.json | 8 |
8 files changed, 352 insertions, 6 deletions
diff --git a/src/background/background.ts b/src/background/background.ts index c11ff0bd9..8a3f2477d 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -33,7 +33,7 @@ window.addEventListener("load", () => { System.import("../wxBackend") .then((wxMessaging: any) => { // Export as global for debugger - (window as any).wxMessaging = wxMessaging; + (window as any).wx = wxMessaging; wxMessaging.wxMain(); }).catch((e: Error) => { console.error("Loading Taler wallet background page failed.", e); diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 000000000..3f8757a07 --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,186 @@ +/* + This file is part of TALER + (C) 2016 Inria + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Configurable logging. + * + * @author Florian Dold + */ + +import {Store, QueryRoot} from "./query"; + +export type Level = "error" | "debug" | "info" | "warn"; + +function makeInfo() { + return console.info.bind(console, "%o"); +} + +function makeWarn() { + return console.warn.bind(console, "%o"); +} + +function makeError() { + return console.error.bind(console, "%o"); +} + +function makeDebug() { + return console.log.bind(console, "%o"); +} + +export async function log(msg: string, level: Level = "info"): Promise<void> { + let ci = getCallInfo(2); + return record(level, msg, ci.file, ci.line, ci.column); +} + +function getCallInfo(level: number) { + // see https://github.com/v8/v8/wiki/Stack-Trace-API + let stack = Error().stack; + if (!stack) { + return unknownFrame; + } + let lines = stack.split("\n"); + return parseStackLine(lines[level + 1]); +} + +interface Frame { + file?: string; + method?: string; + column?: number; + line?: number; +} + +const unknownFrame: Frame = { + file: "(unknown)", + method: "(unknown)", + line: 0, + column: 0 +}; + +/** + * Adapted from https://github.com/errwischt/stacktrace-parser. + */ +function parseStackLine(stackLine: string): Frame { + const chrome = /^\s*at (?:(?:(?:Anonymous function)?|((?:\[object object\])?\S+(?: \[as \S+\])?)) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i; + const gecko = /^(?:\s*([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i; + const node = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i; + let parts; + + if ((parts = gecko.exec(stackLine))) { + let f: Frame = { + file: parts[3], + method: parts[1] || "(unknown)", + line: +parts[4], + column: parts[5] ? +parts[5] : undefined, + }; + return f; + } else if ((parts = chrome.exec(stackLine))) { + let f: Frame = { + file: parts[2], + method: parts[1] || "(unknown)", + line: +parts[3], + column: parts[4] ? +parts[4] : undefined, + }; + return f; + } else if ((parts = node.exec(stackLine))) { + let f: Frame = { + file: parts[2], + method: parts[1] || "(unknown)", + line: +parts[3], + column: parts[4] ? +parts[4] : undefined, + }; + return f; + } + return unknownFrame; +} + + +let db: IDBDatabase|undefined = undefined; + +export interface LogEntry { + timestamp: number; + level: string; + msg: string; + source: string|undefined; + col: number|undefined; + line: number|undefined; + id?: number; +} + +export async function getLogs(): Promise<LogEntry[]> { + if (!db) { + db = await openLoggingDb(); + } + return await new QueryRoot(db).iter(logsStore).toArray(); +} + +export async function record(level: Level, msg: string, source?: string, line?: number, col?: number): Promise<void> { + if (typeof indexedDB === "undefined") { + return; + } + if (!db) { + db = await openLoggingDb(); + } + + let count = await new QueryRoot(db).count(logsStore); + + console.log("count is", count); + + if (count > 1000) { + await new QueryRoot(db).deleteIf(logsStore, (e, i) => (i < 200)); + } + + let entry: LogEntry = { + timestamp: new Date().getTime(), + level, + msg, + source, + line, + col, + }; + await new QueryRoot(db).put(logsStore, entry); +} + +const loggingDbVersion = 1; + +const logsStore: Store<LogEntry> = new Store<LogEntry>("logs"); + +export function openLoggingDb(): Promise<IDBDatabase> { + return new Promise((resolve, reject) => { + const req = indexedDB.open("taler-logging", loggingDbVersion); + req.onerror = (e) => { + reject(e); + }; + req.onsuccess = (e) => { + resolve(req.result); + }; + req.onupgradeneeded = (e) => { + const db = req.result; + if (e.oldVersion != 0) { + try { + db.deleteObjectStore("logs"); + } catch (e) { + console.error(e); + } + } + db.createObjectStore("logs", {keyPath: "id", autoIncrement: true}); + }; + }); +} + +export const info = makeInfo(); +export const debug = makeDebug(); +export const warn = makeWarn(); +export const error = makeError(); diff --git a/src/pages/logs.html b/src/pages/logs.html new file mode 100644 index 000000000..8d35bcbd7 --- /dev/null +++ b/src/pages/logs.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Taler Wallet: Logs</title> + + <link rel="stylesheet" type="text/css" href="../style/lang.css"> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + + <link rel="icon" href="/img/icon.png"> + + <script src="/src/vendor/URI.js"></script> + <script src="/src/vendor/react.js"></script> + <script src="/src/vendor/react-dom.js"></script> + + <!-- i18n --> + <script src="/src/vendor/jed.js"></script> + <script src="/src/i18n.js"></script> + <script src="/src/i18n/strings.js"></script> + + <script src="/src/vendor/system-csp-production.src.js"></script> + <script src="/src/module-trampoline.js"></script> + + <style> + .tree-item { + margin: 2em; + border-radius: 5px; + border: 1px solid gray; + padding: 1em; + } + </style> + + <body> + <div id="container"></div> + </body> +</html> diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx new file mode 100644 index 000000000..f971f3f85 --- /dev/null +++ b/src/pages/logs.tsx @@ -0,0 +1,76 @@ +/* + This file is part of TALER + (C) 2016 Inria + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Show wallet logs. + * + * @author Florian Dold + */ + +import {LogEntry, getLogs} from "src/logging"; + +interface LogViewProps { + log: LogEntry; +} + +class LogView extends React.Component<LogViewProps, void> { + render(): JSX.Element { + let e = this.props.log; + return ( + <div className="tree-item"> + <ul> + <li>id: {e.id || "unknown"}</li> + <li>msg: {e.msg}</li> + <li>file: {e.source || "(unknown)"}</li> + </ul> + </div> + ); + } +} + +interface LogsState { + logs: LogEntry[]|undefined; +} + +class Logs extends React.Component<any, LogsState> { + constructor() { + super(); + this.update(); + this.state = {} as any; + } + + async update() { + let logs = await getLogs(); + this.setState({logs}); + } + + render(): JSX.Element { + let logs = this.state.logs; + if (!logs) { + return <span>...</span>; + } + return ( + <div className="tree-item"> + Logs: + {logs.map(e => <LogView log={e} />)} + </div> + ); + } +} + +export function main() { + ReactDOM.render(<Logs />, document.getElementById("container")!); +} diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 711650bc3..e9fa6a87b 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -492,6 +492,9 @@ function WalletDebug(props: any) { <button onClick={openExtensionPage("/src/pages/tree.html")}> show tree </button> + <button onClick={openExtensionPage("/src/pages/logs.html")}> + show logs + </button> <br /> <button onClick={confirmReset}> reset diff --git a/src/query.ts b/src/query.ts index f3ce0d764..f8a6255b4 100644 --- a/src/query.ts +++ b/src/query.ts @@ -38,9 +38,9 @@ export interface JoinLeftResult<L,R> { export class Store<T> { name: string; validator?: (v: T) => T; - storeParams: IDBObjectStoreParameters; + storeParams?: IDBObjectStoreParameters; - constructor(name: string, storeParams: IDBObjectStoreParameters, + constructor(name: string, storeParams?: IDBObjectStoreParameters, validator?: (v: T) => T) { this.name = name; this.validator = validator; @@ -450,6 +450,43 @@ export class QueryRoot implements PromiseLike<void> { return new IterQueryStream(this, store.name, {}); } + count<T>(store: Store<T>): Promise<number> { + const {resolve, promise} = openPromise(); + + const doCount = (tx: IDBTransaction) => { + const s = tx.objectStore(store.name); + const req = s.count(); + req.onsuccess = () => { + resolve(req.result); + }; + } + + this.addWork(doCount, store.name, false); + return Promise.resolve() + .then(() => this.finish()) + .then(() => promise); + + } + + deleteIf<T>(store: Store<T>, predicate: (x: T, n: number) => boolean): QueryRoot { + const doDeleteIf = (tx: IDBTransaction) => { + const s = tx.objectStore(store.name); + const req = s.openCursor(); + let n = 0; + req.onsuccess = () => { + let cursor: IDBCursorWithValue = req.result; + if (cursor) { + if (predicate(cursor.value, n++)) { + cursor.delete(); + } + cursor.continue(); + } + } + }; + this.addWork(doDeleteIf, store.name, true); + return this; + } + iterIndex<S extends IDBValidKey,T>(index: Index<S,T>, only?: S): QueryStream<T> { this.stores.add(index.storeName); diff --git a/src/wxBackend.ts b/src/wxBackend.ts index c0a31cb63..6667fe47b 100644 --- a/src/wxBackend.ts +++ b/src/wxBackend.ts @@ -30,6 +30,7 @@ import { Notifier } from "./types"; import { Contract } from "./types"; import MessageSender = chrome.runtime.MessageSender; import { ChromeBadge } from "./chromeBadge"; +import * as logging from "./logging"; "use strict"; @@ -413,6 +414,7 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea // Useful for debugging ... export let wallet: Wallet | undefined = undefined; export let badge: ChromeBadge | undefined = undefined; +export let log = logging.log; // Rate limit cache for executePayment operations, to break redirect loops let rateLimitCache: { [n: number]: number } = {}; @@ -422,6 +424,10 @@ function clearRateLimitCache() { } export function wxMain() { + window.onerror = (m, source, lineno, colno, error) => { + logging.record("error", m + error, source || "(unknown)", lineno || 0, colno || 0); + } + chrome.browserAction.setBadgeText({ text: "" }); badge = new ChromeBadge(); diff --git a/tsconfig.json b/tsconfig.json index e79ad03ce..13ad926c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,14 +16,14 @@ "src/checkable.ts", "decl/lib.es6.d.ts", "src/chromeBadge.ts", - "decl/urijs/URIjs.d.ts", "src/cryptoApi-test.ts", + "decl/urijs/URIjs.d.ts", "src/components.ts", - "decl/systemjs/systemjs.d.ts", "src/emscriptif-test.ts", + "decl/systemjs/systemjs.d.ts", "src/cryptoApi.ts", - "decl/react-global.d.ts", "src/helpers-test.ts", + "decl/react-global.d.ts", "src/cryptoLib.ts", "src/types-test.ts", "decl/chrome/chrome.d.ts", @@ -32,6 +32,7 @@ "src/emscriptif.ts", "src/helpers.ts", "src/http.ts", + "src/logging.ts", "src/query.ts", "src/taler-wallet-lib.ts", "src/types.ts", @@ -46,6 +47,7 @@ "src/pages/show-db.ts", "src/pages/confirm-contract.tsx", "src/pages/confirm-create-reserve.tsx", + "src/pages/logs.tsx", "src/pages/tree.tsx", "src/popup/popup.tsx" ] |