summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-11-18 04:09:04 +0100
committerFlorian Dold <florian.dold@gmail.com>2016-11-18 04:09:04 +0100
commit356ebd2137eb5ca31486ed49ab8223c8256b1554 (patch)
treef3aa2214b928119b3d6e5ba22b8fa348fa929c25 /src
parent2986afb3d4e5f750f34e68cd46e394e2da392a4a (diff)
downloadwallet-core-356ebd2137eb5ca31486ed49ab8223c8256b1554.tar.gz
wallet-core-356ebd2137eb5ca31486ed49ab8223c8256b1554.tar.bz2
wallet-core-356ebd2137eb5ca31486ed49ab8223c8256b1554.zip
persistent logging
Diffstat (limited to 'src')
-rw-r--r--src/background/background.ts2
-rw-r--r--src/logging.ts186
-rw-r--r--src/pages/logs.html36
-rw-r--r--src/pages/logs.tsx76
-rw-r--r--src/popup/popup.tsx3
-rw-r--r--src/query.ts41
-rw-r--r--src/wxBackend.ts6
7 files changed, 347 insertions, 3 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();