mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 02:12:23 +00:00
dashboard, vendor: first fully working version (without rebase)
This commit is contained in:
parent
9a1ee019d1
commit
5efc9b99d1
14 changed files with 2298 additions and 1850 deletions
3097
dashboard/assets.go
3097
dashboard/assets.go
File diff suppressed because one or more lines are too long
|
|
@ -68,4 +68,4 @@ export const styles = {
|
|||
light: {
|
||||
color: 'rgba(255, 255, 255, 0.54)',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,13 +32,12 @@ const styles = {
|
|||
};
|
||||
|
||||
export type Props = {
|
||||
opened: boolean,
|
||||
opened: boolean,
|
||||
changeContent: string => void,
|
||||
active: string,
|
||||
content: Content,
|
||||
shouldUpdate: Object,
|
||||
send: (string) => void,
|
||||
logs: () => Object,
|
||||
active: string,
|
||||
content: Content,
|
||||
shouldUpdate: Object,
|
||||
send: string => void,
|
||||
};
|
||||
|
||||
// Body renders the body of the dashboard.
|
||||
|
|
@ -55,7 +54,6 @@ class Body extends Component<Props> {
|
|||
content={this.props.content}
|
||||
shouldUpdate={this.props.shouldUpdate}
|
||||
send={this.props.send}
|
||||
logs={this.props.logs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export type Props = {
|
|||
class CustomTooltip extends Component<Props> {
|
||||
render() {
|
||||
const {active, payload, tooltip} = this.props;
|
||||
if (!active || typeof tooltip !== 'function') {
|
||||
if (!active || typeof tooltip !== 'function' || !Array.isArray(payload) || payload.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return tooltip(payload[0].value);
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import List, {ListItem} from 'material-ui/List';
|
||||
import withStyles from 'material-ui/styles/withStyles';
|
||||
|
||||
import Header from './Header';
|
||||
import Body from './Body';
|
||||
import {MENU} from '../common';
|
||||
import type {Content, Record, Chunk} from '../types/content';
|
||||
import type {Content} from '../types/content';
|
||||
import {inserter as logInserter} from './Logs';
|
||||
|
||||
// deepUpdate updates an object corresponding to the given update data, which has
|
||||
// the shape of the same structure as the original object. updater also has the same
|
||||
|
|
@ -76,104 +76,6 @@ const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, pre
|
|||
...update.map(sample => mapper(sample)),
|
||||
].slice(-limit);
|
||||
|
||||
// fieldPadding is a global map with maximum field value lengths seen until now
|
||||
// to allow padding log contexts in a bit smarter way.
|
||||
const fieldPadding = new Map();
|
||||
|
||||
// createLogChunk creates an HTML formatted object, which displays the given array similarly to
|
||||
// the server side terminal.
|
||||
const createLogChunk = (arr: Array<Record>) => {
|
||||
let content = '';
|
||||
arr.forEach((record) => {
|
||||
let {t, lvl, msg, ctx} = record;
|
||||
let color = '#ce3c23';
|
||||
switch (lvl) {
|
||||
case 'trace':
|
||||
case 'trce':
|
||||
lvl = 'TRACE';
|
||||
color = '#3465a4';
|
||||
break;
|
||||
case 'debug':
|
||||
case 'dbug':
|
||||
lvl = 'DEBUG';
|
||||
color = '#3d989b';
|
||||
break;
|
||||
case 'info':
|
||||
lvl = 'INFO ';
|
||||
color = '#4c8f0f';
|
||||
break;
|
||||
case 'warn':
|
||||
lvl = 'WARN ';
|
||||
color = '#b79a22';
|
||||
break;
|
||||
case 'error':
|
||||
case 'eror':
|
||||
lvl = 'ERROR';
|
||||
color = '#754b70';
|
||||
break;
|
||||
case 'crit':
|
||||
lvl = 'CRIT ';
|
||||
color = '#ce3c23';
|
||||
break;
|
||||
default:
|
||||
lvl = '';
|
||||
}
|
||||
if (lvl === '' || typeof t !== 'string' || t.length < 19 || typeof msg !== 'string' || !Array.isArray(ctx)) {
|
||||
content += `<span style="color:${color}">Invalid log record</span><br />`;
|
||||
return;
|
||||
}
|
||||
if (ctx.length > 0) {
|
||||
msg += ' '.repeat(Math.max(40 - msg.length, 0));
|
||||
}
|
||||
// Time format: 2006-01-02T15:04:05-0700 -> 01-02|15:04:05
|
||||
content += `<span style="color:${color}">${lvl}</span>[${t.substr(5, 5)}|${t.substr(11, 8)}] ${msg}`;
|
||||
|
||||
for (let i = 0; i < ctx.length; i += 2) {
|
||||
const key = ctx[i];
|
||||
const value = ctx[i + 1];
|
||||
let padding = fieldPadding.get(key);
|
||||
if (typeof padding === 'undefined' || padding < value.length) {
|
||||
padding = value.length;
|
||||
fieldPadding.set(key, padding);
|
||||
}
|
||||
content += ` <span style="color:${color}">${key}</span>=${value}${' '.repeat(padding - value.length)}`;
|
||||
}
|
||||
content += '<br />';
|
||||
});
|
||||
return content;
|
||||
};
|
||||
|
||||
// logAppender is a state updater function, which appends the new log chunks to the existing ones.
|
||||
// In case the prev chunk array's last element doesn't have limit number of log record elements,
|
||||
// it will be extended.
|
||||
const logAppender = (limit: number) => (update: Array<Record>, prev: Array<Chunk>) => {
|
||||
const newChunks = [];
|
||||
let first = 0;
|
||||
let last = 0;
|
||||
let extended = 0;
|
||||
if (prev.length > 0 && prev[prev.length - 1].len < limit) {
|
||||
extended = 1;
|
||||
const l = Math.min(limit - prev[prev.length - 1].len, update.length);
|
||||
newChunks.push({
|
||||
content: prev[prev.length - 1].content + createLogChunk(update.slice(0, l)),
|
||||
t: prev[prev.length - 1].t,
|
||||
len: prev[prev.length - 1].len + l,
|
||||
});
|
||||
first = l;
|
||||
last = l;
|
||||
}
|
||||
while (last < update.length) {
|
||||
last = Math.min(update.length, last + limit);
|
||||
newChunks.push({
|
||||
content: createLogChunk(update.slice(first, last)),
|
||||
t: update[first].t,
|
||||
len: last - first,
|
||||
});
|
||||
first += limit;
|
||||
}
|
||||
return [...prev.slice(0, prev.length - extended), ...newChunks];
|
||||
};
|
||||
|
||||
// defaultContent is the initial value of the state content.
|
||||
const defaultContent: Content = {
|
||||
general: {
|
||||
|
|
@ -194,8 +96,12 @@ const defaultContent: Content = {
|
|||
diskRead: [],
|
||||
diskWrite: [],
|
||||
},
|
||||
logs: {
|
||||
chunk: [],
|
||||
logs: {
|
||||
chunks: [],
|
||||
endTop: false,
|
||||
endBottom: true,
|
||||
topChanged: 0,
|
||||
bottomChanged: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -221,9 +127,7 @@ const updaters = {
|
|||
diskRead: appender(200),
|
||||
diskWrite: appender(200),
|
||||
},
|
||||
logs: {
|
||||
chunk: logAppender(50),
|
||||
},
|
||||
logs: logInserter(5),
|
||||
};
|
||||
|
||||
// styles contains the constant styles of the component.
|
||||
|
|
@ -236,10 +140,6 @@ const styles = {
|
|||
zIndex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
logChunk: {
|
||||
color: 'white',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
};
|
||||
|
||||
// themeStyles returns the styles generated from the theme for the component.
|
||||
|
|
@ -254,11 +154,11 @@ export type Props = {
|
|||
};
|
||||
|
||||
type State = {
|
||||
active: string, // active menu
|
||||
sideBar: boolean, // true if the sidebar is opened
|
||||
content: Content, // the visualized data
|
||||
active: string, // active menu
|
||||
sideBar: boolean, // true if the sidebar is opened
|
||||
content: Content, // the visualized data
|
||||
shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message
|
||||
server: ?WebSocket,
|
||||
server: ?WebSocket,
|
||||
};
|
||||
|
||||
// Dashboard is the main component, which renders the whole page, makes connection with the server and
|
||||
|
|
@ -302,7 +202,7 @@ class Dashboard extends Component<Props, State> {
|
|||
};
|
||||
};
|
||||
|
||||
// server can be accessed only through this function for safety reasons.
|
||||
// send sends a message to the server, which can be accessed only through this function for safety reasons.
|
||||
send = (msg: string) => {
|
||||
if (this.state.server != null) {
|
||||
this.state.server.send(msg);
|
||||
|
|
@ -327,18 +227,6 @@ class Dashboard extends Component<Props, State> {
|
|||
this.setState(prevState => ({sideBar: !prevState.sideBar}));
|
||||
};
|
||||
|
||||
// logsHTML visualizes the log chunks. It is more efficient to insert pure HTML into the component, than
|
||||
// to create individual component for each log record and track them all. It also postpones the OOM issue.
|
||||
logsHTML = () => (
|
||||
<List>
|
||||
{this.state.content.logs.chunk.map((c, index) => (
|
||||
<ListItem key={index}>
|
||||
<div style={styles.logChunk} dangerouslySetInnerHTML={{__html: c.content}} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.props.classes.dashboard} style={styles.dashboard}>
|
||||
|
|
@ -352,7 +240,6 @@ class Dashboard extends Component<Props, State> {
|
|||
content={this.state.content}
|
||||
shouldUpdate={this.state.shouldUpdate}
|
||||
send={this.send}
|
||||
logs={this.logsHTML}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,16 +18,274 @@
|
|||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import List, {ListItem} from 'material-ui/List';
|
||||
import type {Record, Content, LogsMessage, Logs as LogsType} from '../types/content';
|
||||
|
||||
// if the scroll position is closer to the top/bottom than this value, the client sends a request for a new log chunk.
|
||||
const requestLimit = 100;
|
||||
|
||||
// fieldPadding is a global map with maximum field value lengths seen until now
|
||||
// to allow padding log contexts in a bit smarter way.
|
||||
const fieldPadding = new Map();
|
||||
|
||||
// createChunk creates an HTML formatted object, which displays the given array similarly to
|
||||
// the server side terminal.
|
||||
const createChunk = (records: Array<Record>) => {
|
||||
let content = '';
|
||||
records.forEach((record) => {
|
||||
const {t, ctx} = record;
|
||||
let {lvl, msg} = record;
|
||||
let color = '#ce3c23';
|
||||
switch (lvl) {
|
||||
case 'trace':
|
||||
case 'trce':
|
||||
lvl = 'TRACE';
|
||||
color = '#3465a4';
|
||||
break;
|
||||
case 'debug':
|
||||
case 'dbug':
|
||||
lvl = 'DEBUG';
|
||||
color = '#3d989b';
|
||||
break;
|
||||
case 'info':
|
||||
lvl = 'INFO ';
|
||||
color = '#4c8f0f';
|
||||
break;
|
||||
case 'warn':
|
||||
lvl = 'WARN ';
|
||||
color = '#b79a22';
|
||||
break;
|
||||
case 'error':
|
||||
case 'eror':
|
||||
lvl = 'ERROR';
|
||||
color = '#754b70';
|
||||
break;
|
||||
case 'crit':
|
||||
lvl = 'CRIT ';
|
||||
color = '#ce3c23';
|
||||
break;
|
||||
default:
|
||||
lvl = '';
|
||||
}
|
||||
if (lvl === '' || typeof t !== 'string' || t.length < 19 || typeof msg !== 'string' || !Array.isArray(ctx)) {
|
||||
content += `<span style="color:${color}">Invalid log record</span><br />`;
|
||||
return;
|
||||
}
|
||||
if (ctx.length > 0) {
|
||||
msg += ' '.repeat(Math.max(40 - msg.length, 0));
|
||||
}
|
||||
// Time format: 2006-01-02T15:04:05-0700 -> 01-02|15:04:05
|
||||
content += `<span style="color:${color}">${lvl}</span>[${t.substr(5, 5)}|${t.substr(11, 8)}] ${msg}`;
|
||||
|
||||
for (let i = 0; i < ctx.length; i += 2) {
|
||||
const key = ctx[i];
|
||||
const value = ctx[i + 1];
|
||||
let padding = fieldPadding.get(key);
|
||||
if (typeof padding === 'undefined' || padding < value.length) {
|
||||
padding = value.length;
|
||||
fieldPadding.set(key, padding);
|
||||
}
|
||||
content += ` <span style="color:${color}">${key}</span>=${value}${' '.repeat(padding - value.length)}`;
|
||||
}
|
||||
content += '<br />';
|
||||
});
|
||||
return content;
|
||||
};
|
||||
|
||||
// inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array.
|
||||
// limit is the maximum length of the chunk array, used in order to prevent the OOM in the browser.
|
||||
export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) => {
|
||||
prev.topChanged = 0;
|
||||
prev.bottomChanged = 0;
|
||||
if (!update.stream && update.end) {
|
||||
if (update.past) {
|
||||
prev.endTop = true;
|
||||
} else {
|
||||
prev.endBottom = true;
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
if (update.stream && !prev.endBottom) {
|
||||
return prev;
|
||||
}
|
||||
if (!Array.isArray(update.chunk) || update.chunk.length < 1) {
|
||||
return prev;
|
||||
}
|
||||
const chunk = {
|
||||
content: createChunk(update.chunk),
|
||||
tFirst: update.chunk[0].t,
|
||||
tLast: update.chunk[update.chunk.length - 1].t,
|
||||
};
|
||||
if (!Array.isArray(prev.chunks) || prev.chunks.length < 1) {
|
||||
prev.chunks = [chunk];
|
||||
prev.topChanged = 1;
|
||||
prev.bottomChanged = 1;
|
||||
return prev;
|
||||
}
|
||||
if (update.stream) {
|
||||
// The stream chunks are appended to the last chunk, because otherwise the small stream chunks would cause
|
||||
// imbalance in the amount of the visualized logs. In order to protect a chunk from growing too large a new
|
||||
// chunk is created when a new file is opened on the server side. In case of stream end indicates if a new file
|
||||
// was opened.
|
||||
if (update.end) {
|
||||
if (prev.chunks.length >= limit) {
|
||||
prev.endTop = false;
|
||||
prev.chunks.splice(0, prev.chunks.length - limit + 1);
|
||||
prev.topChanged = -1;
|
||||
}
|
||||
prev.chunks = [...prev.chunks, chunk];
|
||||
} else {
|
||||
prev.chunks[prev.chunks.length - 1].content += chunk.content;
|
||||
prev.chunks[prev.chunks.length - 1].tLast = chunk.tLast;
|
||||
}
|
||||
prev.bottomChanged = 1;
|
||||
return prev;
|
||||
}
|
||||
if (update.past) {
|
||||
if (prev.chunks.length >= limit) {
|
||||
prev.endBottom = false;
|
||||
prev.chunks.splice(limit - 1, prev.chunks.length - limit + 1);
|
||||
prev.bottomChanged = -1;
|
||||
}
|
||||
prev.chunks = [chunk, ...prev.chunks];
|
||||
prev.topChanged = 1;
|
||||
return prev;
|
||||
}
|
||||
if (prev.chunks.length >= limit) {
|
||||
prev.endTop = false;
|
||||
prev.chunks.splice(0, prev.chunks.length - limit + 1);
|
||||
prev.topChanged = -1;
|
||||
}
|
||||
prev.chunks = [...prev.chunks, chunk];
|
||||
prev.bottomChanged = 1;
|
||||
return prev;
|
||||
};
|
||||
|
||||
// styles contains the constant styles of the component.
|
||||
const styles = {
|
||||
logs: {
|
||||
overflowX: 'auto',
|
||||
},
|
||||
logListItem: {
|
||||
padding: 0,
|
||||
},
|
||||
logChunk: {
|
||||
color: 'white',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
logs: () => Object,
|
||||
container: Object,
|
||||
content: Content,
|
||||
shouldUpdate: Object,
|
||||
send: string => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
requestAllowed: boolean,
|
||||
};
|
||||
|
||||
// Logs renders the log page.
|
||||
class Logs extends Component<Props> {
|
||||
class Logs extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
requestAllowed: true,
|
||||
};
|
||||
}
|
||||
|
||||
// onScroll is triggered by the parent component's scroll event, and sends requests if the scroll position is
|
||||
// at the top or at the bottom.
|
||||
onScroll = () => {
|
||||
const {logs} = this.props.content;
|
||||
if (typeof this.props.container === 'undefined' || logs.chunks.length < 1 || !this.state.requestAllowed) {
|
||||
return;
|
||||
}
|
||||
if (this.atTop() && !logs.endTop) {
|
||||
this.props.send(JSON.stringify({
|
||||
Logs: {
|
||||
Time: logs.chunks[0].tFirst,
|
||||
Past: true,
|
||||
},
|
||||
}));
|
||||
this.setState({requestAllowed: false});
|
||||
}
|
||||
if (this.atBottom() && !logs.endBottom) {
|
||||
this.props.send(JSON.stringify({
|
||||
Logs: {
|
||||
Time: logs.chunks[logs.chunks.length - 1].tLast,
|
||||
Past: false,
|
||||
},
|
||||
}));
|
||||
this.setState({requestAllowed: false});
|
||||
}
|
||||
};
|
||||
|
||||
// atTop checks if the scroll position it at the top of the container.
|
||||
atTop = () => this.props.container.scrollTop <= requestLimit;
|
||||
|
||||
// atBottom checks if the scroll position it at the bottom of the container.
|
||||
atBottom = () =>
|
||||
this.props.container.scrollHeight - this.props.container.scrollTop <=
|
||||
this.props.container.clientHeight + requestLimit;
|
||||
|
||||
// didUpdate is called by the parent component, which provides the container. Sends the first request if the
|
||||
// visible part of the container isn't full, and resets the scroll position in order to avoid jumping when new
|
||||
// chunk is inserted.
|
||||
didUpdate = () => {
|
||||
if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const {logs} = this.props.content;
|
||||
const {container} = this.props;
|
||||
if (typeof container === 'undefined' || logs.chunks.length < 1) {
|
||||
return;
|
||||
}
|
||||
this.setState({requestAllowed: true});
|
||||
if (this.content.clientHeight < container.clientHeight) {
|
||||
// Only enters here at the beginning, when there isn't enough log to fill the container.
|
||||
//
|
||||
// In case there isn't any log chunk in the array, a request with time (new Date()).toISOString()
|
||||
// could be sent, but it would allow to duplicate the first few records from the stream, since the
|
||||
// stream handler loads the last file. No log records will appear before the first stream chunk.
|
||||
if (!logs.endTop) {
|
||||
this.props.send(JSON.stringify({
|
||||
Logs: {
|
||||
Time: logs.chunks[0].tFirst,
|
||||
Past: true,
|
||||
},
|
||||
}));
|
||||
this.setState({requestAllowed: false});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const chunks = this.content.children[0].children;
|
||||
if (this.atTop()) {
|
||||
if (logs.topChanged > 0) {
|
||||
container.scrollTop = chunks[0].clientHeight;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.atBottom() && logs.bottomChanged > 0) {
|
||||
if (logs.endBottom) {
|
||||
container.scrollTop = container.scrollHeight - container.clientHeight;
|
||||
} else {
|
||||
container.scrollTop = container.scrollHeight - chunks[chunks.length - 1].clientHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div >
|
||||
{this.props.logs()}
|
||||
<div style={styles.logs} ref={(ref) => { this.content = ref; }}>
|
||||
<List>
|
||||
{this.props.content.logs.chunks.map((c, index) => (
|
||||
<ListItem style={styles.logListItem} key={index}>
|
||||
<div style={styles.logChunk} dangerouslySetInnerHTML={{__html: c.content}} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,36 +47,27 @@ const themeStyles = theme => ({
|
|||
});
|
||||
|
||||
export type Props = {
|
||||
classes: Object,
|
||||
active: string,
|
||||
content: Content,
|
||||
classes: Object,
|
||||
active: string,
|
||||
content: Content,
|
||||
shouldUpdate: Object,
|
||||
send: (string) => void,
|
||||
logs: () => Object,
|
||||
send: string => void,
|
||||
};
|
||||
|
||||
// Main renders the chosen content.
|
||||
class Main extends Component<Props> {
|
||||
handleScroll = () => {
|
||||
if (typeof this.container !== 'undefined') {
|
||||
// console.log(this.container.scrollTop, this.container.scrollHeight);
|
||||
if (this.container.scrollTop === 0) {
|
||||
// this.props.send(JSON.stringify({Logs: {Time: '2018-04-11T12:48:18.181274193+03:00'}}));
|
||||
console.log("Top");
|
||||
}
|
||||
if (this.container.scrollHeight - this.container.scrollTop === this.container.clientHeight) {
|
||||
console.log("Bottom");
|
||||
// this.container.scrollTop = 0;
|
||||
}
|
||||
class Main extends Component<Props, State> {
|
||||
componentDidUpdate() {
|
||||
if (this.content && typeof this.content.didUpdate === 'function') {
|
||||
this.content.didUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onScroll = () => {
|
||||
if (this.content && typeof this.content.onScroll === 'function') {
|
||||
this.content.onScroll();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
// if (typeof this.container !== 'undefined') {
|
||||
// this.container.scrollTop = this.container.scrollHeight - this.container.clientHeight;
|
||||
// }
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classes, active, content, shouldUpdate,
|
||||
|
|
@ -92,7 +83,15 @@ class Main extends Component<Props> {
|
|||
children = <div>Work in progress.</div>;
|
||||
break;
|
||||
case MENU.get('logs').id:
|
||||
children = <Logs logs={this.props.logs} />;
|
||||
children = (
|
||||
<Logs
|
||||
ref={(ref) => { this.content = ref; }}
|
||||
container={this.container}
|
||||
send={this.props.send}
|
||||
content={this.props.content}
|
||||
shouldUpdate={shouldUpdate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -101,7 +100,7 @@ class Main extends Component<Props> {
|
|||
className={classes.content}
|
||||
style={styles.content}
|
||||
ref={(ref) => { this.container = ref; }}
|
||||
onScroll={this.handleScroll}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ export type ChartEntry = {
|
|||
};
|
||||
|
||||
export type General = {
|
||||
version: ?string,
|
||||
commit: ?string,
|
||||
version: ?string,
|
||||
commit: ?string,
|
||||
};
|
||||
|
||||
export type Home = {
|
||||
|
|
@ -55,29 +55,40 @@ export type Network = {
|
|||
};
|
||||
|
||||
export type System = {
|
||||
activeMemory: ChartEntries,
|
||||
virtualMemory: ChartEntries,
|
||||
networkIngress: ChartEntries,
|
||||
networkEgress: ChartEntries,
|
||||
processCPU: ChartEntries,
|
||||
systemCPU: ChartEntries,
|
||||
diskRead: ChartEntries,
|
||||
diskWrite: ChartEntries,
|
||||
activeMemory: ChartEntries,
|
||||
virtualMemory: ChartEntries,
|
||||
networkIngress: ChartEntries,
|
||||
networkEgress: ChartEntries,
|
||||
processCPU: ChartEntries,
|
||||
systemCPU: ChartEntries,
|
||||
diskRead: ChartEntries,
|
||||
diskWrite: ChartEntries,
|
||||
};
|
||||
|
||||
export type Record = {
|
||||
t: Object,
|
||||
lvl: Object,
|
||||
msg: string,
|
||||
ctx: Array<string>
|
||||
t: string,
|
||||
lvl: Object,
|
||||
msg: string,
|
||||
ctx: Array<string>
|
||||
};
|
||||
|
||||
export type Chunk = {
|
||||
content: string,
|
||||
t: string,
|
||||
len: int,
|
||||
content: string,
|
||||
tFirst: string,
|
||||
tLast: string,
|
||||
};
|
||||
|
||||
export type Logs = {
|
||||
chunk: Array<Chunk>,
|
||||
chunks: Array<Chunk>,
|
||||
endTop: boolean,
|
||||
endBottom: boolean,
|
||||
topChanged: number,
|
||||
bottomChanged: number,
|
||||
};
|
||||
|
||||
export type LogsMessage = {
|
||||
stream: boolean,
|
||||
past: boolean,
|
||||
end: boolean,
|
||||
chunk: Array<Record>,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/mohae/deepcopy"
|
||||
"golang.org/x/net/websocket"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
|
@ -110,7 +111,11 @@ func New(config *Config, commit string, logdir string) (*Dashboard, error) {
|
|||
DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh),
|
||||
DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh),
|
||||
},
|
||||
Logs: &LogsMessage{Chunk: json.RawMessage("[]")},
|
||||
Logs: &LogsMessage{
|
||||
Stream: true,
|
||||
End: false,
|
||||
Chunk: json.RawMessage("[]"),
|
||||
},
|
||||
},
|
||||
logdir: logdir,
|
||||
}, nil
|
||||
|
|
@ -239,7 +244,7 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
|||
|
||||
db.lock.Lock()
|
||||
// Send the past data.
|
||||
client.msg <- db.history.DeepCopy()
|
||||
client.msg <- deepcopy.Copy(db.history).(*Message)
|
||||
// Start tracking the connection and drop at connection loss.
|
||||
db.conns[id] = client
|
||||
db.lock.Unlock()
|
||||
|
|
@ -252,70 +257,121 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
|||
var r Request
|
||||
err := websocket.JSON.Receive(conn, &r)
|
||||
if err != nil {
|
||||
client.logger.Warn("Failed to receive request", "err", err)
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
if r.Logs != nil {
|
||||
db.handleLogs(r.Logs, client) // TODO (kurkomisi): concurrent function call?
|
||||
db.handleLogRequest(r.Logs, client) // TODO (kurkomisi): concurrent function call?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogs searches for the log file specified by the timestamp of the request, creates a JSON array out of it
|
||||
func validateLogFile(path string) ([]byte, bool) {
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open file", "path", path, "err", err)
|
||||
return nil, false
|
||||
}
|
||||
defer f.Close()
|
||||
var buf []byte
|
||||
if buf, err = ioutil.ReadAll(f); err != nil {
|
||||
log.Warn("Failed to read file", "path", path, "err", err)
|
||||
return nil, false
|
||||
}
|
||||
end := -1
|
||||
for j := 0; j < len(buf); j++ {
|
||||
if buf[j] == '\n' {
|
||||
buf[j] = ','
|
||||
end = j
|
||||
}
|
||||
}
|
||||
if end < 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return buf[:end], true
|
||||
}
|
||||
|
||||
// handleLogRequest searches for the log file specified by the timestamp of the request, creates a JSON array out of it
|
||||
// and sends it to the requesting client.
|
||||
func (db *Dashboard) handleLogs(r *LogsRequest, c *client) {
|
||||
func (db *Dashboard) handleLogRequest(r *LogsRequest, c *client) {
|
||||
files, err := ioutil.ReadDir(db.logdir)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open logdir", "logdir", db.logdir, "err", err)
|
||||
return
|
||||
}
|
||||
re := regexp.MustCompile(".log$")
|
||||
valid := make([]string, len(files))
|
||||
fileNames := make([]string, len(files))
|
||||
n := 0
|
||||
for _, f := range files {
|
||||
if f.Mode().IsRegular() && re.Match([]byte(f.Name())) {
|
||||
valid[n] = f.Name()
|
||||
fileNames[n] = f.Name()
|
||||
n++
|
||||
}
|
||||
}
|
||||
if len(valid) < 1 {
|
||||
log.Warn("There isn't any log file in the logdir", "logdir", db.logdir)
|
||||
n-- // The last file is handled by the stream handler in order to avoid log duplication on the client side.
|
||||
if n < 1 {
|
||||
log.Warn("There isn't any old log file in the logdir", "path", db.logdir)
|
||||
return
|
||||
}
|
||||
timestamp := fmt.Sprintf("%s.log", strings.Replace(r.Time.Format("060102150405.00"), ".", "", 1))
|
||||
i := sort.Search(len(valid), func(i int) bool {
|
||||
return valid[i] >= timestamp
|
||||
})
|
||||
if i >= len(valid) {
|
||||
i = len(valid) - 1
|
||||
}
|
||||
f, err := os.OpenFile(filepath.Join(db.logdir, valid[i]), os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open file", "name", valid[i], "err", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
buf, err := ioutil.ReadAll(f)
|
||||
last := -1
|
||||
for i := 0; i < len(buf); i++ {
|
||||
if buf[i] == '\n' {
|
||||
buf[i] = ','
|
||||
last = i
|
||||
}
|
||||
}
|
||||
if last >= 0 {
|
||||
b := make([]byte, last+2)
|
||||
b[0] = '['
|
||||
copy(b[1:], buf[:last])
|
||||
b[last+1] = ']'
|
||||
|
||||
db.lock.Lock() // TODO (kurkomisi): Maybe create mutex for the client.
|
||||
c.msg <- &Message{
|
||||
Logs: &LogsMessage{
|
||||
Chunk: b,
|
||||
},
|
||||
i := sort.Search(n, func(i int) bool {
|
||||
return fileNames[i] >= timestamp // Returns the smallest index such as fileNames[i] >= timestamp.
|
||||
})
|
||||
ok := false
|
||||
var buf json.RawMessage
|
||||
if r.Past {
|
||||
if i >= n {
|
||||
i = n - 1
|
||||
}
|
||||
db.lock.Unlock()
|
||||
for i >= 0 && fileNames[i] >= timestamp {
|
||||
i--
|
||||
}
|
||||
for i >= 0 && !ok {
|
||||
buf, ok = validateLogFile(filepath.Join(db.logdir, fileNames[i]))
|
||||
i--
|
||||
}
|
||||
} else {
|
||||
for i < n && fileNames[i] <= timestamp {
|
||||
i++
|
||||
}
|
||||
for i < n && !ok {
|
||||
buf, ok = validateLogFile(filepath.Join(db.logdir, fileNames[i]))
|
||||
i++
|
||||
}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = json.RawMessage{}
|
||||
}
|
||||
b := make(json.RawMessage, len(buf)+2)
|
||||
b[0] = '['
|
||||
copy(b[1:], buf)
|
||||
b[len(buf)+1] = ']'
|
||||
|
||||
db.lock.Lock()
|
||||
c.msg <- &Message{
|
||||
Logs: &LogsMessage{
|
||||
Stream: false,
|
||||
Past: r.Past,
|
||||
End: !ok,
|
||||
Chunk: b,
|
||||
},
|
||||
}
|
||||
db.lock.Unlock()
|
||||
}
|
||||
|
||||
// metricCollector returns a function, which retrieves a specific metric.
|
||||
func metricCollector(name string) func() int64 {
|
||||
if metric := metrics.DefaultRegistry.Get(name); metric != nil {
|
||||
m := metric.(metrics.Meter)
|
||||
return func() int64 {
|
||||
return m.Count()
|
||||
}
|
||||
}
|
||||
return func() int64 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,12 +384,17 @@ func (db *Dashboard) collectData() {
|
|||
var (
|
||||
mem runtime.MemStats
|
||||
|
||||
prevNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
|
||||
prevNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
|
||||
collectNetworkIngress = metricCollector("p2p/InboundTraffic")
|
||||
collectNetworkEgress = metricCollector("p2p/OutboundTraffic")
|
||||
collectDiskRead = metricCollector("eth/db/chaindata/disk/read")
|
||||
collectDiskWrite = metricCollector("eth/db/chaindata/disk/write")
|
||||
|
||||
prevNetworkIngress = collectNetworkIngress()
|
||||
prevNetworkEgress = collectNetworkEgress()
|
||||
prevProcessCPUTime = getProcessCPUTime()
|
||||
prevSystemCPUUsage = systemCPUUsage
|
||||
prevDiskRead = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/read").(metrics.Meter).Count()
|
||||
prevDiskWrite = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/write").(metrics.Meter).Count()
|
||||
prevDiskRead = collectDiskRead()
|
||||
prevDiskWrite = collectDiskWrite()
|
||||
|
||||
frequency = float64(db.config.Refresh / time.Second)
|
||||
numCPU = float64(runtime.NumCPU())
|
||||
|
|
@ -349,12 +410,12 @@ func (db *Dashboard) collectData() {
|
|||
case <-time.After(db.config.Refresh):
|
||||
systemCPUUsage.Get()
|
||||
var (
|
||||
curNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
|
||||
curNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
|
||||
curNetworkIngress = collectNetworkIngress()
|
||||
curNetworkEgress = collectNetworkEgress()
|
||||
curProcessCPUTime = getProcessCPUTime()
|
||||
curSystemCPUUsage = systemCPUUsage
|
||||
curDiskRead = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/read").(metrics.Meter).Count()
|
||||
curDiskWrite = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/write").(metrics.Meter).Count()
|
||||
curDiskRead = collectDiskRead()
|
||||
curDiskWrite = collectDiskWrite()
|
||||
|
||||
deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress)
|
||||
deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress)
|
||||
|
|
@ -436,13 +497,6 @@ func (db *Dashboard) collectData() {
|
|||
func (db *Dashboard) streamLogs() {
|
||||
defer db.wg.Done()
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Warn("Failed to create fs watcher", "err", err)
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
files, err := ioutil.ReadDir(db.logdir)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open logdir", "logdir", db.logdir, "err", err)
|
||||
|
|
@ -468,31 +522,34 @@ func (db *Dashboard) streamLogs() {
|
|||
return
|
||||
}
|
||||
}
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Warn("Failed to create fs watcher", "err", err)
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
err = watcher.Add(db.logdir)
|
||||
if err != nil {
|
||||
log.Warn("Failed to add logdir to fs watcher", "logdir", db.logdir, "err", err)
|
||||
return
|
||||
}
|
||||
change := fsnotify.Create | fsnotify.Remove | fsnotify.Rename
|
||||
defer opened.Close() // Close the lastly opened file.
|
||||
ticker := time.NewTicker(db.config.Refresh)
|
||||
defer ticker.Stop()
|
||||
|
||||
newFile := false
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
switch {
|
||||
// If new log file is opened.
|
||||
case event.Op&change != 0:
|
||||
if re.Match([]byte(event.Name)) && opened.Name() < event.Name {
|
||||
if opened, err = os.OpenFile(event.Name, os.O_RDONLY, 0644); err != nil {
|
||||
log.Warn("Failed to open file", "name", event.Name, "err", err)
|
||||
return
|
||||
}
|
||||
db.lock.Lock()
|
||||
db.history.Logs.Chunk = json.RawMessage("[]")
|
||||
db.lock.Unlock()
|
||||
}
|
||||
// If new log records were written into the opened log file.
|
||||
case event.Op&fsnotify.Write != 0:
|
||||
// If new log file was created.
|
||||
if event.Op&fsnotify.Create != 0 && re.Match([]byte(event.Name)) {
|
||||
if opened != nil {
|
||||
// The new log file's timestamp is always greater, since it is created of the actual time.
|
||||
if opened.Name() >= event.Name {
|
||||
break
|
||||
}
|
||||
// Read the rest of the previously opened file.
|
||||
chunk, err := ioutil.ReadAll(opened)
|
||||
if err != nil {
|
||||
log.Warn("Failed to read file", "name", opened.Name(), "err", err)
|
||||
|
|
@ -502,6 +559,37 @@ func (db *Dashboard) streamLogs() {
|
|||
copy(b, buf)
|
||||
copy(b[len(buf):], chunk)
|
||||
buf = b
|
||||
opened.Close()
|
||||
}
|
||||
last := -1
|
||||
for i := 0; i < len(buf); i++ {
|
||||
if buf[i] == '\n' {
|
||||
buf[i] = ','
|
||||
last = i
|
||||
}
|
||||
}
|
||||
if last >= 0 {
|
||||
msg := make([]byte, last+2)
|
||||
msg[0] = '['
|
||||
copy(msg[1:], buf[:last])
|
||||
msg[last+1] = ']'
|
||||
|
||||
db.sendToAll(&Message{
|
||||
Logs: &LogsMessage{
|
||||
Stream: true,
|
||||
End: false,
|
||||
Chunk: msg,
|
||||
},
|
||||
})
|
||||
db.lock.Lock()
|
||||
db.history.Logs.Chunk = json.RawMessage("[]")
|
||||
db.lock.Unlock()
|
||||
}
|
||||
buf = buf[:0]
|
||||
newFile = true
|
||||
if opened, err = os.OpenFile(event.Name, os.O_RDONLY, 0644); err != nil {
|
||||
log.Warn("Failed to open file", "name", event.Name, "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
|
|
@ -509,51 +597,62 @@ func (db *Dashboard) streamLogs() {
|
|||
log.Warn("Fs watcher error", "err", err)
|
||||
}
|
||||
return
|
||||
// Send log updates to the client.
|
||||
case <-time.After(db.config.Refresh):
|
||||
last := -1
|
||||
for i := 0; i < len(buf); i++ {
|
||||
if buf[i] == '\n' {
|
||||
buf[i] = ','
|
||||
last = i
|
||||
}
|
||||
}
|
||||
if last >= 0 {
|
||||
b := make([]byte, last+2)
|
||||
b[0] = '['
|
||||
copy(b[1:], buf[:last])
|
||||
b[last+1] = ']'
|
||||
|
||||
db.sendToAll(&Message{
|
||||
Logs: &LogsMessage{
|
||||
Chunk: b,
|
||||
},
|
||||
})
|
||||
|
||||
b = make([]byte, len(db.history.Logs.Chunk)+last+1)
|
||||
// Cut the ']' from the end in order to concatenate the two arrays.
|
||||
n := len(db.history.Logs.Chunk) - 1
|
||||
copy(b, db.history.Logs.Chunk[:n])
|
||||
if len(db.history.Logs.Chunk) > 2 {
|
||||
// In case the array already contained log records, put the comma separator.
|
||||
b[n] = ','
|
||||
n++
|
||||
}
|
||||
copy(b[n:], buf[:last])
|
||||
n += last
|
||||
b[n] = ']'
|
||||
n++
|
||||
|
||||
db.lock.Lock()
|
||||
db.history.Logs.Chunk = b[:n]
|
||||
db.lock.Unlock()
|
||||
|
||||
// Clear the valid/sent part of the buffer.
|
||||
buf = buf[last+1:]
|
||||
}
|
||||
case errc := <-db.quit:
|
||||
errc <- nil
|
||||
return
|
||||
// Send log updates to the client.
|
||||
case <-ticker.C:
|
||||
if opened == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Read the new logs created since the last read.
|
||||
chunk, err := ioutil.ReadAll(opened)
|
||||
if err != nil {
|
||||
log.Warn("Failed to read file", "name", opened.Name(), "err", err)
|
||||
return
|
||||
}
|
||||
b := make([]byte, len(buf)+len(chunk))
|
||||
copy(b, buf)
|
||||
copy(b[len(buf):], chunk)
|
||||
last := -1
|
||||
for i := 0; i < len(b); i++ {
|
||||
if b[i] == '\n' {
|
||||
b[i] = ','
|
||||
last = i
|
||||
}
|
||||
}
|
||||
if last < 0 {
|
||||
break
|
||||
}
|
||||
// Clear the valid/sent part of the buffer.
|
||||
buf = b[last+1:]
|
||||
|
||||
msg := make([]byte, last+2)
|
||||
msg[0] = '['
|
||||
copy(msg[1:], b[:last])
|
||||
msg[last+1] = ']'
|
||||
|
||||
db.sendToAll(&Message{
|
||||
Logs: &LogsMessage{
|
||||
Stream: true,
|
||||
End: newFile,
|
||||
Chunk: msg,
|
||||
},
|
||||
})
|
||||
newFile = false
|
||||
|
||||
db.lock.Lock()
|
||||
if len(db.history.Logs.Chunk) == 2 {
|
||||
db.history.Logs.Chunk = msg
|
||||
} else {
|
||||
b = make([]byte, len(db.history.Logs.Chunk)+len(msg)-1)
|
||||
copy(b, db.history.Logs.Chunk)
|
||||
b[len(db.history.Logs.Chunk)-1] = ','
|
||||
copy(b[len(db.history.Logs.Chunk):], msg[1:])
|
||||
db.history.Logs.Chunk = b
|
||||
}
|
||||
db.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,46 +31,18 @@ type Message struct {
|
|||
Logs *LogsMessage `json:"logs,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) DeepCopy() *Message {
|
||||
return &Message{
|
||||
m.General.DeepCopy(),
|
||||
m.Home,
|
||||
m.Chain,
|
||||
m.TxPool,
|
||||
m.Network,
|
||||
m.System.DeepCopy(),
|
||||
m.Logs,
|
||||
}
|
||||
}
|
||||
|
||||
type ChartEntries []*ChartEntry
|
||||
|
||||
func (ce ChartEntries) DeepCopy() ChartEntries {
|
||||
nce := make(ChartEntries, len(ce))
|
||||
for i, v := range ce {
|
||||
nce[i] = v.DeepCopy()
|
||||
}
|
||||
return nce
|
||||
}
|
||||
|
||||
type ChartEntry struct {
|
||||
Time time.Time `json:"time,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (ce *ChartEntry) DeepCopy() *ChartEntry {
|
||||
return &ChartEntry{ce.Time, ce.Value}
|
||||
}
|
||||
|
||||
type GeneralMessage struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
}
|
||||
|
||||
func (m *GeneralMessage) DeepCopy() *GeneralMessage {
|
||||
return &GeneralMessage{m.Version, m.Commit}
|
||||
}
|
||||
|
||||
type HomeMessage struct {
|
||||
/* TODO (kurkomisi) */
|
||||
}
|
||||
|
|
@ -98,21 +70,11 @@ type SystemMessage struct {
|
|||
DiskWrite ChartEntries `json:"diskWrite,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SystemMessage) DeepCopy() *SystemMessage {
|
||||
return &SystemMessage{
|
||||
m.ActiveMemory.DeepCopy(),
|
||||
m.VirtualMemory.DeepCopy(),
|
||||
m.NetworkIngress.DeepCopy(),
|
||||
m.NetworkEgress.DeepCopy(),
|
||||
m.ProcessCPU.DeepCopy(),
|
||||
m.SystemCPU.DeepCopy(),
|
||||
m.DiskRead.DeepCopy(),
|
||||
m.DiskWrite.DeepCopy(),
|
||||
}
|
||||
}
|
||||
|
||||
type LogsMessage struct {
|
||||
Chunk json.RawMessage `json:"chunk,omitempty"`
|
||||
Stream bool `json:"stream"` // Denotes if the chunk is part of the stream or if it contains the records from a file.
|
||||
Past bool `json:"past"` // Denotes whether the logs in the chunk were issued before or after the time given in the request.
|
||||
End bool `json:"end"` // In case of stream denotes if new file was opened, otherwise denotes if there isn't more file.
|
||||
Chunk json.RawMessage `json:"chunk"` // Contains log records.
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
|
|
@ -120,5 +82,6 @@ type Request struct {
|
|||
}
|
||||
|
||||
type LogsRequest struct {
|
||||
Time time.Time `json:"time,omitempty"`
|
||||
Time time.Time `json:"time"` // The request handler searches for log file based on this timestamp.
|
||||
Past bool `json:"past"` // Denotes whether the message should contain logs issued before or after the given time.
|
||||
}
|
||||
|
|
|
|||
21
vendor/github.com/mohae/deepcopy/LICENSE
generated
vendored
Normal file
21
vendor/github.com/mohae/deepcopy/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Joel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
8
vendor/github.com/mohae/deepcopy/README.md
generated
vendored
Normal file
8
vendor/github.com/mohae/deepcopy/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
deepCopy
|
||||
========
|
||||
[](https://godoc.org/github.com/mohae/deepcopy)[](https://travis-ci.org/mohae/deepcopy)
|
||||
|
||||
DeepCopy makes deep copies of things: unexported field values are not copied.
|
||||
|
||||
## Usage
|
||||
cpy := deepcopy.Copy(orig)
|
||||
125
vendor/github.com/mohae/deepcopy/deepcopy.go
generated
vendored
Normal file
125
vendor/github.com/mohae/deepcopy/deepcopy.go
generated
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// deepcopy makes deep copies of things. A standard copy will copy the
|
||||
// pointers: deep copy copies the values pointed to. Unexported field
|
||||
// values are not copied.
|
||||
//
|
||||
// Copyright (c)2014-2016, Joel Scoble (github.com/mohae), all rights reserved.
|
||||
// License: MIT, for more details check the included LICENSE file.
|
||||
package deepcopy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Interface for delegating copy process to type
|
||||
type Interface interface {
|
||||
DeepCopy() interface{}
|
||||
}
|
||||
|
||||
// Iface is an alias to Copy; this exists for backwards compatibility reasons.
|
||||
func Iface(iface interface{}) interface{} {
|
||||
return Copy(iface)
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of whatever is passed to it and returns the copy
|
||||
// in an interface{}. The returned value will need to be asserted to the
|
||||
// correct type.
|
||||
func Copy(src interface{}) interface{} {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the interface a reflect.Value
|
||||
original := reflect.ValueOf(src)
|
||||
|
||||
// Make a copy of the same type as the original.
|
||||
cpy := reflect.New(original.Type()).Elem()
|
||||
|
||||
// Recursively copy the original.
|
||||
copyRecursive(original, cpy)
|
||||
|
||||
// Return the copy as an interface.
|
||||
return cpy.Interface()
|
||||
}
|
||||
|
||||
// copyRecursive does the actual copying of the interface. It currently has
|
||||
// limited support for what it can handle. Add as needed.
|
||||
func copyRecursive(original, cpy reflect.Value) {
|
||||
// check for implement deepcopy.Interface
|
||||
if original.CanInterface() {
|
||||
if copier, ok := original.Interface().(Interface); ok {
|
||||
cpy.Set(reflect.ValueOf(copier.DeepCopy()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handle according to original's Kind
|
||||
switch original.Kind() {
|
||||
case reflect.Ptr:
|
||||
// Get the actual value being pointed to.
|
||||
originalValue := original.Elem()
|
||||
|
||||
// if it isn't valid, return.
|
||||
if !originalValue.IsValid() {
|
||||
return
|
||||
}
|
||||
cpy.Set(reflect.New(originalValue.Type()))
|
||||
copyRecursive(originalValue, cpy.Elem())
|
||||
|
||||
case reflect.Interface:
|
||||
// If this is a nil, don't do anything
|
||||
if original.IsNil() {
|
||||
return
|
||||
}
|
||||
// Get the value for the interface, not the pointer.
|
||||
originalValue := original.Elem()
|
||||
|
||||
// Get the value by calling Elem().
|
||||
copyValue := reflect.New(originalValue.Type()).Elem()
|
||||
copyRecursive(originalValue, copyValue)
|
||||
cpy.Set(copyValue)
|
||||
|
||||
case reflect.Struct:
|
||||
t, ok := original.Interface().(time.Time)
|
||||
if ok {
|
||||
cpy.Set(reflect.ValueOf(t))
|
||||
return
|
||||
}
|
||||
// Go through each field of the struct and copy it.
|
||||
for i := 0; i < original.NumField(); i++ {
|
||||
// The Type's StructField for a given field is checked to see if StructField.PkgPath
|
||||
// is set to determine if the field is exported or not because CanSet() returns false
|
||||
// for settable fields. I'm not sure why. -mohae
|
||||
if original.Type().Field(i).PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
copyRecursive(original.Field(i), cpy.Field(i))
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
if original.IsNil() {
|
||||
return
|
||||
}
|
||||
// Make a new slice and copy each element.
|
||||
cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
|
||||
for i := 0; i < original.Len(); i++ {
|
||||
copyRecursive(original.Index(i), cpy.Index(i))
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
if original.IsNil() {
|
||||
return
|
||||
}
|
||||
cpy.Set(reflect.MakeMap(original.Type()))
|
||||
for _, key := range original.MapKeys() {
|
||||
originalValue := original.MapIndex(key)
|
||||
copyValue := reflect.New(originalValue.Type()).Elem()
|
||||
copyRecursive(originalValue, copyValue)
|
||||
copyKey := Copy(key.Interface())
|
||||
cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
|
||||
}
|
||||
|
||||
default:
|
||||
cpy.Set(original)
|
||||
}
|
||||
}
|
||||
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
|
|
@ -291,6 +291,12 @@
|
|||
"revision": "ad45545899c7b13c020ea92b2072220eefad42b8",
|
||||
"revisionTime": "2015-03-14T17:03:34Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "2jsbDTvwxafPp7FJjJ8IIFlTLjs=",
|
||||
"path": "github.com/mohae/deepcopy",
|
||||
"revision": "c48cc78d482608239f6c4c92a4abd87eb8761c90",
|
||||
"revisionTime": "2017-09-29T03:49:55Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "2gmvVTDCks8cPhpmyDlvm0sbrXE=",
|
||||
"path": "github.com/naoina/toml",
|
||||
|
|
|
|||
Loading…
Reference in a new issue