Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.

Commit a7f11a6

Browse files
authored
Merge pull request #1270 from lightninglabs/dev/wbobeirne-export-logs
Dev/wbobeirne export logs
2 parents 8a87aee + a565f8b commit a7f11a6

File tree

11 files changed

+227
-39
lines changed

11 files changed

+227
-39
lines changed

src/action/file-mobile.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @fileOverview actions wrapping file I/O operations on mobile.
3+
*/
4+
5+
import * as log from './log';
6+
7+
class FileAction {
8+
constructor(store, FS, Share) {
9+
this._store = store;
10+
this._FS = FS;
11+
this._Share = Share;
12+
}
13+
14+
/**
15+
* Gets the path of the lnd directory where `logs` and `data` are stored.
16+
* @return {string}
17+
*/
18+
getLndDir() {
19+
return this._FS.DocumentDirectoryPath;
20+
}
21+
22+
/**
23+
* Retrieves the entire lnd log file contents as a string.
24+
* @return {Promise<string>}
25+
*/
26+
async readLogs() {
27+
const { network } = this._store;
28+
const path = `${this.getLndDir()}/logs/bitcoin/${network}/lnd.log`;
29+
return this._FS.readFile(path, 'utf8');
30+
}
31+
32+
/**
33+
* Shares the log file using whatever native share function we have.
34+
* @return {Promise}
35+
*/
36+
async shareLogs() {
37+
try {
38+
const logs = await this.readLogs();
39+
await this._Share.share({
40+
title: 'Lightning App logs',
41+
message: logs,
42+
});
43+
} catch (err) {
44+
log.error('Exporting logs failed', err);
45+
}
46+
}
47+
48+
/**
49+
* Delete the wallet.db file. This allows the user to restore their wallet
50+
* (including channel state) from the seed if they've forgotten the pin.
51+
* @return {Promise<undefined>}
52+
*/
53+
async deleteWalletDB(network) {
54+
const path = `${this.getLndDir()}/data/chain/bitcoin/${network}/wallet.db`;
55+
try {
56+
await this._FS.unlink(path);
57+
} catch (err) {
58+
log.info(`No ${network} wallet to delete.`);
59+
}
60+
}
61+
}
62+
63+
export default FileAction;

src/action/index-mobile.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
NativeModules,
1515
ActionSheetIOS,
1616
NativeEventEmitter,
17+
Share,
1718
} from 'react-native';
1819
import * as Random from 'expo-random';
1920
import * as LocalAuthentication from 'expo-local-authentication';
@@ -28,6 +29,7 @@ import GrpcAction from './grpc-mobile';
2829
import NavAction from './nav-mobile';
2930
import WalletAction from './wallet';
3031
import LogAction from './log';
32+
import FileAction from './file-mobile';
3133
import InfoAction from './info';
3234
import NotificationAction from './notification';
3335
import ChannelAction from './channel';
@@ -47,10 +49,11 @@ store.init(); // initialize computed values
4749
export const db = new AppStorage(store, AsyncStorage);
4850
export const grpc = new GrpcAction(store, NativeModules, NativeEventEmitter);
4951
export const ipc = new IpcAction(grpc);
52+
export const file = new FileAction(store, RNFS, Share);
5053
export const log = new LogAction(store, ipc, false);
5154
export const nav = new NavAction(store, NavigationActions, StackActions);
5255
export const notify = new NotificationAction(store, nav);
53-
export const wallet = new WalletAction(store, grpc, db, nav, notify, RNFS);
56+
export const wallet = new WalletAction(store, grpc, db, nav, notify, file);
5457
export const info = new InfoAction(store, grpc, nav, notify);
5558
export const transaction = new TransactionAction(store, grpc, nav, notify);
5659
export const channel = new ChannelAction(store, grpc, nav, notify);

src/action/wallet.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import { when } from 'mobx';
1414
import * as log from './log';
1515

