Geth v1.9.11 #17
@@ -188,6 +188,11 @@ func makeFullNode(ctx *cli.Context) *node.Node {
|
||||
utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
|
||||
}
|
||||
|
||||
// Add dashboard daemon if requested. This should be the last registered service
|
||||
// in order to be able to collect information about the other services.
|
||||
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
|
||||
utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
|
||||
}
|
||||
return stack
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -1588,9 +1588,18 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
||||
|
||||
// RegisterDashboardService adds a dashboard to the stack.
|
||||
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
|
||||
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
return dashboard.New(cfg, commit, ctx.ResolvePath("logs")), nil
|
||||
err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
var (
|
||||
ethServ *eth.Ethereum
|
||||
lesServ *les.LightEthereum
|
||||
)
|
||||
_ = ctx.Service(ðServ)
|
||||
_ = ctx.Service(&lesServ)
|
||||
return dashboard.New(cfg, ethServ, lesServ, commit, ctx.ResolvePath("logs")), nil
|
||||
})
|
||||
if err != nil {
|
||||
Fatalf("Failed to register the dashboard service: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterShhService configures Whisper and adds it to the given node.
|
||||
|
||||
+10131
-9745
File diff suppressed because one or more lines are too long
@@ -0,0 +1,53 @@
|
||||
// @flow
|
||||
|
||||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import type {Chain as ChainType} from '../types/content';
|
||||
|
||||
export const inserter = () => (update: ChainType, prev: ChainType) => {
|
||||
if (!update.currentBlock) {
|
||||
return;
|
||||
}
|
||||
if (!prev.currentBlock) {
|
||||
prev.currentBlock = {};
|
||||
}
|
||||
prev.currentBlock.number = update.currentBlock.number;
|
||||
prev.currentBlock.timestamp = update.currentBlock.timestamp;
|
||||
return prev;
|
||||
};
|
||||
|
||||
// styles contains the constant styles of the component.
|
||||
const styles = {};
|
||||
|
||||
// themeStyles returns the styles generated from the theme for the component.
|
||||
const themeStyles = theme => ({});
|
||||
|
||||
export type Props = {
|
||||
content: Content,
|
||||
};
|
||||
|
||||
type State = {};
|
||||
|
||||
// Logs renders the log page.
|
||||
class Chain extends Component<Props, State> {
|
||||
render() {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
export default Chain;
|
||||
@@ -25,6 +25,7 @@ import Header from 'Header';
|
||||
import Body from 'Body';
|
||||
import {inserter as logInserter, SAME} from 'Logs';
|
||||
import {inserter as peerInserter} from 'Network';
|
||||
import {inserter as chainInserter} from 'Chain';
|
||||
import {MENU} from '../common';
|
||||
import type {Content} from '../types/content';
|
||||
|
||||
@@ -83,17 +84,24 @@ const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, pre
|
||||
// the execution of unnecessary operations (e.g. copy of the log array).
|
||||
const defaultContent: () => Content = () => ({
|
||||
general: {
|
||||
version: null,
|
||||
commit: null,
|
||||
version: null,
|
||||
genesis: '',
|
||||
},
|
||||
home: {},
|
||||
chain: {
|
||||
currentBlock: {
|
||||
number: 0,
|
||||
timestamp: 0,
|
||||
},
|
||||
},
|
||||
home: {},
|
||||
chain: {},
|
||||
txpool: {},
|
||||
network: {
|
||||
peers: {
|
||||
bundles: {},
|
||||
},
|
||||
diff: [],
|
||||
diff: [],
|
||||
activePeerCount: 0,
|
||||
},
|
||||
system: {
|
||||
activeMemory: [],
|
||||
@@ -121,9 +129,10 @@ const updaters = {
|
||||
general: {
|
||||
version: replacer,
|
||||
commit: replacer,
|
||||
genesis: replacer,
|
||||
},
|
||||
home: null,
|
||||
chain: null,
|
||||
chain: chainInserter(),
|
||||
txpool: null,
|
||||
network: peerInserter(200),
|
||||
system: {
|
||||
@@ -241,6 +250,7 @@ class Dashboard extends Component<Props, State> {
|
||||
<div className={this.props.classes.dashboard} style={styles.dashboard}>
|
||||
<Header
|
||||
switchSideBar={this.switchSideBar}
|
||||
content={this.state.content}
|
||||
/>
|
||||
<Body
|
||||
opened={this.state.sideBar}
|
||||
|
||||
@@ -32,6 +32,9 @@ import ChartRow from 'ChartRow';
|
||||
import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from 'CustomTooltip';
|
||||
import {chartStrokeWidth, styles as commonStyles} from '../common';
|
||||
import type {General, System} from '../types/content';
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faNetworkWired} from "@fortawesome/free-solid-svg-icons";
|
||||
import Toolbar from "@material-ui/core/Toolbar";
|
||||
|
||||
const FOOTER_SYNC_ID = 'footerSyncId';
|
||||
|
||||
@@ -154,6 +157,23 @@ class Footer extends Component<Props, State> {
|
||||
|
||||
render() {
|
||||
const {general, system} = this.props;
|
||||
let network = '';
|
||||
switch (general.genesis) {
|
||||
case '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3':
|
||||
network = 'main';
|
||||
break;
|
||||
case '0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d':
|
||||
network = 'ropsten';
|
||||
break;
|
||||
case '0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177':
|
||||
network = 'rinkeby';
|
||||
break;
|
||||
case '0xbf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a':
|
||||
network = 'görli';
|
||||
break;
|
||||
default:
|
||||
network = `unknown (${general.genesis.substring(0, 8)})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}>
|
||||
@@ -202,6 +222,9 @@ class Footer extends Component<Props, State> {
|
||||
</a>
|
||||
</Typography>
|
||||
)}
|
||||
<Typography style={styles.headerText}>
|
||||
<span style={commonStyles.light}>Network</span> {network}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -23,16 +23,25 @@ import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faBars} from '@fortawesome/free-solid-svg-icons';
|
||||
import {faBars, faSortAmountUp, faClock, faUsers, faSync} from '@fortawesome/free-solid-svg-icons';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import type {Content} from '../types/content';
|
||||
|
||||
|
||||
const magnitude = [31536000, 604800, 86400, 3600, 60, 1];
|
||||
const label = ['y', 'w', 'd', 'h', 'm', 's'];
|
||||
|
||||
// styles contains the constant styles of the component.
|
||||
const styles = {
|
||||
header: {
|
||||
height: '8%',
|
||||
},
|
||||
headerText: {
|
||||
marginRight: 15,
|
||||
},
|
||||
toolbar: {
|
||||
height: '100%',
|
||||
height: '100%',
|
||||
minHeight: 'unset',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -50,16 +59,52 @@ const themeStyles = (theme: Object) => ({
|
||||
title: {
|
||||
paddingLeft: theme.spacing.unit,
|
||||
fontSize: 3 * theme.spacing.unit,
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export type Props = {
|
||||
classes: Object, // injected by withStyles()
|
||||
classes: Object, // injected by withStyles()
|
||||
switchSideBar: () => void,
|
||||
content: Content,
|
||||
networkID: number,
|
||||
};
|
||||
|
||||
type State = {
|
||||
since: string,
|
||||
}
|
||||
// Header renders the header of the dashboard.
|
||||
class Header extends Component<Props> {
|
||||
class Header extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {since: ''};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.interval = setInterval(() => this.setState(() => {
|
||||
// time (seconds) since last block.
|
||||
let timeDiff = Math.floor((Date.now() - this.props.content.chain.currentBlock.timestamp * 1000) / 1000);
|
||||
let since = '';
|
||||
let i = 0;
|
||||
for (; i < magnitude.length && timeDiff < magnitude[i]; i++);
|
||||
for (let j = 2; i < magnitude.length && j > 0; j--, i++) {
|
||||
const t = Math.floor(timeDiff / magnitude[i]);
|
||||
if (t > 0) {
|
||||
since += `${t}${label[i]} `;
|
||||
timeDiff %= magnitude[i];
|
||||
}
|
||||
}
|
||||
if (since === '') {
|
||||
since = 'now';
|
||||
}
|
||||
this.setState({since: since});
|
||||
}), 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
|
||||
@@ -72,6 +117,15 @@ class Header extends Component<Props> {
|
||||
<Typography type='title' color='inherit' noWrap className={classes.title}>
|
||||
Go Ethereum Dashboard
|
||||
</Typography>
|
||||
<Typography style={styles.headerText}>
|
||||
<FontAwesomeIcon icon={faSortAmountUp} /> {this.props.content.chain.currentBlock.number}
|
||||
</Typography>
|
||||
<Typography style={styles.headerText}>
|
||||
<FontAwesomeIcon icon={faClock} /> {this.state.since}
|
||||
</Typography>
|
||||
<Typography style={styles.headerText}>
|
||||
<FontAwesomeIcon icon={faUsers} /> {this.props.content.network.activePeerCount}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ import React, {Component} from 'react';
|
||||
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
|
||||
import Chain from 'Chain';
|
||||
import Network from 'Network';
|
||||
import Logs from 'Logs';
|
||||
import Footer from 'Footer';
|
||||
@@ -95,7 +96,9 @@ class Main extends Component<Props, State> {
|
||||
children = <div>Work in progress.</div>;
|
||||
break;
|
||||
case MENU.get('chain').id:
|
||||
children = <div>Work in progress.</div>;
|
||||
children = <Chain
|
||||
content={this.props.content.chain}
|
||||
/>;
|
||||
break;
|
||||
case MENU.get('txpool').id:
|
||||
children = <div>Work in progress.</div>;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
@@ -27,17 +28,23 @@ import Grid from '@material-ui/core/Grid/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import {AreaChart, Area, Tooltip, YAxis} from 'recharts';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
import {faCircle as farCircle} from '@fortawesome/free-regular-svg-icons';
|
||||
import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons'; // More icons at fontawesome.com/icons
|
||||
import {faCircle as farCircle, faClipboard as farClipboard} from '@fortawesome/free-regular-svg-icons';
|
||||
import convert from 'color-convert';
|
||||
import {Scrollbars} from 'react-custom-scrollbars';
|
||||
|
||||
import CustomTooltip, {bytePlotter, multiplier} from 'CustomTooltip';
|
||||
import type {Network as NetworkType, PeerEvent} from '../types/content';
|
||||
import {styles as commonStyles, chartStrokeWidth, hues, hueScale} from '../common';
|
||||
import {chartStrokeWidth, hues, hueScale} from '../common';
|
||||
|
||||
// Peer chart dimensions.
|
||||
const trafficChartHeight = 18;
|
||||
const trafficChartWidth = 400;
|
||||
const trafficChartHeight = 15;
|
||||
const trafficChartWidth = 200;
|
||||
|
||||
// attemptSeparator separates the peer connection attempts
|
||||
// such as the peers from the addresses with more attempts
|
||||
// go to the beginning of the table, and the rest go to the end.
|
||||
const attemptSeparator = 9;
|
||||
|
||||
// setMaxIngress adjusts the peer chart's gradient values based on the given value.
|
||||
const setMaxIngress = (peer, value) => {
|
||||
@@ -120,6 +127,58 @@ const setEgressChartAttributes = (peer) => {
|
||||
setMaxEgress(peer, max);
|
||||
};
|
||||
|
||||
// shortName adds some heuristics to the node name in order to make it look meaningful.
|
||||
const shortName = (name: string) => {
|
||||
const parts = name.split('/');
|
||||
if (parts[0].substring(0, 'parity'.length).toLowerCase() === 'parity') {
|
||||
// Merge Parity and Parity-Ethereum under the same name.
|
||||
parts[0] = 'Parity';
|
||||
}
|
||||
if (parts.length < 2) {
|
||||
console.error('Incorrect node name', name);
|
||||
return parts[0];
|
||||
}
|
||||
const versionRE = RegExp(/^v?\d+\.\d+\.\d+.*/);
|
||||
// Drop optional custom identifier.
|
||||
if (!versionRE.test(parts[1])) {
|
||||
if (parts.length < 3 || !versionRE.test(parts[2])) {
|
||||
console.error('Incorrect node name', name);
|
||||
return parts[0];
|
||||
}
|
||||
parts[1] = parts[2];
|
||||
}
|
||||
// Cutting anything from the version after the first - or +.
|
||||
parts[1] = parts[1].split('-')[0].split('+')[0];
|
||||
return `${parts[0]}/${parts[1]}`;
|
||||
};
|
||||
|
||||
// shortLocation returns a shortened version of the given location object.
|
||||
const shortLocation = (location: Object) => {
|
||||
if (!location) {
|
||||
return '';
|
||||
}
|
||||
return `${location.city ? `${location.city}/` : ''}${location.country ? location.country : ''}`;
|
||||
};
|
||||
|
||||
// protocol returns a shortened version of the eth protocol values.
|
||||
const protocol = (p: Object) => {
|
||||
if (!p) {
|
||||
return '';
|
||||
}
|
||||
if (typeof p === 'string') {
|
||||
return p;
|
||||
}
|
||||
if (!(p instanceof Object)) {
|
||||
console.error('Wrong protocol type', p, typeof p);
|
||||
return '';
|
||||
}
|
||||
if (!p.hasOwnProperty('version') || !p.hasOwnProperty('difficulty') || !p.hasOwnProperty('head')) {
|
||||
console.error('Missing protocol attributes', p);
|
||||
return '';
|
||||
}
|
||||
return `h=${p.head.substring(0, 10)} v=${p.version} td=${p.difficulty}`;
|
||||
};
|
||||
|
||||
// inserter is a state updater function for the main component, which handles the peers.
|
||||
export const inserter = (sampleLimit: number) => (update: NetworkType, prev: NetworkType) => {
|
||||
// The first message contains the metered peer history.
|
||||
@@ -134,84 +193,104 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
|
||||
if (!peer.maxEgress) {
|
||||
setEgressChartAttributes(peer);
|
||||
}
|
||||
if (!peer.name) {
|
||||
peer.name = '';
|
||||
peer.shortName = '';
|
||||
} else if (!peer.shortName) {
|
||||
peer.shortName = shortName(peer.name);
|
||||
}
|
||||
if (!peer.enode) {
|
||||
peer.enode = '';
|
||||
}
|
||||
if (!peer.protocols) {
|
||||
peer.protocols = {};
|
||||
}
|
||||
peer.eth = protocol(peer.protocols.eth);
|
||||
peer.les = protocol(peer.protocols.les);
|
||||
});
|
||||
}
|
||||
bundle.shortLocation = shortLocation(bundle.location);
|
||||
});
|
||||
}
|
||||
if (Array.isArray(update.diff)) {
|
||||
update.diff.forEach((event: PeerEvent) => {
|
||||
if (!event.ip) {
|
||||
console.error('Peer event without IP', event);
|
||||
if (!event.addr) {
|
||||
console.error('Peer event without TCP address', event);
|
||||
return;
|
||||
}
|
||||
switch (event.remove) {
|
||||
case 'bundle': {
|
||||
delete prev.peers.bundles[event.ip];
|
||||
delete prev.peers.bundles[event.addr];
|
||||
return;
|
||||
}
|
||||
case 'known': {
|
||||
if (!event.id) {
|
||||
console.error('Remove known peer event without ID', event.ip);
|
||||
if (!event.enode) {
|
||||
console.error('Remove known peer event without node URL', event.addr);
|
||||
return;
|
||||
}
|
||||
const bundle = prev.peers.bundles[event.ip];
|
||||
if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.id]) {
|
||||
console.error('No known peer to remove', event.ip, event.id);
|
||||
const bundle = prev.peers.bundles[event.addr];
|
||||
if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.enode]) {
|
||||
console.error('No known peer to remove', event.addr, event.enode);
|
||||
return;
|
||||
}
|
||||
delete bundle.knownPeers[event.id];
|
||||
return;
|
||||
}
|
||||
case 'attempt': {
|
||||
const bundle = prev.peers.bundles[event.ip];
|
||||
if (!bundle || !Array.isArray(bundle.attempts) || bundle.attempts.length < 1) {
|
||||
console.error('No unknown peer to remove', event.ip);
|
||||
return;
|
||||
}
|
||||
bundle.attempts.splice(0, 1);
|
||||
delete bundle.knownPeers[event.enode];
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!prev.peers.bundles[event.ip]) {
|
||||
prev.peers.bundles[event.ip] = {
|
||||
if (!prev.peers.bundles[event.addr]) {
|
||||
prev.peers.bundles[event.addr] = {
|
||||
location: {
|
||||
country: '',
|
||||
city: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
},
|
||||
shortLocation: '',
|
||||
knownPeers: {},
|
||||
attempts: [],
|
||||
attempts: 0,
|
||||
};
|
||||
}
|
||||
const bundle = prev.peers.bundles[event.ip];
|
||||
const bundle = prev.peers.bundles[event.addr];
|
||||
if (event.location) {
|
||||
bundle.location = event.location;
|
||||
bundle.shortLocation = shortLocation(bundle.location);
|
||||
return;
|
||||
}
|
||||
if (!event.id) {
|
||||
if (!bundle.attempts) {
|
||||
bundle.attempts = [];
|
||||
}
|
||||
bundle.attempts.push({
|
||||
connected: event.connected,
|
||||
disconnected: event.disconnected,
|
||||
});
|
||||
if (!event.enode) {
|
||||
bundle.attempts++;
|
||||
return;
|
||||
}
|
||||
if (!bundle.knownPeers) {
|
||||
bundle.knownPeers = {};
|
||||
}
|
||||
if (!bundle.knownPeers[event.id]) {
|
||||
bundle.knownPeers[event.id] = {
|
||||
if (!bundle.knownPeers[event.enode]) {
|
||||
bundle.knownPeers[event.enode] = {
|
||||
connected: [],
|
||||
disconnected: [],
|
||||
ingress: [],
|
||||
egress: [],
|
||||
active: false,
|
||||
name: '',
|
||||
shortName: '',
|
||||
enode: '',
|
||||
protocols: {},
|
||||
eth: '',
|
||||
les: '',
|
||||
};
|
||||
}
|
||||
const peer = bundle.knownPeers[event.id];
|
||||
const peer = bundle.knownPeers[event.enode];
|
||||
if (event.name) {
|
||||
peer.name = event.name;
|
||||
peer.shortName = shortName(event.name);
|
||||
}
|
||||
if (event.enode) {
|
||||
peer.enode = event.enode;
|
||||
}
|
||||
if (event.protocols) {
|
||||
peer.protocols = event.protocols;
|
||||
peer.eth = protocol(peer.protocols.eth);
|
||||
peer.les = protocol(peer.protocols.les);
|
||||
}
|
||||
if (!peer.maxIngress) {
|
||||
setIngressChartAttributes(peer);
|
||||
}
|
||||
@@ -300,11 +379,29 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
|
||||
}
|
||||
});
|
||||
}
|
||||
prev.activePeerCount = 0;
|
||||
Object.entries(prev.peers.bundles).forEach(([addr, bundle]) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return;
|
||||
}
|
||||
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
|
||||
if (peer.active === true) {
|
||||
prev.activePeerCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
return prev;
|
||||
};
|
||||
|
||||
// styles contains the constant styles of the component.
|
||||
const styles = {
|
||||
title: {
|
||||
marginLeft: 5,
|
||||
},
|
||||
table: {
|
||||
borderCollapse: 'unset',
|
||||
padding: 5,
|
||||
},
|
||||
tableHead: {
|
||||
height: 'auto',
|
||||
},
|
||||
@@ -317,13 +414,39 @@ const styles = {
|
||||
paddingBottom: 0,
|
||||
paddingLeft: 5,
|
||||
border: 'none',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
},
|
||||
content: {
|
||||
height: '800px',
|
||||
},
|
||||
};
|
||||
|
||||
// themeStyles returns the styles generated from the theme for the component.
|
||||
const themeStyles = theme => ({
|
||||
title: {
|
||||
color: theme.palette.common.white,
|
||||
},
|
||||
table: {
|
||||
background: theme.palette.grey[900],
|
||||
},
|
||||
});
|
||||
|
||||
// limitedWidthStyle returns a style object which cuts the long text with three dots.
|
||||
const limitedWidthStyle = (width) => {
|
||||
return {
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: width,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
container: Object,
|
||||
content: NetworkType,
|
||||
shouldUpdate: Object,
|
||||
classes: Object, // injected by withStyles()
|
||||
container: Object,
|
||||
content: NetworkType,
|
||||
shouldUpdate: Object,
|
||||
};
|
||||
|
||||
type State = {};
|
||||
@@ -351,179 +474,385 @@ class Network extends Component<Props, State> {
|
||||
return `${month}/${date}/${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
copyToClipboard = (id) => (event) => {
|
||||
copyToClipboard = (text: string) => (event) => {
|
||||
event.preventDefault();
|
||||
navigator.clipboard.writeText(id).then(() => {}, () => {
|
||||
console.error("Failed to copy node id", id);
|
||||
navigator.clipboard.writeText(text).then(() => {}, () => {
|
||||
console.error("Failed to copy", text);
|
||||
});
|
||||
};
|
||||
|
||||
peerTableRow = (ip, id, bundle, peer) => {
|
||||
lesList = () => {
|
||||
const list = [];
|
||||
Object.values(this.props.content.peers.bundles).forEach((bundle) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return;
|
||||
}
|
||||
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
|
||||
if (peer.les === '' || peer.eth !== '') {
|
||||
return;
|
||||
}
|
||||
list.push({enode, name: peer.name, location: bundle.location, protocols: peer.protocols});
|
||||
});
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
ethList = () => {
|
||||
const list = [];
|
||||
Object.values(this.props.content.peers.bundles).forEach((bundle) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return;
|
||||
}
|
||||
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
|
||||
if (peer.eth === '' && peer.les !== '') {
|
||||
return;
|
||||
}
|
||||
list.push({enode, name: peer.name, location: bundle.location, protocols: peer.protocols});
|
||||
});
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
attemptList = () => {
|
||||
const list = [];
|
||||
Object.entries(this.props.content.peers.bundles).forEach(([addr, bundle]) => {
|
||||
if (!bundle.attempts) {
|
||||
return;
|
||||
}
|
||||
list.push({addr, location: bundle.location, attempts: bundle.attempts});
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
knownPeerTableRow = (addr, enode, bundle, peer, showTraffic, proto) => {
|
||||
const ingressValues = peer.ingress.map(({value}) => ({ingress: value || 0.001}));
|
||||
const egressValues = peer.egress.map(({value}) => ({egress: -value || -0.001}));
|
||||
|
||||
return (
|
||||
<TableRow key={`known_${ip}_${id}`} style={styles.tableRow}>
|
||||
<TableRow key={`known_${addr}_${enode}`} style={styles.tableRow}>
|
||||
<TableCell style={styles.tableCell}>
|
||||
{peer.active
|
||||
? <FontAwesomeIcon icon={fasCircle} color='green' />
|
||||
: <FontAwesomeIcon icon={farCircle} style={commonStyles.light} />
|
||||
: <FontAwesomeIcon icon={farCircle} />
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell style={{fontFamily: 'monospace', cursor: 'copy', ...styles.tableCell, ...commonStyles.light}} onClick={this.copyToClipboard(id)}>
|
||||
{id.substring(0, 10)}
|
||||
<TableCell
|
||||
style={{
|
||||
cursor: 'copy',
|
||||
...styles.tableCell,
|
||||
...limitedWidthStyle(80),
|
||||
}}
|
||||
onClick={this.copyToClipboard(enode)}
|
||||
>
|
||||
{enode.substring(8)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
cursor: 'copy',
|
||||
...styles.tableCell,
|
||||
...limitedWidthStyle(80),
|
||||
}}
|
||||
onClick={this.copyToClipboard(peer.name)}
|
||||
>
|
||||
{peer.shortName}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
cursor: 'copy',
|
||||
...styles.tableCell,
|
||||
...limitedWidthStyle(100),
|
||||
}}
|
||||
onClick={this.copyToClipboard(JSON.stringify(bundle.location))}
|
||||
>
|
||||
{bundle.shortLocation}
|
||||
</TableCell>
|
||||
<TableCell style={styles.tableCell}>
|
||||
{bundle.location ? (() => {
|
||||
const l = bundle.location;
|
||||
return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
|
||||
})() : ''}
|
||||
</TableCell>
|
||||
<TableCell style={styles.tableCell}>
|
||||
<AreaChart
|
||||
width={trafficChartWidth}
|
||||
height={trafficChartHeight}
|
||||
data={ingressValues}
|
||||
margin={{top: 5, right: 5, bottom: 0, left: 5}}
|
||||
syncId={`peerIngress_${ip}_${id}`}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={`ingressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
|
||||
{peer.ingressGradient
|
||||
&& peer.ingressGradient.map(({offset, color}, i) => (
|
||||
<stop
|
||||
key={`ingressStop_${ip}_${id}_${i}`}
|
||||
offset={`${offset}%`}
|
||||
stopColor={color}
|
||||
/>
|
||||
))}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
|
||||
<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
|
||||
<Area
|
||||
dataKey='ingress'
|
||||
isAnimationActive={false}
|
||||
type='monotone'
|
||||
fill={`url(#ingressGradient_${ip}_${id})`}
|
||||
stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
|
||||
strokeWidth={chartStrokeWidth}
|
||||
/>
|
||||
</AreaChart>
|
||||
<AreaChart
|
||||
width={trafficChartWidth}
|
||||
height={trafficChartHeight}
|
||||
data={egressValues}
|
||||
margin={{top: 0, right: 5, bottom: 5, left: 5}}
|
||||
syncId={`peerIngress_${ip}_${id}`}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={`egressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
|
||||
{peer.egressGradient
|
||||
&& peer.egressGradient.map(({offset, color}, i) => (
|
||||
<stop
|
||||
key={`egressStop_${ip}_${id}_${i}`}
|
||||
offset={`${offset}%`}
|
||||
stopColor={color}
|
||||
/>
|
||||
))}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
|
||||
<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
|
||||
<Area
|
||||
dataKey='egress'
|
||||
isAnimationActive={false}
|
||||
type='monotone'
|
||||
fill={`url(#egressGradient_${ip}_${id})`}
|
||||
stroke={peer.egressGradient[0].color}
|
||||
strokeWidth={chartStrokeWidth}
|
||||
/>
|
||||
</AreaChart>
|
||||
{showTraffic ? (
|
||||
<>
|
||||
<AreaChart
|
||||
width={trafficChartWidth}
|
||||
height={trafficChartHeight}
|
||||
data={ingressValues}
|
||||
margin={{top: 5, right: 5, bottom: 0, left: 5}}
|
||||
syncId={`peerIngress_${addr}_${enode}`}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={`ingressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
|
||||
{peer.ingressGradient
|
||||
&& peer.ingressGradient.map(({offset, color}, i) => (
|
||||
<stop
|
||||
key={`ingressStop_${addr}_${enode}_${i}`}
|
||||
offset={`${offset}%`}
|
||||
stopColor={color}
|
||||
/>
|
||||
))}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
|
||||
<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
|
||||
<Area
|
||||
dataKey='ingress'
|
||||
isAnimationActive={false}
|
||||
type='monotone'
|
||||
fill={`url(#ingressGradient_${addr}_${enode})`}
|
||||
stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
|
||||
strokeWidth={chartStrokeWidth}
|
||||
/>
|
||||
</AreaChart>
|
||||
<AreaChart
|
||||
width={trafficChartWidth}
|
||||
height={trafficChartHeight}
|
||||
data={egressValues}
|
||||
margin={{top: 0, right: 5, bottom: 5, left: 5}}
|
||||
syncId={`peerIngress_${addr}_${enode}`}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={`egressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
|
||||
{peer.egressGradient
|
||||
&& peer.egressGradient.map(({offset, color}, i) => (
|
||||
<stop
|
||||
key={`egressStop_${addr}_${enode}_${i}`}
|
||||
offset={`${offset}%`}
|
||||
stopColor={color}
|
||||
/>
|
||||
))}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
|
||||
<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
|
||||
<Area
|
||||
dataKey='egress'
|
||||
isAnimationActive={false}
|
||||
type='monotone'
|
||||
fill={`url(#egressGradient_${addr}_${enode})`}
|
||||
stroke={peer.egressGradient[0].color}
|
||||
strokeWidth={chartStrokeWidth}
|
||||
/>
|
||||
</AreaChart>
|
||||
</>
|
||||
) : null}
|
||||
</TableCell>
|
||||
{typeof proto === 'object' ? (
|
||||
<>
|
||||
<TableCell
|
||||
style={{
|
||||
cursor: 'copy',
|
||||
...styles.tableCell,
|
||||
...limitedWidthStyle(80),
|
||||
}}
|
||||
onClick={this.copyToClipboard(JSON.stringify(proto.head))}
|
||||
>
|
||||
{proto.head}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
cursor: 'copy',
|
||||
...styles.tableCell,
|
||||
}}
|
||||
onClick={this.copyToClipboard(JSON.stringify(proto.difficulty))}
|
||||
>
|
||||
{proto.difficulty}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
cursor: 'copy',
|
||||
...styles.tableCell,
|
||||
}}
|
||||
onClick={this.copyToClipboard(JSON.stringify(proto.version))}
|
||||
>
|
||||
{proto.version}
|
||||
</TableCell>
|
||||
</>
|
||||
) : null }
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
connectionAttemptTableRow = (addr, bundle) => (
|
||||
<TableRow key={`attempt_${addr}`} style={styles.tableRow}>
|
||||
<TableCell
|
||||
style={{cursor: 'copy', ...styles.tableCell}}
|
||||
onClick={this.copyToClipboard(addr)}
|
||||
>
|
||||
{addr}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{cursor: 'copy', ...limitedWidthStyle(100), ...styles.tableCell}}
|
||||
onClick={this.copyToClipboard(JSON.stringify(bundle.location))}
|
||||
>
|
||||
{bundle.shortLocation}
|
||||
</TableCell>
|
||||
<TableCell style={styles.tableCell}>
|
||||
{bundle.attempts}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
return (
|
||||
<Grid container direction='row' justify='space-between'>
|
||||
<Grid item>
|
||||
<Table>
|
||||
<TableHead style={styles.tableHead}>
|
||||
<TableRow style={styles.tableRow}>
|
||||
<TableCell style={styles.tableCell} />
|
||||
<TableCell style={styles.tableCell}>Node ID</TableCell>
|
||||
<TableCell style={styles.tableCell}>Location</TableCell>
|
||||
<TableCell style={styles.tableCell}>Traffic</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return null;
|
||||
}
|
||||
return Object.entries(bundle.knownPeers).map(([id, peer]) => {
|
||||
if (peer.active === false) {
|
||||
return null;
|
||||
}
|
||||
return this.peerTableRow(ip, id, bundle, peer);
|
||||
});
|
||||
})}
|
||||
</TableBody>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return null;
|
||||
}
|
||||
return Object.entries(bundle.knownPeers).map(([id, peer]) => {
|
||||
if (peer.active === true) {
|
||||
return null;
|
||||
}
|
||||
return this.peerTableRow(ip, id, bundle, peer);
|
||||
});
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant='subtitle1' gutterBottom>
|
||||
Connection attempts
|
||||
</Typography>
|
||||
<Table>
|
||||
<TableHead style={styles.tableHead}>
|
||||
<TableRow style={styles.tableRow}>
|
||||
<TableCell style={styles.tableCell}>IP</TableCell>
|
||||
<TableCell style={styles.tableCell}>Location</TableCell>
|
||||
<TableCell style={styles.tableCell}>Nr</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
|
||||
if (!bundle.attempts || bundle.attempts.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TableRow key={`attempt_${ip}`} style={styles.tableRow}>
|
||||
<TableCell style={styles.tableCell}>{ip}</TableCell>
|
||||
<TableCell style={styles.tableCell}>
|
||||
{bundle.location ? (() => {
|
||||
const l = bundle.location;
|
||||
return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
|
||||
})() : ''}
|
||||
</TableCell>
|
||||
<TableCell style={styles.tableCell}>
|
||||
{Object.values(bundle.attempts).length}
|
||||
</TableCell>
|
||||
<Grid container direction='row' spacing={3}>
|
||||
<Grid item style={{width: '40%'}}>
|
||||
<div className={classes.table} style={styles.table}>
|
||||
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
|
||||
Full peers
|
||||
<FontAwesomeIcon
|
||||
icon={farClipboard}
|
||||
onClick={this.copyToClipboard(JSON.stringify(this.ethList()))}
|
||||
style={{float: 'right'}}
|
||||
/>
|
||||
</Typography>
|
||||
<Scrollbars style={styles.content}>
|
||||
<Table>
|
||||
<TableHead style={styles.tableHead}>
|
||||
<TableRow style={styles.tableRow}>
|
||||
<TableCell style={styles.tableCell} />
|
||||
<TableCell style={styles.tableCell}>Node URL</TableCell>
|
||||
<TableCell style={styles.tableCell}>Name</TableCell>
|
||||
<TableCell style={styles.tableCell}>Location</TableCell>
|
||||
<TableCell style={styles.tableCell}>Traffic</TableCell>
|
||||
<TableCell style={styles.tableCell}>Head</TableCell>
|
||||
<TableCell style={styles.tableCell}>TD</TableCell>
|
||||
<TableCell style={styles.tableCell}>V</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return null;
|
||||
}
|
||||
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
|
||||
if (peer.active === false) {
|
||||
return null;
|
||||
}
|
||||
if (peer.eth === '' && peer.les !== '') {
|
||||
return null;
|
||||
}
|
||||
return this.knownPeerTableRow(addr, enode, bundle, peer, true, peer.protocols.eth);
|
||||
});
|
||||
})}
|
||||
</TableBody>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return null;
|
||||
}
|
||||
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
|
||||
if (peer.active === true) {
|
||||
return null;
|
||||
}
|
||||
if (peer.eth === '' && peer.les !== '') {
|
||||
return null;
|
||||
}
|
||||
return this.knownPeerTableRow(addr, enode, bundle, peer, false, peer.protocols.eth);
|
||||
});
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item style={{width: '40%'}}>
|
||||
<div className={classes.table} style={styles.table}>
|
||||
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
|
||||
Light peers
|
||||
<FontAwesomeIcon
|
||||
icon={farClipboard}
|
||||
onClick={this.copyToClipboard(JSON.stringify(this.lesList()))}
|
||||
style={{float: 'right'}}
|
||||
/>
|
||||
</Typography>
|
||||
<Scrollbars style={styles.content}>
|
||||
<Table>
|
||||
<TableHead style={styles.tableHead}>
|
||||
<TableRow style={styles.tableRow}>
|
||||
<TableCell style={styles.tableCell} />
|
||||
<TableCell style={styles.tableCell}>Node URL</TableCell>
|
||||
<TableCell style={styles.tableCell}>Name</TableCell>
|
||||
<TableCell style={styles.tableCell}>Location</TableCell>
|
||||
<TableCell style={styles.tableCell}>Traffic</TableCell>
|
||||
<TableCell style={styles.tableCell}>Head</TableCell>
|
||||
<TableCell style={styles.tableCell}>TD</TableCell>
|
||||
<TableCell style={styles.tableCell}>V</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return null;
|
||||
}
|
||||
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
|
||||
if (peer.active === false) {
|
||||
return null;
|
||||
}
|
||||
if (peer.les === '' || peer.eth !== '') {
|
||||
return null;
|
||||
}
|
||||
return this.knownPeerTableRow(addr, enode, bundle, peer, true, peer.protocols.les);
|
||||
});
|
||||
})}
|
||||
</TableBody>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
|
||||
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
|
||||
return null;
|
||||
}
|
||||
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
|
||||
if (peer.active === true) {
|
||||
return null;
|
||||
}
|
||||
if (peer.les === '' || peer.eth !== '') {
|
||||
return null;
|
||||
}
|
||||
return this.knownPeerTableRow(addr, enode, bundle, peer, false, peer.protocols.les);
|
||||
});
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<div className={classes.table} style={styles.table}>
|
||||
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
|
||||
Connection attempts
|
||||
<FontAwesomeIcon
|
||||
icon={farClipboard}
|
||||
onClick={this.copyToClipboard(JSON.stringify(this.attemptList()))}
|
||||
style={{float: 'right'}}
|
||||
/>
|
||||
</Typography>
|
||||
<Scrollbars style={styles.content}>
|
||||
<Table>
|
||||
<TableHead style={styles.tableHead}>
|
||||
<TableRow style={styles.tableRow}>
|
||||
<TableCell style={styles.tableCell}>TCP address</TableCell>
|
||||
<TableCell style={styles.tableCell}>Location</TableCell>
|
||||
<TableCell style={styles.tableCell}>Nr</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
|
||||
if (!bundle.attempts || bundle.attempts <= attemptSeparator) {
|
||||
return null;
|
||||
}
|
||||
return this.connectionAttemptTableRow(addr, bundle);
|
||||
})}
|
||||
</TableBody>
|
||||
<TableBody>
|
||||
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
|
||||
if (!bundle.attempts || bundle.attempts < 1 || bundle.attempts > attemptSeparator) {
|
||||
return null;
|
||||
}
|
||||
return this.connectionAttemptTableRow(addr, bundle);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Network;
|
||||
export default withStyles(themeStyles)(Network);
|
||||
|
||||
@@ -47,10 +47,11 @@ const themeStyles = theme => ({
|
||||
background: theme.palette.grey[900],
|
||||
},
|
||||
listItem: {
|
||||
minWidth: theme.spacing.unit * 7,
|
||||
minWidth: theme.spacing(7),
|
||||
color: theme.palette.common.white,
|
||||
},
|
||||
icon: {
|
||||
fontSize: theme.spacing.unit * 3,
|
||||
fontSize: theme.spacing(3),
|
||||
overflow: 'unset',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,54 +1,56 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.3.4",
|
||||
"@babel/plugin-proposal-class-properties": "7.3.4",
|
||||
"@babel/core": "7.4.5",
|
||||
"@babel/plugin-proposal-class-properties": "7.4.4",
|
||||
"@babel/plugin-proposal-function-bind": "7.2.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.3.4",
|
||||
"@babel/preset-env": "7.3.4",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.4.4",
|
||||
"@babel/preset-env": "7.4.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-stage-0": "^7.0.0",
|
||||
"@fortawesome/fontawesome-free-regular": "^5.0.13",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.15",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.7.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.18",
|
||||
"@fortawesome/free-regular-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"@material-ui/core": "3.9.2",
|
||||
"@material-ui/icons": "3.0.2",
|
||||
"@material-ui/core": "4.0.1",
|
||||
"@material-ui/icons": "4.0.1",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-loader": "8.0.5",
|
||||
"babel-loader": "8.0.6",
|
||||
"classnames": "^2.2.6",
|
||||
"color-convert": "^2.0.0",
|
||||
"css-loader": "2.1.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"eslint": "5.15.1",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-airbnb": "^17.0.0",
|
||||
"eslint-loader": "2.1.2",
|
||||
"eslint-plugin-flowtype": "3.4.2",
|
||||
"eslint-plugin-import": "2.16.0",
|
||||
"eslint-plugin-flowtype": "3.9.1",
|
||||
"eslint-plugin-import": "2.17.3",
|
||||
"eslint-plugin-jsx-a11y": "6.2.1",
|
||||
"eslint-plugin-node": "8.0.1",
|
||||
"eslint-plugin-promise": "4.0.1",
|
||||
"eslint-plugin-react": "7.12.4",
|
||||
"eslint-plugin-node": "9.1.0",
|
||||
"eslint-plugin-promise": "4.1.1",
|
||||
"eslint-plugin-react": "7.13.0",
|
||||
"file-loader": "3.0.1",
|
||||
"flow-bin": "0.94.0",
|
||||
"flow-bin": "0.98.1",
|
||||
"flow-bin-loader": "^1.0.3",
|
||||
"flow-typed": "^2.5.1",
|
||||
"js-beautify": "1.9.0",
|
||||
"flow-typed": "2.5.2",
|
||||
"js-beautify": "1.10.0",
|
||||
"path": "^0.12.7",
|
||||
"react": "16.8.4",
|
||||
"react-dom": "16.8.4",
|
||||
"react-hot-loader": "4.8.0",
|
||||
"react-transition-group": "2.6.1",
|
||||
"recharts": "1.5.0",
|
||||
"react": "16.8.6",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "16.8.6",
|
||||
"react-hot-loader": "4.8.8",
|
||||
"react-scrollbar": "0.5.6",
|
||||
"react-transition-group": "4.0.1",
|
||||
"recharts": "1.6.2",
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"terser-webpack-plugin": "1.3.0",
|
||||
"url": "^0.11.0",
|
||||
"url-loader": "1.1.2",
|
||||
"webpack": "4.29.6",
|
||||
"webpack-cli": "3.2.3",
|
||||
"webpack-dashboard": "3.0.0",
|
||||
"webpack-dev-server": "3.2.1",
|
||||
"webpack": "4.32.2",
|
||||
"webpack-cli": "3.3.2",
|
||||
"webpack-dashboard": "3.0.7",
|
||||
"webpack-dev-server": "3.4.1",
|
||||
"webpack-merge": "4.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -35,6 +35,7 @@ export type ChartEntry = {
|
||||
export type General = {
|
||||
version: ?string,
|
||||
commit: ?string,
|
||||
genesis: ?string,
|
||||
};
|
||||
|
||||
export type Home = {
|
||||
@@ -42,21 +43,29 @@ export type Home = {
|
||||
};
|
||||
|
||||
export type Chain = {
|
||||
/* TODO (kurkomisi) */
|
||||
currentBlock: Block,
|
||||
};
|
||||
|
||||
export type Block = {
|
||||
number: number,
|
||||
timestamp: number,
|
||||
}
|
||||
|
||||
export type TxPool = {
|
||||
/* TODO (kurkomisi) */
|
||||
};
|
||||
|
||||
export type Network = {
|
||||
peers: Peers,
|
||||
diff: Array<PeerEvent>
|
||||
peers: Peers,
|
||||
diff: Array<PeerEvent>,
|
||||
activePeerCount: number,
|
||||
};
|
||||
|
||||
export type PeerEvent = {
|
||||
ip: string,
|
||||
id: string,
|
||||
name: string,
|
||||
addr: string,
|
||||
enode: string,
|
||||
protocols: {[string]: Object},
|
||||
remove: string,
|
||||
location: GeoLocation,
|
||||
connected: Date,
|
||||
@@ -71,9 +80,9 @@ export type Peers = {
|
||||
};
|
||||
|
||||
export type PeerBundle = {
|
||||
location: GeoLocation,
|
||||
knownPeers: {[string]: KnownPeer},
|
||||
attempts: Array<UnknownPeer>,
|
||||
location: GeoLocation,
|
||||
knownPeers: {[string]: KnownPeer},
|
||||
attempts: number,
|
||||
};
|
||||
|
||||
export type KnownPeer = {
|
||||
@@ -81,14 +90,12 @@ export type KnownPeer = {
|
||||
disconnected: Array<Date>,
|
||||
ingress: Array<ChartEntries>,
|
||||
egress: Array<ChartEntries>,
|
||||
name: string,
|
||||
enode: string,
|
||||
protocols: {[string]: Object},
|
||||
active: boolean,
|
||||
};
|
||||
|
||||
export type UnknownPeer = {
|
||||
connected: Date,
|
||||
disconnected: Date,
|
||||
};
|
||||
|
||||
export type GeoLocation = {
|
||||
country: string,
|
||||
city: string,
|
||||
|
||||
+739
-664
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
type block struct {
|
||||
Number int64 `json:"number,omitempty"`
|
||||
Time uint64 `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
func (db *Dashboard) collectChainData() {
|
||||
defer db.wg.Done()
|
||||
|
||||
var (
|
||||
currentBlock *block
|
||||
chainCh chan core.ChainHeadEvent
|
||||
chainSub event.Subscription
|
||||
)
|
||||
switch {
|
||||
case db.ethServ != nil:
|
||||
chain := db.ethServ.BlockChain()
|
||||
currentBlock = &block{
|
||||
Number: chain.CurrentHeader().Number.Int64(),
|
||||
Time: chain.CurrentHeader().Time,
|
||||
}
|
||||
chainCh = make(chan core.ChainHeadEvent)
|
||||
chainSub = chain.SubscribeChainHeadEvent(chainCh)
|
||||
case db.lesServ != nil:
|
||||
chain := db.lesServ.BlockChain()
|
||||
currentBlock = &block{
|
||||
Number: chain.CurrentHeader().Number.Int64(),
|
||||
Time: chain.CurrentHeader().Time,
|
||||
}
|
||||
chainCh = make(chan core.ChainHeadEvent)
|
||||
chainSub = chain.SubscribeChainHeadEvent(chainCh)
|
||||
default:
|
||||
errc := <-db.quit
|
||||
errc <- nil
|
||||
return
|
||||
}
|
||||
defer chainSub.Unsubscribe()
|
||||
|
||||
db.chainLock.Lock()
|
||||
db.history.Chain = &ChainMessage{
|
||||
CurrentBlock: currentBlock,
|
||||
}
|
||||
db.chainLock.Unlock()
|
||||
db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
|
||||
|
||||
for {
|
||||
select {
|
||||
case e := <-chainCh:
|
||||
currentBlock := &block{
|
||||
Number: e.Block.Number().Int64(),
|
||||
Time: e.Block.Time(),
|
||||
}
|
||||
db.chainLock.Lock()
|
||||
db.history.Chain = &ChainMessage{
|
||||
CurrentBlock: currentBlock,
|
||||
}
|
||||
db.chainLock.Unlock()
|
||||
|
||||
db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
|
||||
case err := <-chainSub.Err():
|
||||
log.Warn("Chain subscription error", "err", err)
|
||||
errc := <-db.quit
|
||||
errc <- nil
|
||||
return
|
||||
case errc := <-db.quit:
|
||||
errc <- nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-26
@@ -27,14 +27,16 @@ package dashboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
@@ -44,7 +46,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
sampleLimit = 200 // Maximum number of data samples
|
||||
sampleLimit = 200 // Maximum number of data samples
|
||||
dataCollectorCount = 4
|
||||
)
|
||||
|
||||
// Dashboard contains the dashboard internals.
|
||||
@@ -57,16 +60,23 @@ type Dashboard struct {
|
||||
|
||||
history *Message // Stored historical data
|
||||
|
||||
lock sync.Mutex // Lock protecting the dashboard's internals
|
||||
sysLock sync.RWMutex // Lock protecting the stored system data
|
||||
peerLock sync.RWMutex // Lock protecting the stored peer data
|
||||
logLock sync.RWMutex // Lock protecting the stored log data
|
||||
lock sync.Mutex // Lock protecting the dashboard's internals
|
||||
chainLock sync.RWMutex // Lock protecting the stored blockchain data
|
||||
sysLock sync.RWMutex // Lock protecting the stored system data
|
||||
peerLock sync.RWMutex // Lock protecting the stored peer data
|
||||
logLock sync.RWMutex // Lock protecting the stored log data
|
||||
|
||||
geodb *geoDB // geoip database instance for IP to geographical information conversions
|
||||
logdir string // Directory containing the log files
|
||||
|
||||
quit chan chan error // Channel used for graceful exit
|
||||
wg sync.WaitGroup // Wait group used to close the data collector threads
|
||||
|
||||
peerCh chan p2p.MeteredPeerEvent // Peer event channel.
|
||||
subPeer event.Subscription // Peer event subscription.
|
||||
|
||||
ethServ *eth.Ethereum // Ethereum object serving internals.
|
||||
lesServ *les.LightEthereum // LightEthereum object serving internals.
|
||||
}
|
||||
|
||||
// client represents active websocket connection with a remote browser.
|
||||
@@ -77,12 +87,23 @@ type client struct {
|
||||
}
|
||||
|
||||
// New creates a new dashboard instance with the given configuration.
|
||||
func New(config *Config, commit string, logdir string) *Dashboard {
|
||||
now := time.Now()
|
||||
func New(config *Config, ethServ *eth.Ethereum, lesServ *les.LightEthereum, commit string, logdir string) *Dashboard {
|
||||
// There is a data race between the network layer and the dashboard, which
|
||||
// can cause some lost peer events, therefore some peers might not appear
|
||||
// on the dashboard.
|
||||
// In order to solve this problem, the peer event subscription is registered
|
||||
// here, before the network layer starts.
|
||||
peerCh := make(chan p2p.MeteredPeerEvent, p2p.MeteredPeerLimit)
|
||||
versionMeta := ""
|
||||
if len(params.VersionMeta) > 0 {
|
||||
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
|
||||
}
|
||||
var genesis common.Hash
|
||||
if ethServ != nil {
|
||||
genesis = ethServ.BlockChain().Genesis().Hash()
|
||||
} else if lesServ != nil {
|
||||
genesis = lesServ.BlockChain().Genesis().Hash()
|
||||
}
|
||||
return &Dashboard{
|
||||
conns: make(map[uint32]*client),
|
||||
config: config,
|
||||
@@ -91,24 +112,29 @@ func New(config *Config, commit string, logdir string) *Dashboard {
|
||||
General: &GeneralMessage{
|
||||
Commit: commit,
|
||||
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
|
||||
Genesis: genesis,
|
||||
},
|
||||
System: &SystemMessage{
|
||||
ActiveMemory: emptyChartEntries(now, sampleLimit),
|
||||
VirtualMemory: emptyChartEntries(now, sampleLimit),
|
||||
NetworkIngress: emptyChartEntries(now, sampleLimit),
|
||||
NetworkEgress: emptyChartEntries(now, sampleLimit),
|
||||
ProcessCPU: emptyChartEntries(now, sampleLimit),
|
||||
SystemCPU: emptyChartEntries(now, sampleLimit),
|
||||
DiskRead: emptyChartEntries(now, sampleLimit),
|
||||
DiskWrite: emptyChartEntries(now, sampleLimit),
|
||||
ActiveMemory: emptyChartEntries(sampleLimit),
|
||||
VirtualMemory: emptyChartEntries(sampleLimit),
|
||||
NetworkIngress: emptyChartEntries(sampleLimit),
|
||||
NetworkEgress: emptyChartEntries(sampleLimit),
|
||||
ProcessCPU: emptyChartEntries(sampleLimit),
|
||||
SystemCPU: emptyChartEntries(sampleLimit),
|
||||
DiskRead: emptyChartEntries(sampleLimit),
|
||||
DiskWrite: emptyChartEntries(sampleLimit),
|
||||
},
|
||||
},
|
||||
logdir: logdir,
|
||||
logdir: logdir,
|
||||
peerCh: peerCh,
|
||||
subPeer: p2p.SubscribeMeteredPeerEvent(peerCh),
|
||||
ethServ: ethServ,
|
||||
lesServ: lesServ,
|
||||
}
|
||||
}
|
||||
|
||||
// emptyChartEntries returns a ChartEntry array containing limit number of empty samples.
|
||||
func emptyChartEntries(t time.Time, limit int) ChartEntries {
|
||||
func emptyChartEntries(limit int) ChartEntries {
|
||||
ce := make(ChartEntries, limit)
|
||||
for i := 0; i < limit; i++ {
|
||||
ce[i] = new(ChartEntry)
|
||||
@@ -127,7 +153,8 @@ func (db *Dashboard) APIs() []rpc.API { return nil }
|
||||
func (db *Dashboard) Start(server *p2p.Server) error {
|
||||
log.Info("Starting dashboard", "url", fmt.Sprintf("http://%s:%d", db.config.Host, db.config.Port))
|
||||
|
||||
db.wg.Add(3)
|
||||
db.wg.Add(dataCollectorCount)
|
||||
go db.collectChainData()
|
||||
go db.collectSystemData()
|
||||
go db.streamLogs()
|
||||
go db.collectPeerData()
|
||||
@@ -141,7 +168,11 @@ func (db *Dashboard) Start(server *p2p.Server) error {
|
||||
}
|
||||
db.listener = listener
|
||||
|
||||
go http.Serve(listener, nil)
|
||||
go func() {
|
||||
if err := http.Serve(listener, nil); err != http.ErrServerClosed {
|
||||
log.Warn("Could not accept incoming HTTP connections", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -155,8 +186,8 @@ func (db *Dashboard) Stop() error {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
// Close the collectors.
|
||||
errc := make(chan error, 1)
|
||||
for i := 0; i < 3; i++ {
|
||||
errc := make(chan error, dataCollectorCount)
|
||||
for i := 0; i < dataCollectorCount; i++ {
|
||||
db.quit <- errc
|
||||
if err := <-errc; err != nil {
|
||||
errs = append(errs, err)
|
||||
@@ -230,20 +261,21 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||
}()
|
||||
|
||||
// Send the past data.
|
||||
db.chainLock.RLock()
|
||||
db.sysLock.RLock()
|
||||
db.peerLock.RLock()
|
||||
db.logLock.RLock()
|
||||
|
||||
h := deepcopy.Copy(db.history).(*Message)
|
||||
|
||||
db.chainLock.RUnlock()
|
||||
db.sysLock.RUnlock()
|
||||
db.peerLock.RUnlock()
|
||||
db.logLock.RUnlock()
|
||||
|
||||
client.msg <- h
|
||||
|
||||
// Start tracking the connection and drop at connection loss.
|
||||
db.lock.Lock()
|
||||
client.msg <- h
|
||||
db.conns[id] = client
|
||||
db.lock.Unlock()
|
||||
defer func() {
|
||||
|
||||
@@ -18,6 +18,8 @@ package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
@@ -37,8 +39,9 @@ type ChartEntry struct {
|
||||
}
|
||||
|
||||
type GeneralMessage struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
Genesis common.Hash `json:"genesis,omitempty"`
|
||||
}
|
||||
|
||||
type HomeMessage struct {
|
||||
@@ -46,7 +49,7 @@ type HomeMessage struct {
|
||||
}
|
||||
|
||||
type ChainMessage struct {
|
||||
/* TODO (kurkomisi) */
|
||||
CurrentBlock *block `json:"currentBlock,omitempty"`
|
||||
}
|
||||
|
||||
type TxPoolMessage struct {
|
||||
|
||||
+129
-151
@@ -18,6 +18,7 @@ package dashboard
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -28,9 +29,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
eventBufferLimit = 128 // Maximum number of buffered peer events.
|
||||
knownPeerLimit = 100 // Maximum number of stored peers, which successfully made the handshake.
|
||||
attemptLimit = 200 // Maximum number of stored peers, which failed to make the handshake.
|
||||
knownPeerLimit = 100 // Maximum number of stored peers, which successfully made the handshake.
|
||||
|
||||
// eventLimit is the maximum number of the dashboard's custom peer events,
|
||||
// that are collected between two metering period and sent to the clients
|
||||
@@ -83,14 +82,6 @@ type peerContainer struct {
|
||||
// inactivePeers contains the peers with closed connection in chronological order.
|
||||
inactivePeers *list.List
|
||||
|
||||
// attemptOrder is the super array containing the IP addresses, from which
|
||||
// the peers attempted to connect then failed before/during the handshake.
|
||||
// Its values are appended in chronological order, which means that the
|
||||
// oldest attempt is at the beginning of the array. When the first element
|
||||
// is removed, the first element of the related bundle's attempt array is
|
||||
// removed too, ensuring that always the latest attempts are stored.
|
||||
attemptOrder []string
|
||||
|
||||
// geodb is the geoip database used to retrieve the peers' geographical location.
|
||||
geodb *geoDB
|
||||
}
|
||||
@@ -100,7 +91,6 @@ func newPeerContainer(geodb *geoDB) *peerContainer {
|
||||
return &peerContainer{
|
||||
Bundles: make(map[string]*peerBundle),
|
||||
inactivePeers: list.New(),
|
||||
attemptOrder: make([]string, 0, attemptLimit),
|
||||
geodb: geodb,
|
||||
}
|
||||
}
|
||||
@@ -110,48 +100,62 @@ func newPeerContainer(geodb *geoDB) *peerContainer {
|
||||
// the IP address from the database and creates a corresponding peer event.
|
||||
// Returns the bundle belonging to the given IP and the events occurring during
|
||||
// the initialization.
|
||||
func (pc *peerContainer) bundle(ip string) (*peerBundle, []*peerEvent) {
|
||||
func (pc *peerContainer) bundle(addr string) (*peerBundle, []*peerEvent) {
|
||||
var events []*peerEvent
|
||||
if _, ok := pc.Bundles[ip]; !ok {
|
||||
location := pc.geodb.location(ip)
|
||||
if _, ok := pc.Bundles[addr]; !ok {
|
||||
i := strings.IndexByte(addr, ':')
|
||||
if i < 0 {
|
||||
i = len(addr)
|
||||
}
|
||||
location := pc.geodb.location(addr[:i])
|
||||
events = append(events, &peerEvent{
|
||||
IP: ip,
|
||||
Addr: addr,
|
||||
Location: location,
|
||||
})
|
||||
pc.Bundles[ip] = &peerBundle{
|
||||
pc.Bundles[addr] = &peerBundle{
|
||||
Location: location,
|
||||
KnownPeers: make(map[string]*knownPeer),
|
||||
}
|
||||
}
|
||||
return pc.Bundles[ip], events
|
||||
return pc.Bundles[addr], events
|
||||
}
|
||||
|
||||
// extendKnown handles the events of the successfully connected peers.
|
||||
// Returns the events occurring during the extension.
|
||||
func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
|
||||
bundle, events := pc.bundle(event.IP)
|
||||
peer, peerEvents := bundle.knownPeer(event.IP, event.ID)
|
||||
bundle, events := pc.bundle(event.Addr)
|
||||
peer, peerEvents := bundle.knownPeer(event.Addr, event.Enode)
|
||||
events = append(events, peerEvents...)
|
||||
// Append the connect and the disconnect events to
|
||||
// the corresponding arrays keeping the limit.
|
||||
switch {
|
||||
case event.Connected != nil:
|
||||
case event.Connected != nil: // Handshake succeeded
|
||||
peer.Connected = append(peer.Connected, event.Connected)
|
||||
if first := len(peer.Connected) - sampleLimit; first > 0 {
|
||||
peer.Connected = peer.Connected[first:]
|
||||
}
|
||||
if event.peer == nil {
|
||||
log.Warn("Peer handshake succeeded event without peer instance", "addr", event.Addr, "enode", event.Enode)
|
||||
}
|
||||
peer.peer = event.peer
|
||||
info := event.peer.Info()
|
||||
peer.Name = info.Name
|
||||
peer.Protocols = info.Protocols
|
||||
peer.Active = true
|
||||
events = append(events, &peerEvent{
|
||||
Activity: Active,
|
||||
IP: peer.ip,
|
||||
ID: peer.id,
|
||||
})
|
||||
e := &peerEvent{
|
||||
Activity: Active,
|
||||
Name: info.Name,
|
||||
Addr: peer.addr,
|
||||
Enode: peer.enode,
|
||||
Protocols: peer.Protocols,
|
||||
}
|
||||
events = append(events, e)
|
||||
pc.activeCount++
|
||||
if peer.listElement != nil {
|
||||
_ = pc.inactivePeers.Remove(peer.listElement)
|
||||
peer.listElement = nil
|
||||
}
|
||||
case event.Disconnected != nil:
|
||||
case event.Disconnected != nil: // Peer disconnected
|
||||
peer.Disconnected = append(peer.Disconnected, event.Disconnected)
|
||||
if first := len(peer.Disconnected) - sampleLimit; first > 0 {
|
||||
peer.Disconnected = peer.Disconnected[first:]
|
||||
@@ -159,8 +163,8 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
|
||||
peer.Active = false
|
||||
events = append(events, &peerEvent{
|
||||
Activity: Inactive,
|
||||
IP: peer.ip,
|
||||
ID: peer.id,
|
||||
Addr: peer.addr,
|
||||
Enode: peer.enode,
|
||||
})
|
||||
pc.activeCount--
|
||||
if peer.listElement != nil {
|
||||
@@ -169,12 +173,14 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
|
||||
}
|
||||
// Insert the peer into the list.
|
||||
peer.listElement = pc.inactivePeers.PushBack(peer)
|
||||
default:
|
||||
log.Warn("Unexpected known peer event", "event", *event)
|
||||
}
|
||||
for pc.inactivePeers.Len() > 0 && pc.activeCount+pc.inactivePeers.Len() > knownPeerLimit {
|
||||
// While the count of the known peers is greater than the limit,
|
||||
// remove the first element from the inactive peer list and from the map.
|
||||
if removedPeer, ok := pc.inactivePeers.Remove(pc.inactivePeers.Front()).(*knownPeer); ok {
|
||||
events = append(events, pc.removeKnown(removedPeer.ip, removedPeer.id)...)
|
||||
events = append(events, pc.removeKnown(removedPeer.addr, removedPeer.enode)...)
|
||||
} else {
|
||||
log.Warn("Failed to parse the removed peer")
|
||||
}
|
||||
@@ -185,25 +191,6 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
|
||||
return events
|
||||
}
|
||||
|
||||
// handleAttempt handles the events of the peers failing before/during the handshake.
|
||||
// Returns the events occurring during the extension.
|
||||
func (pc *peerContainer) handleAttempt(event *peerEvent) []*peerEvent {
|
||||
bundle, events := pc.bundle(event.IP)
|
||||
bundle.Attempts = append(bundle.Attempts, &peerAttempt{
|
||||
Connected: *event.Connected,
|
||||
Disconnected: *event.Disconnected,
|
||||
})
|
||||
pc.attemptOrder = append(pc.attemptOrder, event.IP)
|
||||
for len(pc.attemptOrder) > attemptLimit {
|
||||
// While the length of the connection attempt order array is greater
|
||||
// than the limit, remove the first element from the involved peer's
|
||||
// array and also from the super array.
|
||||
events = append(events, pc.removeAttempt(pc.attemptOrder[0])...)
|
||||
pc.attemptOrder = pc.attemptOrder[1:]
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// peerBundle contains the peers belonging to a given IP address.
|
||||
type peerBundle struct {
|
||||
// Location contains the geographical location based on the bundle's IP address.
|
||||
@@ -213,57 +200,35 @@ type peerBundle struct {
|
||||
// maintainer data structure using the node ID as key.
|
||||
KnownPeers map[string]*knownPeer `json:"knownPeers,omitempty"`
|
||||
|
||||
// Attempts contains the failed connection attempts of the
|
||||
// peers belonging to a given IP address in chronological order.
|
||||
Attempts []*peerAttempt `json:"attempts,omitempty"`
|
||||
// Attempts contains the count of the failed connection
|
||||
// attempts of the peers belonging to a given IP address.
|
||||
Attempts uint `json:"attempts,omitempty"`
|
||||
}
|
||||
|
||||
// removeKnown removes the known peer belonging to the
|
||||
// given IP address and node ID from the peer tree.
|
||||
func (pc *peerContainer) removeKnown(ip, id string) (events []*peerEvent) {
|
||||
func (pc *peerContainer) removeKnown(addr, enode string) (events []*peerEvent) {
|
||||
// TODO (kurkomisi): Remove peers that don't have traffic samples anymore.
|
||||
if bundle, ok := pc.Bundles[ip]; ok {
|
||||
if _, ok := bundle.KnownPeers[id]; ok {
|
||||
if bundle, ok := pc.Bundles[addr]; ok {
|
||||
if _, ok := bundle.KnownPeers[enode]; ok {
|
||||
events = append(events, &peerEvent{
|
||||
Remove: RemoveKnown,
|
||||
IP: ip,
|
||||
ID: id,
|
||||
Addr: addr,
|
||||
Enode: enode,
|
||||
})
|
||||
delete(bundle.KnownPeers, id)
|
||||
delete(bundle.KnownPeers, enode)
|
||||
} else {
|
||||
log.Warn("No peer to remove", "ip", ip, "id", id)
|
||||
log.Warn("No peer to remove", "addr", addr, "enode", enode)
|
||||
}
|
||||
if len(bundle.KnownPeers) < 1 && len(bundle.Attempts) < 1 {
|
||||
if len(bundle.KnownPeers) < 1 && bundle.Attempts < 1 {
|
||||
events = append(events, &peerEvent{
|
||||
Remove: RemoveBundle,
|
||||
IP: ip,
|
||||
Addr: addr,
|
||||
})
|
||||
delete(pc.Bundles, ip)
|
||||
delete(pc.Bundles, addr)
|
||||
}
|
||||
} else {
|
||||
log.Warn("No bundle to remove", "ip", ip)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// removeAttempt removes the peer attempt belonging to the
|
||||
// given IP address and node ID from the peer tree.
|
||||
func (pc *peerContainer) removeAttempt(ip string) (events []*peerEvent) {
|
||||
if bundle, ok := pc.Bundles[ip]; ok {
|
||||
if len(bundle.Attempts) > 0 {
|
||||
events = append(events, &peerEvent{
|
||||
Remove: RemoveAttempt,
|
||||
IP: ip,
|
||||
})
|
||||
bundle.Attempts = bundle.Attempts[1:]
|
||||
}
|
||||
if len(bundle.Attempts) < 1 && len(bundle.KnownPeers) < 1 {
|
||||
events = append(events, &peerEvent{
|
||||
Remove: RemoveBundle,
|
||||
IP: ip,
|
||||
})
|
||||
delete(pc.Bundles, ip)
|
||||
}
|
||||
log.Warn("No bundle to remove", "addr", addr)
|
||||
}
|
||||
return events
|
||||
}
|
||||
@@ -272,26 +237,25 @@ func (pc *peerContainer) removeAttempt(ip string) (events []*peerEvent) {
|
||||
// to the given IP address and node ID wasn't metered so far. Returns the peer
|
||||
// belonging to the given IP and ID as well as the events occurring during the
|
||||
// initialization.
|
||||
func (bundle *peerBundle) knownPeer(ip, id string) (*knownPeer, []*peerEvent) {
|
||||
func (bundle *peerBundle) knownPeer(addr, enode string) (*knownPeer, []*peerEvent) {
|
||||
var events []*peerEvent
|
||||
if _, ok := bundle.KnownPeers[id]; !ok {
|
||||
now := time.Now()
|
||||
ingress := emptyChartEntries(now, sampleLimit)
|
||||
egress := emptyChartEntries(now, sampleLimit)
|
||||
if _, ok := bundle.KnownPeers[enode]; !ok {
|
||||
ingress := emptyChartEntries(sampleLimit)
|
||||
egress := emptyChartEntries(sampleLimit)
|
||||
events = append(events, &peerEvent{
|
||||
IP: ip,
|
||||
ID: id,
|
||||
Addr: addr,
|
||||
Enode: enode,
|
||||
Ingress: append([]*ChartEntry{}, ingress...),
|
||||
Egress: append([]*ChartEntry{}, egress...),
|
||||
})
|
||||
bundle.KnownPeers[id] = &knownPeer{
|
||||
ip: ip,
|
||||
id: id,
|
||||
bundle.KnownPeers[enode] = &knownPeer{
|
||||
addr: addr,
|
||||
enode: enode,
|
||||
Ingress: ingress,
|
||||
Egress: egress,
|
||||
}
|
||||
}
|
||||
return bundle.KnownPeers[id], events
|
||||
return bundle.KnownPeers[enode], events
|
||||
}
|
||||
|
||||
// knownPeer contains the metered data of a particular peer.
|
||||
@@ -312,31 +276,26 @@ type knownPeer struct {
|
||||
Ingress ChartEntries `json:"ingress,omitempty"`
|
||||
Egress ChartEntries `json:"egress,omitempty"`
|
||||
|
||||
Name string `json:"name,omitempty"` // Name of the node, including client type, version, OS, custom data
|
||||
Enode string `json:"enode,omitempty"` // Node URL
|
||||
Protocols map[string]interface{} `json:"protocols,omitempty"` // Sub-protocol specific metadata fields
|
||||
|
||||
Active bool `json:"active"` // Denotes if the peer is still connected.
|
||||
|
||||
listElement *list.Element // Pointer to the peer element in the list.
|
||||
ip, id string // The IP and the ID by which the peer can be accessed in the tree.
|
||||
addr, enode string // The IP and the ID by which the peer can be accessed in the tree.
|
||||
prevIngress float64
|
||||
prevEgress float64
|
||||
}
|
||||
|
||||
// peerAttempt contains a failed peer connection attempt's attributes.
|
||||
type peerAttempt struct {
|
||||
// Connected contains the timestamp of the connection attempt's moment.
|
||||
Connected time.Time `json:"connected"`
|
||||
|
||||
// Disconnected contains the timestamp of the
|
||||
// moment when the connection attempt failed.
|
||||
Disconnected time.Time `json:"disconnected"`
|
||||
peer *p2p.Peer // Connected remote node instance
|
||||
}
|
||||
|
||||
type RemovedPeerType string
|
||||
type ActivityType string
|
||||
|
||||
const (
|
||||
RemoveKnown RemovedPeerType = "known"
|
||||
RemoveAttempt RemovedPeerType = "attempt"
|
||||
RemoveBundle RemovedPeerType = "bundle"
|
||||
RemoveKnown RemovedPeerType = "known"
|
||||
RemoveBundle RemovedPeerType = "bundle"
|
||||
|
||||
Active ActivityType = "active"
|
||||
Inactive ActivityType = "inactive"
|
||||
@@ -344,15 +303,19 @@ const (
|
||||
|
||||
// peerEvent contains the attributes of a peer event.
|
||||
type peerEvent struct {
|
||||
IP string `json:"ip,omitempty"` // IP address of the peer.
|
||||
ID string `json:"id,omitempty"` // Node ID of the peer.
|
||||
Remove RemovedPeerType `json:"remove,omitempty"` // Type of the peer that is to be removed.
|
||||
Location *geoLocation `json:"location,omitempty"` // Geographical location of the peer.
|
||||
Connected *time.Time `json:"connected,omitempty"` // Timestamp of the connection moment.
|
||||
Disconnected *time.Time `json:"disconnected,omitempty"` // Timestamp of the disonnection moment.
|
||||
Ingress ChartEntries `json:"ingress,omitempty"` // Ingress samples.
|
||||
Egress ChartEntries `json:"egress,omitempty"` // Egress samples.
|
||||
Activity ActivityType `json:"activity,omitempty"` // Connection status change.
|
||||
Name string `json:"name,omitempty"` // Name of the node, including client type, version, OS, custom data
|
||||
Addr string `json:"addr,omitempty"` // TCP address of the peer.
|
||||
Enode string `json:"enode,omitempty"` // Node URL
|
||||
Protocols map[string]interface{} `json:"protocols,omitempty"` // Sub-protocol specific metadata fields
|
||||
Remove RemovedPeerType `json:"remove,omitempty"` // Type of the peer that is to be removed.
|
||||
Location *geoLocation `json:"location,omitempty"` // Geographical location of the peer.
|
||||
Connected *time.Time `json:"connected,omitempty"` // Timestamp of the connection moment.
|
||||
Disconnected *time.Time `json:"disconnected,omitempty"` // Timestamp of the disonnection moment.
|
||||
Ingress ChartEntries `json:"ingress,omitempty"` // Ingress samples.
|
||||
Egress ChartEntries `json:"egress,omitempty"` // Egress samples.
|
||||
Activity ActivityType `json:"activity,omitempty"` // Connection status change.
|
||||
|
||||
peer *p2p.Peer // Connected remote node instance.
|
||||
}
|
||||
|
||||
// trafficMap is a container for the periodically collected peer traffic.
|
||||
@@ -376,14 +339,12 @@ func (db *Dashboard) collectPeerData() {
|
||||
db.geodb, err = openGeoDB()
|
||||
if err != nil {
|
||||
log.Warn("Failed to open geodb", "err", err)
|
||||
errc := <-db.quit
|
||||
errc <- nil
|
||||
return
|
||||
}
|
||||
defer db.geodb.close()
|
||||
|
||||
peerCh := make(chan p2p.MeteredPeerEvent, eventBufferLimit) // Peer event channel.
|
||||
subPeer := p2p.SubscribeMeteredPeerEvent(peerCh) // Subscribe to peer events.
|
||||
defer subPeer.Unsubscribe() // Unsubscribe at the end.
|
||||
|
||||
ticker := time.NewTicker(db.config.Refresh)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -400,11 +361,11 @@ func (db *Dashboard) collectPeerData() {
|
||||
// The function which can be passed to the registry.
|
||||
return func(name string, i interface{}) {
|
||||
if m, ok := i.(metrics.Meter); ok {
|
||||
// The name of the meter has the format: <common traffic prefix><IP>/<ID>
|
||||
if k := strings.Split(strings.TrimPrefix(name, prefix), "/"); len(k) == 2 {
|
||||
traffic.insert(k[0], k[1], float64(m.Count()))
|
||||
enode := strings.TrimPrefix(name, prefix)
|
||||
if addr := strings.Split(enode, "@"); len(addr) == 2 {
|
||||
traffic.insert(addr[1], enode, float64(m.Count()))
|
||||
} else {
|
||||
log.Warn("Invalid meter name", "name", name, "prefix", prefix)
|
||||
log.Warn("Invalid enode", "enode", enode)
|
||||
}
|
||||
} else {
|
||||
log.Warn("Invalid meter type", "name", name)
|
||||
@@ -428,23 +389,32 @@ func (db *Dashboard) collectPeerData() {
|
||||
ingress, egress := new(trafficMap), new(trafficMap)
|
||||
*ingress, *egress = make(trafficMap), make(trafficMap)
|
||||
|
||||
defer db.subPeer.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case event := <-peerCh:
|
||||
case event := <-db.peerCh:
|
||||
now := time.Now()
|
||||
switch event.Type {
|
||||
case p2p.PeerConnected:
|
||||
case p2p.PeerHandshakeFailed:
|
||||
connected := now.Add(-event.Elapsed)
|
||||
newPeerEvents = append(newPeerEvents, &peerEvent{
|
||||
IP: event.IP.String(),
|
||||
ID: event.ID.String(),
|
||||
Addr: event.Addr,
|
||||
Connected: &connected,
|
||||
Disconnected: &now,
|
||||
})
|
||||
case p2p.PeerHandshakeSucceeded:
|
||||
connected := now.Add(-event.Elapsed)
|
||||
newPeerEvents = append(newPeerEvents, &peerEvent{
|
||||
Addr: event.Addr,
|
||||
Enode: event.Peer.Node().String(),
|
||||
peer: event.Peer,
|
||||
Connected: &connected,
|
||||
})
|
||||
case p2p.PeerDisconnected:
|
||||
ip, id := event.IP.String(), event.ID.String()
|
||||
addr, enode := event.Addr, event.Peer.Node().String()
|
||||
newPeerEvents = append(newPeerEvents, &peerEvent{
|
||||
IP: ip,
|
||||
ID: id,
|
||||
Addr: addr,
|
||||
Enode: enode,
|
||||
Disconnected: &now,
|
||||
})
|
||||
// The disconnect event comes with the last metered traffic count,
|
||||
@@ -453,15 +423,8 @@ func (db *Dashboard) collectPeerData() {
|
||||
// period the same peer disconnects multiple times, and appending
|
||||
// all the samples to the traffic arrays would shift the metering,
|
||||
// so only the last metering is stored, overwriting the previous one.
|
||||
ingress.insert(ip, id, float64(event.Ingress))
|
||||
egress.insert(ip, id, float64(event.Egress))
|
||||
case p2p.PeerHandshakeFailed:
|
||||
connected := now.Add(-event.Elapsed)
|
||||
newPeerEvents = append(newPeerEvents, &peerEvent{
|
||||
IP: event.IP.String(),
|
||||
Connected: &connected,
|
||||
Disconnected: &now,
|
||||
})
|
||||
ingress.insert(addr, enode, float64(event.Ingress))
|
||||
egress.insert(addr, enode, float64(event.Egress))
|
||||
default:
|
||||
log.Error("Unknown metered peer event type", "type", event.Type)
|
||||
}
|
||||
@@ -475,7 +438,7 @@ func (db *Dashboard) collectPeerData() {
|
||||
|
||||
var diff []*peerEvent
|
||||
for i := 0; i < len(newPeerEvents); i++ {
|
||||
if newPeerEvents[i].IP == "" {
|
||||
if newPeerEvents[i].Addr == "" {
|
||||
log.Warn("Peer event without IP", "event", *newPeerEvents[i])
|
||||
continue
|
||||
}
|
||||
@@ -487,18 +450,20 @@ func (db *Dashboard) collectPeerData() {
|
||||
//
|
||||
// The extension can produce additional peer events, such
|
||||
// as remove, location and initial samples events.
|
||||
if newPeerEvents[i].ID == "" {
|
||||
diff = append(diff, peers.handleAttempt(newPeerEvents[i])...)
|
||||
if newPeerEvents[i].Enode == "" {
|
||||
bundle, events := peers.bundle(newPeerEvents[i].Addr)
|
||||
bundle.Attempts++
|
||||
diff = append(diff, events...)
|
||||
continue
|
||||
}
|
||||
diff = append(diff, peers.extendKnown(newPeerEvents[i])...)
|
||||
}
|
||||
// Update the peer tree using the traffic maps.
|
||||
for ip, bundle := range peers.Bundles {
|
||||
for id, peer := range bundle.KnownPeers {
|
||||
for addr, bundle := range peers.Bundles {
|
||||
for enode, peer := range bundle.KnownPeers {
|
||||
// Value is 0 if the traffic map doesn't have the
|
||||
// entry corresponding to the given IP and ID.
|
||||
curIngress, curEgress := (*ingress)[ip][id], (*egress)[ip][id]
|
||||
curIngress, curEgress := (*ingress)[addr][enode], (*egress)[addr][enode]
|
||||
deltaIngress, deltaEgress := curIngress, curEgress
|
||||
if deltaIngress >= peer.prevIngress {
|
||||
deltaIngress -= peer.prevIngress
|
||||
@@ -523,11 +488,22 @@ func (db *Dashboard) collectPeerData() {
|
||||
}
|
||||
// Creating the traffic sample events.
|
||||
diff = append(diff, &peerEvent{
|
||||
IP: ip,
|
||||
ID: id,
|
||||
Addr: addr,
|
||||
Enode: enode,
|
||||
Ingress: ChartEntries{i},
|
||||
Egress: ChartEntries{e},
|
||||
})
|
||||
if peer.peer != nil {
|
||||
info := peer.peer.Info()
|
||||
if !reflect.DeepEqual(peer.Protocols, info.Protocols) {
|
||||
peer.Protocols = info.Protocols
|
||||
diff = append(diff, &peerEvent{
|
||||
Addr: addr,
|
||||
Enode: enode,
|
||||
Protocols: peer.Protocols,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
db.peerLock.Unlock()
|
||||
@@ -541,8 +517,10 @@ func (db *Dashboard) collectPeerData() {
|
||||
// prepare them for the next metering.
|
||||
*ingress, *egress = make(trafficMap), make(trafficMap)
|
||||
newPeerEvents = newPeerEvents[:0]
|
||||
case err := <-subPeer.Err():
|
||||
case err := <-db.subPeer.Err():
|
||||
log.Warn("Peer subscription error", "err", err)
|
||||
errc := <-db.quit
|
||||
errc <- nil
|
||||
return
|
||||
case errc := <-db.quit:
|
||||
errc <- nil
|
||||
|
||||
+1
-1
@@ -304,7 +304,7 @@ func (t *dialTask) dial(srv *Server, dest *enode.Node) error {
|
||||
if err != nil {
|
||||
return &dialError{err}
|
||||
}
|
||||
mfd := newMeteredConn(fd, false, dest.IP())
|
||||
mfd := newMeteredConn(fd, false, &net.TCPAddr{IP: dest.IP(), Port: dest.TCP()})
|
||||
return srv.SetupConn(mfd, t.flags, dest)
|
||||
}
|
||||
|
||||
|
||||
+37
-43
@@ -19,7 +19,6 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -28,7 +27,6 @@ import (
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -58,24 +56,24 @@ var (
|
||||
type MeteredPeerEventType int
|
||||
|
||||
const (
|
||||
// PeerConnected is the type of event emitted when a peer successfully
|
||||
// made the handshake.
|
||||
PeerConnected MeteredPeerEventType = iota
|
||||
// PeerHandshakeSucceeded is the type of event
|
||||
// emitted when a peer successfully makes the handshake.
|
||||
PeerHandshakeSucceeded MeteredPeerEventType = iota
|
||||
|
||||
// PeerHandshakeFailed is the type of event emitted when a peer fails to
|
||||
// make the handshake or disconnects before it.
|
||||
PeerHandshakeFailed
|
||||
|
||||
// PeerDisconnected is the type of event emitted when a peer disconnects.
|
||||
PeerDisconnected
|
||||
|
||||
// PeerHandshakeFailed is the type of event emitted when a peer fails to
|
||||
// make the handshake or disconnects before the handshake.
|
||||
PeerHandshakeFailed
|
||||
)
|
||||
|
||||
// MeteredPeerEvent is an event emitted when peers connect or disconnect.
|
||||
type MeteredPeerEvent struct {
|
||||
Type MeteredPeerEventType // Type of peer event
|
||||
IP net.IP // IP address of the peer
|
||||
ID enode.ID // NodeID of the peer
|
||||
Addr string // TCP address of the peer
|
||||
Elapsed time.Duration // Time elapsed between the connection and the handshake/disconnection
|
||||
Peer *Peer // Connected remote node instance
|
||||
Ingress uint64 // Ingress count at the moment of the event
|
||||
Egress uint64 // Egress count at the moment of the event
|
||||
}
|
||||
@@ -91,9 +89,9 @@ func SubscribeMeteredPeerEvent(ch chan<- MeteredPeerEvent) event.Subscription {
|
||||
type meteredConn struct {
|
||||
net.Conn // Network connection to wrap with metering
|
||||
|
||||
connected time.Time // Connection time of the peer
|
||||
ip net.IP // IP address of the peer
|
||||
id enode.ID // NodeID of the peer
|
||||
connected time.Time // Connection time of the peer
|
||||
addr *net.TCPAddr // TCP address of the peer
|
||||
peer *Peer // Peer instance
|
||||
|
||||
// trafficMetered denotes if the peer is registered in the traffic registries.
|
||||
// Its value is true if the metered peer count doesn't reach the limit in the
|
||||
@@ -109,13 +107,13 @@ type meteredConn struct {
|
||||
// connection meter and also increases the metered peer count. If the metrics
|
||||
// system is disabled or the IP address is unspecified, this function returns
|
||||
// the original object.
|
||||
func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn {
|
||||
func newMeteredConn(conn net.Conn, ingress bool, addr *net.TCPAddr) net.Conn {
|
||||
// Short circuit if metrics are disabled
|
||||
if !metrics.Enabled {
|
||||
return conn
|
||||
}
|
||||
if ip.IsUnspecified() {
|
||||
log.Warn("Peer IP is unspecified")
|
||||
if addr == nil || addr.IP.IsUnspecified() {
|
||||
log.Warn("Peer address is unspecified")
|
||||
return conn
|
||||
}
|
||||
// Bump the connection counters and wrap the connection
|
||||
@@ -128,7 +126,7 @@ func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn {
|
||||
|
||||
return &meteredConn{
|
||||
Conn: conn,
|
||||
ip: ip,
|
||||
addr: addr,
|
||||
connected: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -159,30 +157,27 @@ func (c *meteredConn) Write(b []byte) (n int, err error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// handshakeDone is called when a peer handshake is done. Registers the peer to
|
||||
// the ingress and the egress traffic registries using the peer's IP and node ID,
|
||||
// also emits connect event.
|
||||
func (c *meteredConn) handshakeDone(id enode.ID) {
|
||||
// TODO (kurkomisi): use the node URL instead of the pure node ID. (the String() method of *Node)
|
||||
// handshakeDone is called after the connection passes the handshake.
|
||||
func (c *meteredConn) handshakeDone(peer *Peer) {
|
||||
if atomic.AddInt32(&meteredPeerCount, 1) >= MeteredPeerLimit {
|
||||
// Don't register the peer in the traffic registries.
|
||||
atomic.AddInt32(&meteredPeerCount, -1)
|
||||
c.lock.Lock()
|
||||
c.id, c.trafficMetered = id, false
|
||||
c.peer, c.trafficMetered = peer, false
|
||||
c.lock.Unlock()
|
||||
log.Warn("Metered peer count reached the limit")
|
||||
} else {
|
||||
key := fmt.Sprintf("%s/%s", c.ip, id.String())
|
||||
enode := peer.Node().String()
|
||||
c.lock.Lock()
|
||||
c.id, c.trafficMetered = id, true
|
||||
c.ingressMeter = metrics.NewRegisteredMeter(key, PeerIngressRegistry)
|
||||
c.egressMeter = metrics.NewRegisteredMeter(key, PeerEgressRegistry)
|
||||
c.peer, c.trafficMetered = peer, true
|
||||
c.ingressMeter = metrics.NewRegisteredMeter(enode, PeerIngressRegistry)
|
||||
c.egressMeter = metrics.NewRegisteredMeter(enode, PeerEgressRegistry)
|
||||
c.lock.Unlock()
|
||||
}
|
||||
meteredPeerFeed.Send(MeteredPeerEvent{
|
||||
Type: PeerConnected,
|
||||
IP: c.ip,
|
||||
ID: id,
|
||||
Type: PeerHandshakeSucceeded,
|
||||
Addr: c.addr.String(),
|
||||
Peer: peer,
|
||||
Elapsed: time.Since(c.connected),
|
||||
})
|
||||
}
|
||||
@@ -192,44 +187,43 @@ func (c *meteredConn) handshakeDone(id enode.ID) {
|
||||
func (c *meteredConn) Close() error {
|
||||
err := c.Conn.Close()
|
||||
c.lock.RLock()
|
||||
if c.id == (enode.ID{}) {
|
||||
// If the peer disconnects before the handshake.
|
||||
if c.peer == nil {
|
||||
// If the peer disconnects before/during the handshake.
|
||||
c.lock.RUnlock()
|
||||
meteredPeerFeed.Send(MeteredPeerEvent{
|
||||
Type: PeerHandshakeFailed,
|
||||
IP: c.ip,
|
||||
Addr: c.addr.String(),
|
||||
Elapsed: time.Since(c.connected),
|
||||
})
|
||||
activePeerGauge.Dec(1)
|
||||
return err
|
||||
}
|
||||
id := c.id
|
||||
peer := c.peer
|
||||
if !c.trafficMetered {
|
||||
// If the peer isn't registered in the traffic registries.
|
||||
c.lock.RUnlock()
|
||||
meteredPeerFeed.Send(MeteredPeerEvent{
|
||||
Type: PeerDisconnected,
|
||||
IP: c.ip,
|
||||
ID: id,
|
||||
Addr: c.addr.String(),
|
||||
Peer: peer,
|
||||
})
|
||||
activePeerGauge.Dec(1)
|
||||
return err
|
||||
}
|
||||
ingress, egress := uint64(c.ingressMeter.Count()), uint64(c.egressMeter.Count())
|
||||
ingress, egress, enode := uint64(c.ingressMeter.Count()), uint64(c.egressMeter.Count()), c.peer.Node().String()
|
||||
c.lock.RUnlock()
|
||||
|
||||
// Decrement the metered peer count
|
||||
atomic.AddInt32(&meteredPeerCount, -1)
|
||||
|
||||
// Unregister the peer from the traffic registries
|
||||
key := fmt.Sprintf("%s/%s", c.ip, id)
|
||||
PeerIngressRegistry.Unregister(key)
|
||||
PeerEgressRegistry.Unregister(key)
|
||||
PeerIngressRegistry.Unregister(enode)
|
||||
PeerEgressRegistry.Unregister(enode)
|
||||
|
||||
meteredPeerFeed.Send(MeteredPeerEvent{
|
||||
Type: PeerDisconnected,
|
||||
IP: c.ip,
|
||||
ID: id,
|
||||
Addr: c.addr.String(),
|
||||
Peer: peer,
|
||||
Ingress: ingress,
|
||||
Egress: egress,
|
||||
})
|
||||
|
||||
+9
-5
@@ -779,6 +779,9 @@ running:
|
||||
if p.Inbound() {
|
||||
inboundCount++
|
||||
}
|
||||
if conn, ok := c.fd.(*meteredConn); ok {
|
||||
conn.handshakeDone(p)
|
||||
}
|
||||
}
|
||||
// The dialer logic relies on the assumption that
|
||||
// dial tasks complete after the peer has been added or
|
||||
@@ -902,9 +905,13 @@ func (srv *Server) listenLoop() {
|
||||
continue
|
||||
}
|
||||
if remoteIP != nil {
|
||||
fd = newMeteredConn(fd, true, remoteIP)
|
||||
var addr *net.TCPAddr
|
||||
if tcp, ok := fd.RemoteAddr().(*net.TCPAddr); ok {
|
||||
addr = tcp
|
||||
}
|
||||
fd = newMeteredConn(fd, true, addr)
|
||||
srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr())
|
||||
}
|
||||
srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr())
|
||||
go func() {
|
||||
srv.SetupConn(fd, inboundConn, nil)
|
||||
slots <- struct{}{}
|
||||
@@ -974,9 +981,6 @@ func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *enode.Node) erro
|
||||
} else {
|
||||
c.node = nodeFromConn(remotePubkey, c.fd)
|
||||
}
|
||||
if conn, ok := c.fd.(*meteredConn); ok {
|
||||
conn.handshakeDone(c.node.ID())
|
||||
}
|
||||
clog := srv.log.New("id", c.node.ID(), "addr", c.fd.RemoteAddr(), "conn", c.flags)
|
||||
err = srv.checkpoint(c, srv.checkpointPostHandshake)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user