1616
class WalletAction {
17-
constructor(store, grpc, db, nav, notification, FS) {
17+
constructor(store, grpc, db, nav, notification, file) {
1818
this._store = store;
1919
this._grpc = grpc;
2020
this._db = db;
2121
this._nav = nav;
2222
this._notification = notification;
23-
this._FS = FS;
23+
this._file = file;
2424
}
2525

2626
//
@@ -266,7 +266,7 @@ class WalletAction {
266266
*/
267267
async initWallet({ walletPassword, seedMnemonic, recoveryWindow = 0 }) {
268268
try {
269-
await Promise.all([this.deleteDB('testnet'), this.deleteDB('mainnet')]);
269+
await this.deleteDB();
270270
await this._grpc.sendUnlockerCommand('InitWallet', {
271271
walletPassword: toBuffer(walletPassword),
272272
cipherSeedMnemonic: seedMnemonic,
@@ -293,20 +293,19 @@ class WalletAction {
293293
/**
294294
* Delete the wallet.db file. This allows the user to restore their wallet
295295
* (including channel state) from the seed if they've they've forgotten the
296-
* wallet pin/password.
296+
* wallet pin/password. We need to delete both mainnet and testnet wallet
297+
* files since we haven't set `store.network` at this point in the app life
298+
* cycle yet (which happens later when we query getInfo).
297299
* @return {Promise<undefined>}
298300
*/
299-
async deleteDB(network) {
300-
if (!this._FS) {
301+
async deleteDB() {
302+
if (!this._file) {
301303
return;
302304
}
303-
const lndDir = this._FS.DocumentDirectoryPath;
304-
const dbPath = `${lndDir}/data/chain/bitcoin/${network}/wallet.db`;
305-
try {
306-
await this._FS.unlink(dbPath);
307-
} catch (err) {
308-
log.info(`No ${network} wallet to delete.`);
309-
}
305+
await Promise.all([
306+
this._file.deleteWalletDB('testnet'),
307+
this._file.deleteWalletDB('mainnet'),
308+
]);
310309
}
311310

312311
/**

src/asset/icon/share.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import Svg, { G, Path } from '../../component/svg';
3+
const Share = props => (
4+
<Svg viewBox="0 0 44 47" width="1em" height="1em" {...props}>
5+
<G fill="#FFFFFF" fillRule="nonzero">
6+
<Path
7+
d="M20.2823393,45.8089582 L8.6208326,33.9983559 C6.7386017,32.112201 9.561948,29.2839686 11.4431809,31.1701236 L19.3034582,39.0535264 C19.6891903,39.4403934 20.0040283,39.3079355 20.0040283,38.7550254 L20.0040283,17.0049295 C20.0040283,15.8957094 20.8976693,14.9999992 22.0000313,14.9999992 C23.1100789,14.9999992 23.9960344,15.8976371 23.9960344,17.0049295 L23.9960344,38.7550254 C23.9960344,39.3053352 24.3096903,39.4415789 24.6966045,39.0535264 L32.5568818,31.1701236 C34.4391127,29.2839686 37.261461,32.112201 35.3792301,33.9983559 L23.7942534,45.7949866 C22.8210143,46.7860065 21.2509472,46.7899499 20.2823393,45.8089582 Z M4,4.99702929 L4,11.9999992 C4,13.1045687 3.1045695,13.9999992 2,13.9999992 C0.8954305,13.9999992 -7.10542736e-15,13.1045687 -7.10542736e-15,11.9999992 L-7.10542736e-15,2.99539679 C-7.10542736e-15,1.33499009 1.3506856,-8.07948688e-07 3.0087752,-8.07948688e-07 L40.9912248,-8.07948688e-07 C42.6465785,-8.07948688e-07 44,1.34474899 44,2.99539679 L44,11.9999992 C44,13.1045687 43.1045695,13.9999992 42,13.9999992 C40.8954305,13.9999992 40,13.1045687 40,11.9999992 L40,4.99702929 C40,4.45303549 39.552097,3.99999919 38.9995806,3.99999919 L5.0004194,3.99999919 C4.455173,3.99999919 4,4.44638479 4,4.99702929 Z"
8+
transform="translate(22.000000, 23.270746) rotate(180.000000) translate(-22.000000, -23.270746)"
9+
/>
10+
</G>
11+
</Svg>
12+
);
13+
14+
export default Share;

src/asset/icon/share.svg

Lines changed: 6 additions & 0 deletions
Loading

src/component/button.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import BackIcon from '../asset/icon/back';
1313
import CancelIcon from '../asset/icon/cancel';
1414
import PlusIcon from '../asset/icon/plus';
1515
import QrIcon from '../asset/icon/qr';
16+
import ShareIcon from '../asset/icon/share';
1617
import { color, font } from './style';
1718

1819
//
@@ -558,4 +559,20 @@ MaxButton.propTypes = {
558559
style: ViewPropTypes.style,
559560
};
560561

562+
//
563+
// Share Button
564+
//
565+
566+
export const ShareButton = ({ onPress, disabled, style }) => (
567+
<Button onPress={onPress} disabled={disabled} style={style}>
568+
<ShareIcon height={16} width={14.979} />
569+
</Button>
570+
);
571+
572+
ShareButton.propTypes = {
573+
onPress: PropTypes.func,
574+
disabled: PropTypes.bool,
575+
style: ViewPropTypes.style,
576+
};
577+
561578
export default Button;

src/view/cli.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React, { Component } from 'react';
2-
import { ScrollView, StyleSheet } from 'react-native';
2+
import { ScrollView, StyleSheet, Platform } from 'react-native';
33
import { observer } from 'mobx-react';
44
import PropTypes from 'prop-types';
55
import { SplitBackground } from '../component/background';
66
import { Header, Title } from '../component/header';
77
import Text from '../component/text';
8-
import { Button, BackButton } from '../component/button';
8+
import { BackButton, ShareButton, Button } from '../component/button';
99
import { createStyles, maxWidth } from '../component/media-query';
1010
import { color, font, breakWidth } from '../component/style';
1111

@@ -19,12 +19,16 @@ const styles = StyleSheet.create({
1919
},
2020
});
2121

22-
const CLIView = ({ store, nav }) => (
22+
const CLIView = ({ store, nav, file }) => (
2323
<SplitBackground color={color.blackDark} bottom={color.cliBackground}>
2424
<Header separator style={styles.header}>
2525
<BackButton onPress={() => nav.goSettings()} />
2626
<Title title="Logs" />
27-
<Button disabled onPress={() => {}} />
27+
{Platform.OS === 'web' ? (
28+
<Button disabled onPress={() => {}} />
29+
) : (
30+
<ShareButton onPress={() => file.shareLogs()} />
31+
)}
2832
</Header>
2933
<LogOutput logs={store.logs} />
3034
</SplitBackground>
@@ -33,6 +37,7 @@ const CLIView = ({ store, nav }) => (
3337
CLIView.propTypes = {
3438
store: PropTypes.object.isRequired,
3539
nav: PropTypes.object.isRequired,
40+
file: PropTypes.object,
3641
};
3742

3843
//

src/view/main-mobile.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
info,
6262
auth,
6363
autopilot,
64+
file,
6465
} from '../action/index-mobile';
6566

6667
import store from '../store';
@@ -137,7 +138,7 @@ const SettingsFiat = () => (
137138
<SettingFiatView store={store} nav={nav} setting={setting} />
138139
);
139140

140-
const CLI = () => <CLIView store={store} nav={nav} />;
141+
const CLI = () => <CLIView store={store} nav={nav} file={file} />;
141142

142143
const Notifications = () => <NotificationView store={store} nav={nav} />;
143144

stories/screen-story.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PaymentAction from '../src/action/payment';
1717
import ChannelAction from '../src/action/channel';
1818
import TransactionAction from '../src/action/transaction';
1919
import AuthAction from '../src/action/auth-mobile';
20+
import FileAction from '../src/action/file-mobile';
2021
import AtplAction from '../src/action/autopilot';
2122
import Welcome from '../src/view/welcome';
2223
import Transaction from '../src/view/transaction';
@@ -90,6 +91,7 @@ import RestoreSeedMobile from '../src/view/restore-seed-mobile';
9091

9192
const store = new Store();
9293
store.init();
94+
const file = sinon.createStubInstance(FileAction);
9395
const nav = sinon.createStubInstance(NavAction);
9496
const db = sinon.createStubInstance(AppStorage);
9597
const ipc = sinon.createStubInstance(IpcAction);
@@ -218,7 +220,7 @@ storiesOf('Screens', module)
218220
.add('Notifications (Mobile)', () => (
219221
<NotificationMobile store={store} nav={nav} />
220222
))
221-
.add('CLI', () => <CLI store={store} nav={nav} />)
223+
.add('CLI', () => <CLI store={store} nav={nav} file={file} />)
222224
.add('Transactions', () => (
223225
<Transaction store={store} transaction={transaction} nav={nav} />
224226
))
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Store } from '../../../src/store';
2+
import FileAction from '../../../src/action/file-mobile';
3+
import * as logger from '../../../src/action/log';
4+
5+
describe('Action File Mobile Unit Tests', () => {
6+
let store;
7+
let sandbox;
8+
let RNFS;
9+
let Share;
10+
let file;
11+
12+
beforeEach(() => {
13+
sandbox = sinon.createSandbox({});
14+
sandbox.stub(logger);
15+
store = new Store();
16+
RNFS = {
17+
DocumentDirectoryPath: '/foo/bar',
18+
readFile: sinon.stub().resolves('some-logs'),
19+
unlink: sinon.stub().resolves(),
20+
};
21+
Share = {
22+
share: sinon.stub().resolves(),
23+
};
24+
file = new FileAction(store, RNFS, Share);
25+
});
26+
27+
afterEach(() => {
28+
sandbox.restore();
29+
});
30+
31+
describe('getLndDir()', () => {
32+
it('should get lnd directory', () => {
33+
const path = file.getLndDir();
34+
expect(path, 'to equal', '/foo/bar');
35+
});
36+
});
37+
38+
describe('readLogs()', () => {
39+
it('should read log file contents', async () => {
40+
store.network = 'mainnet';
41+
const logs = await file.readLogs();
42+
expect(logs, 'to equal', 'some-logs');
43+
expect(
44+
RNFS.readFile,
45+
'was called with',
46+
'/foo/bar/logs/bitcoin/mainnet/lnd.log',
47+
'utf8'
48+
);
49+
});
50+
});
51+
52+
describe('shareLogs()', () => {
53+
beforeEach(() => {
54+
sandbox.stub(file, 'readLogs');
55+
});
56+
57+
it('should invoke the native share api', async () => {
58+
file.readLogs.resolves('some-logs');
59+
await file.shareLogs();
60+
expect(Share.share, 'was called with', {
61+
title: 'Lightning App logs',
62+
message: 'some-logs',
63+
});
64+
});
65+
66+
it('should log error if sharing fails', async () => {
67+
file.readLogs.rejects(new Error('Boom!'));
68+
await file.shareLogs();
69+
expect(logger.error, 'was called once');
70+
});
71+
});
72+
73+
describe('deleteWalletDB()', () => {
74+
it('should delete wallet db file', async () => {
75+
await file.deleteWalletDB('mainnet');
76+
expect(
77+
RNFS.unlink,
78+
'was called with',
79+
'/foo/bar/data/chain/bitcoin/mainnet/wallet.db'
80+
);
81+
});
82+
83+
it('should log error if deleting fails', async () => {
84+
RNFS.unlink.rejects(new Error('Boom!'));
85+
await file.deleteWalletDB('mainnet');
86+
expect(logger.info, 'was called once');
87+
});
88+
});
89+
});

0 commit comments

Comments
 (0)