Implemented experimental Script Console for debugging with editor in the diag ui.
This commit is contained in:
parent
031371652b
commit
94f8e38d5a
|
@ -3,7 +3,7 @@
|
||||||
# Uncomment this to turn on verbose mode.
|
# Uncomment this to turn on verbose mode.
|
||||||
#export DH_VERBOSE=1
|
#export DH_VERBOSE=1
|
||||||
|
|
||||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0
|
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
|
||||||
|
|
||||||
%:
|
%:
|
||||||
dh $@ --with=systemd --with=cli
|
dh $@ --with=systemd --with=cli
|
||||||
|
|
|
@ -119,6 +119,11 @@ const config = {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.worker\.js$/,
|
test: /\.worker\.js$/,
|
||||||
|
issuer: {
|
||||||
|
// monaco-editor includes the editor.worker.js in other language workers,
|
||||||
|
// don't use worker-loader in that case
|
||||||
|
exclude: /monaco-editor/
|
||||||
|
},
|
||||||
use: {
|
use: {
|
||||||
loader: 'worker-loader',
|
loader: 'worker-loader',
|
||||||
options: {
|
options: {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
|
import Diagnostic from 'Diagnostic/Diagnostic';
|
||||||
|
|
||||||
function AppRoutes(props) {
|
function AppRoutes(props) {
|
||||||
const {
|
const {
|
||||||
|
@ -229,6 +230,15 @@ function AppRoutes(props) {
|
||||||
component={Logs}
|
component={Logs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Diagnostics
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/diag"
|
||||||
|
component={Diagnostic}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Not Found
|
Not Found
|
||||||
*/}
|
*/}
|
||||||
|
|
|
@ -165,6 +165,23 @@ const links = [
|
||||||
to: '/system/logs/files'
|
to: '/system/logs/files'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
iconName: icons.DEBUG,
|
||||||
|
hidden: true,
|
||||||
|
title: 'Diagnostics',
|
||||||
|
to: '/diag/status',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Status',
|
||||||
|
to: '/diag/status'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Script Console',
|
||||||
|
to: '/diag/script'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -473,6 +490,10 @@ class PageSidebar extends Component {
|
||||||
const isActiveParent = activeParent === link.to;
|
const isActiveParent = activeParent === link.to;
|
||||||
const hasActiveChild = hasActiveChildLink(link, pathname);
|
const hasActiveChild = hasActiveChildLink(link, pathname);
|
||||||
|
|
||||||
|
if (link.hidden && !isActiveParent && !hasActiveChild) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSidebarItem
|
<PageSidebarItem
|
||||||
key={link.to}
|
key={link.to}
|
||||||
|
|
|
@ -41,6 +41,7 @@ function PageToolbarButton(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
PageToolbarButton.propTypes = {
|
PageToolbarButton.propTypes = {
|
||||||
|
...Link.propTypes,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
iconName: PropTypes.object.isRequired,
|
iconName: PropTypes.object.isRequired,
|
||||||
spinningName: PropTypes.object,
|
spinningName: PropTypes.object,
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Route, Redirect } from 'react-router-dom';
|
||||||
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
import Switch from 'Components/Router/Switch';
|
||||||
|
import StatusConnector from './Status/StatusConnector';
|
||||||
|
import ScriptConnector from './Script/ScriptConnector';
|
||||||
|
|
||||||
|
class Diagnostic extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/diag/status"
|
||||||
|
component={StatusConnector}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/diag/script"
|
||||||
|
component={ScriptConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Redirect root to status */}
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/diag"
|
||||||
|
render={() => {
|
||||||
|
return (
|
||||||
|
<Redirect
|
||||||
|
to={getPathWithUrlBase('/diag/status')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Diagnostic;
|
|
@ -0,0 +1,82 @@
|
||||||
|
import ReactMonacoEditor from 'react-monaco-editor';
|
||||||
|
import shallowEqual from 'shallowequal';
|
||||||
|
|
||||||
|
// All editor features -> 7.56 MiB
|
||||||
|
// import 'monaco-editor/esm/vs/editor/editor.all';
|
||||||
|
|
||||||
|
// Only the needed editor features -> 6.88 MiB
|
||||||
|
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands';
|
||||||
|
import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/browser/widget/diffEditorWidget';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/browser/widget/diffNavigator';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/caretOperations';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/transpose';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/clipboard/clipboard';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/codeAction/codeActionContributions';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/codelens/codelensController';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/colorPicker/colorDetector';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/comment/comment';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/contextmenu';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/cursorUndo';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/dnd/dnd';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/find/findController';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/folding/folding';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/fontZoom/fontZoom';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/format/formatActions';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionCommands';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionMouse';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/gotoError/gotoError';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/hover/hover';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/inPlaceReplace/inPlaceReplace';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/linesOperations/linesOperations';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/links/links';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/multicursor/multicursor';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/referenceSearch/referenceSearch';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/rename/rename';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/smartSelect/smartSelect';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/snippet/snippetController2';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/tokenization/tokenization';
|
||||||
|
// import 'monaco-editor/esm/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations';
|
||||||
|
import 'monaco-editor/esm/vs/editor/contrib/wordPartOperations/wordPartOperations';
|
||||||
|
|
||||||
|
// csharp&json language
|
||||||
|
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp';
|
||||||
|
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp.contribution';
|
||||||
|
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
|
||||||
|
import 'monaco-editor/esm/vs/language/json/jsonWorker';
|
||||||
|
import 'monaco-editor/esm/vs/language/json/jsonMode';
|
||||||
|
|
||||||
|
// Create a WebWorker from a blob rather than an url
|
||||||
|
import * as EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker';
|
||||||
|
import * as JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker';
|
||||||
|
|
||||||
|
self.MonacoEnvironment = {
|
||||||
|
getWorker: (moduleId, label) => {
|
||||||
|
if (label === 'editorWorkerService') {
|
||||||
|
return new EditorWorker();
|
||||||
|
}
|
||||||
|
if (label === 'json') {
|
||||||
|
return new JsonWorker();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MonacoEditor extends ReactMonacoEditor {
|
||||||
|
|
||||||
|
// ReactMonacoEditor should've been PureComponent
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
if (!shallowEqual(this.props, nextProps)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MonacoEditor;
|
|
@ -0,0 +1,93 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchStatus, updateScript, validateScript, executeScript } from 'Store/Actions/diagnosticActions';
|
||||||
|
import ScriptConsole from './ScriptConsole';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.diagnostic,
|
||||||
|
(diag) => {
|
||||||
|
return {
|
||||||
|
isStatusPopulated: diag.status.isPopulated,
|
||||||
|
isScriptConsoleEnabled: diag.status.item.scriptConsoleEnabled,
|
||||||
|
isExecuting: diag.script.isExecuting || false,
|
||||||
|
isDebugging: diag.script.isDebugging || false,
|
||||||
|
isValidating: diag.script.isValidating,
|
||||||
|
code: diag.script.code,
|
||||||
|
result: diag.script.result,
|
||||||
|
validation: diag.script.validation,
|
||||||
|
error: diag.script.error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchStatus,
|
||||||
|
updateScript,
|
||||||
|
validateScript,
|
||||||
|
executeScript
|
||||||
|
};
|
||||||
|
|
||||||
|
class ScriptConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.isStatusPopulated) {
|
||||||
|
this.props.fetchStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.props.isStatusPopulated) {
|
||||||
|
return (
|
||||||
|
<PageContent>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
} else if (!this.props.isScriptConsoleEnabled) {
|
||||||
|
return (
|
||||||
|
<PageContent>
|
||||||
|
<Alert kind={kinds.WARNING}>
|
||||||
|
Diagnostic Scripting is disabled
|
||||||
|
</Alert>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScriptConsole
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptConnector.propTypes = {
|
||||||
|
isStatusPopulated: PropTypes.bool.isRequired,
|
||||||
|
isScriptConsoleEnabled: PropTypes.bool,
|
||||||
|
isExecuting: PropTypes.bool.isRequired,
|
||||||
|
isDebugging: PropTypes.bool.isRequired,
|
||||||
|
isValidating: PropTypes.bool.isRequired,
|
||||||
|
code: PropTypes.string,
|
||||||
|
result: PropTypes.object,
|
||||||
|
error: PropTypes.object,
|
||||||
|
validation: PropTypes.object,
|
||||||
|
fetchStatus: PropTypes.func.isRequired,
|
||||||
|
updateScript: PropTypes.func.isRequired,
|
||||||
|
validateScript: PropTypes.func.isRequired,
|
||||||
|
executeScript: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ScriptConnector);
|
|
@ -0,0 +1,6 @@
|
||||||
|
.split {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, lazy, Suspense } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import styles from './ScriptConsole.css';
|
||||||
|
|
||||||
|
// Lazy load the Monaco Editor since it's a big bundle
|
||||||
|
const MonacoEditor = lazy(() => import(/* webpackChunkName: "monaco-editor" */ './MonacoEditor'));
|
||||||
|
|
||||||
|
const DefaultOptions = {
|
||||||
|
selectOnLineNumbers: true,
|
||||||
|
scrollBeyondLastLine: false
|
||||||
|
};
|
||||||
|
const DefaultResultOptions = {
|
||||||
|
...DefaultOptions,
|
||||||
|
readOnly: true
|
||||||
|
};
|
||||||
|
|
||||||
|
class ScriptConsole extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
editorDidMount = (editor, monaco) => {
|
||||||
|
console.log('editorDidMount', editor);
|
||||||
|
editor.focus();
|
||||||
|
this.monaco = monaco;
|
||||||
|
this.editor = editor;
|
||||||
|
|
||||||
|
this.updateValidation(this.props.validation);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValidation(validation) {
|
||||||
|
if (!this.monaco) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diagnostics = [];
|
||||||
|
|
||||||
|
if (validation && validation.errorDiagnostics) {
|
||||||
|
diagnostics = validation.errorDiagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
|
||||||
|
this.monaco.editor.setModelMarkers(model, 'editor', diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (newValue, e) => {
|
||||||
|
this.props.updateScript({ code: newValue });
|
||||||
|
|
||||||
|
this.validateCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCode = _.debounce(() => {
|
||||||
|
const code = this.props.code;
|
||||||
|
this.props.validateScript({ code });
|
||||||
|
}, 250, { leading: false, trailing: true })
|
||||||
|
|
||||||
|
onExecuteScriptPress = () => {
|
||||||
|
const code = this.props.code;
|
||||||
|
this.props.executeScript({ code });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDebugScriptPress = () => {
|
||||||
|
const code = this.props.code;
|
||||||
|
this.props.executeScript({ code, debug: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
render() {
|
||||||
|
const code = this.props.code;
|
||||||
|
const result = JSON.stringify(this.props.result, null, 2);
|
||||||
|
|
||||||
|
this.updateValidation(this.props.validation);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Run"
|
||||||
|
iconName={this.props.isExecuting ? icons.REFRESH : icons.SCRIPT_RUN}
|
||||||
|
isSpinning={this.props.isExecuting}
|
||||||
|
onPress={this.onExecuteScriptPress}
|
||||||
|
/>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Debug"
|
||||||
|
iconName={this.props.isDebugging ? icons.REFRESH : icons.SCRIPT_DEBUG}
|
||||||
|
isSpinning={this.props.isDebugging}
|
||||||
|
onPress={this.onDebugScriptPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
<Suspense fallback={<LoadingIndicator />}>
|
||||||
|
<div className={styles.split}>
|
||||||
|
<MonacoEditor
|
||||||
|
language="csharp"
|
||||||
|
theme="vs-light"
|
||||||
|
width="50%"
|
||||||
|
value={code}
|
||||||
|
options={DefaultOptions}
|
||||||
|
onChange={this.onChange}
|
||||||
|
editorDidMount={this.editorDidMount}
|
||||||
|
/>
|
||||||
|
<MonacoEditor
|
||||||
|
language="json"
|
||||||
|
theme="vs-light"
|
||||||
|
width="50%"
|
||||||
|
value={result}
|
||||||
|
options={DefaultResultOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptConsole.propTypes = {
|
||||||
|
isExecuting: PropTypes.bool.isRequired,
|
||||||
|
isDebugging: PropTypes.bool.isRequired,
|
||||||
|
isValidating: PropTypes.bool.isRequired,
|
||||||
|
code: PropTypes.string,
|
||||||
|
result: PropTypes.object,
|
||||||
|
error: PropTypes.object,
|
||||||
|
validation: PropTypes.object,
|
||||||
|
updateScript: PropTypes.func.isRequired,
|
||||||
|
validateScript: PropTypes.func.isRequired,
|
||||||
|
executeScript: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScriptConsole;
|
|
@ -0,0 +1,5 @@
|
||||||
|
.descriptionList {
|
||||||
|
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
|
||||||
|
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import styles from './Statistics.css';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
function formatValue(val, formatter) {
|
||||||
|
if (val === undefined) {
|
||||||
|
return 'n/a';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formatter) {
|
||||||
|
return formatter(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Statistics extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
process,
|
||||||
|
databaseMain,
|
||||||
|
databaseLog,
|
||||||
|
commandsExecuted
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend="Statistics">
|
||||||
|
<DescriptionList className={styles.descriptionList}>
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Up Time"
|
||||||
|
data={formatValue(process.startTime, (startTime) => formatTimeSpan(moment().diff(startTime)))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Processor Time"
|
||||||
|
data={formatValue(process.totalProcessorTime, formatTimeSpan)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Memory Working Set"
|
||||||
|
data={formatValue(process.workingSet, formatBytes)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Memory Virtual Size"
|
||||||
|
data={formatValue(process.virtualMemorySize, formatBytes)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Main Database Size"
|
||||||
|
data={formatValue(databaseMain.size, formatBytes)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Logs Database Size"
|
||||||
|
data={formatValue(databaseLog.size, formatBytes)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Commands Executed"
|
||||||
|
data={formatValue(commandsExecuted, formatBytes)}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Statistics.propTypes = {
|
||||||
|
process: PropTypes.object,
|
||||||
|
databaseMain: PropTypes.object,
|
||||||
|
databaseLog: PropTypes.object,
|
||||||
|
commandsExecuted: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
Statistics.defaultProps = {
|
||||||
|
process: {},
|
||||||
|
databaseMain: {},
|
||||||
|
databaseLog: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Statistics;
|
|
@ -0,0 +1,46 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import Statistics from './Statistics/Statistics';
|
||||||
|
|
||||||
|
class Status extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PageContent title="Diagnostic Status">
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Refresh"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={this.props.isStatusFetching}
|
||||||
|
onPress={this.props.onRefreshPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
<PageContentBody>
|
||||||
|
<Statistics
|
||||||
|
{...this.props.status}
|
||||||
|
/>
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Status.propTypes = {
|
||||||
|
status: PropTypes.object.isRequired,
|
||||||
|
isStatusFetching: PropTypes.bool.isRequired,
|
||||||
|
onRefreshPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Status;
|
|
@ -0,0 +1,59 @@
|
||||||
|
// @ts-check
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchStatus } from 'Store/Actions/diagnosticActions';
|
||||||
|
import Status from './Status';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.diagnostic.status,
|
||||||
|
(status) => {
|
||||||
|
return {
|
||||||
|
isStatusFetching: status.isFetching,
|
||||||
|
status: status.item
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
class DiagnosticConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
this.props.fetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Status
|
||||||
|
onRefreshPress={this.onRefreshPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagnosticConnector.propTypes = {
|
||||||
|
status: PropTypes.object.isRequired,
|
||||||
|
fetchStatus: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(DiagnosticConnector);
|
|
@ -81,6 +81,7 @@ import {
|
||||||
faSignOutAlt as fasSignOutAlt,
|
faSignOutAlt as fasSignOutAlt,
|
||||||
faSitemap as fasSitemap,
|
faSitemap as fasSitemap,
|
||||||
faSpinner as fasSpinner,
|
faSpinner as fasSpinner,
|
||||||
|
faStepForward as fasStepForward,
|
||||||
faSort as fasSort,
|
faSort as fasSort,
|
||||||
faSortDown as fasSortDown,
|
faSortDown as fasSortDown,
|
||||||
faSortUp as fasSortUp,
|
faSortUp as fasSortUp,
|
||||||
|
@ -126,6 +127,7 @@ export const CLONE = farClone;
|
||||||
export const COLLAPSE = fasChevronCircleUp;
|
export const COLLAPSE = fasChevronCircleUp;
|
||||||
export const COMPUTER = fasDesktop;
|
export const COMPUTER = fasDesktop;
|
||||||
export const DANGER = fasExclamationCircle;
|
export const DANGER = fasExclamationCircle;
|
||||||
|
export const DEBUG = fasBug;
|
||||||
export const DELETE = fasTrashAlt;
|
export const DELETE = fasTrashAlt;
|
||||||
export const DOWNLOAD = fasDownload;
|
export const DOWNLOAD = fasDownload;
|
||||||
export const DOWNLOADED = fasDownload;
|
export const DOWNLOADED = fasDownload;
|
||||||
|
@ -180,6 +182,8 @@ export const REORDER = fasBars;
|
||||||
export const RSS = fasRss;
|
export const RSS = fasRss;
|
||||||
export const SAVE = fasSave;
|
export const SAVE = fasSave;
|
||||||
export const SCHEDULED = farClock;
|
export const SCHEDULED = farClock;
|
||||||
|
export const SCRIPT_DEBUG = fasStepForward;
|
||||||
|
export const SCRIPT_RUN = fasPlay;
|
||||||
export const SCORE = fasUserPlus;
|
export const SCORE = fasUserPlus;
|
||||||
export const SEARCH = fasSearch;
|
export const SEARCH = fasSearch;
|
||||||
export const SERIES_CONTINUING = fasPlay;
|
export const SERIES_CONTINUING = fasPlay;
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import { set } from './baseActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'diagnostic';
|
||||||
|
const scriptSection = 'diagnostic.script';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
const exampleScript = `// Obtain the instance of ISeriesService
|
||||||
|
var seriesService = Resolve<ISeriesService>();
|
||||||
|
|
||||||
|
// Get all series
|
||||||
|
var series = seriesService.GetAllSeries();
|
||||||
|
|
||||||
|
// Find the top 5 highest rated ones
|
||||||
|
var top5 = series.Where(s => s.Ratings.Votes > 6)
|
||||||
|
.OrderByDescending(s => s.Ratings.Value)
|
||||||
|
.Take(5)
|
||||||
|
.Select(s => s.Title);
|
||||||
|
|
||||||
|
return new {
|
||||||
|
Top5 = top5,
|
||||||
|
Count = series.Count()
|
||||||
|
};`;
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
status: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
item: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
script: {
|
||||||
|
isExecuting: false,
|
||||||
|
isDebugging: false,
|
||||||
|
isValidating: false,
|
||||||
|
workspaceId: null,
|
||||||
|
code: exampleScript,
|
||||||
|
validation: null,
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_STATUS = 'diagnostic/status/fetchStatus';
|
||||||
|
export const UPDATE_SCRIPT = 'diagnostic/script/update';
|
||||||
|
export const VALIDATE_SCRIPT = 'diagnostic/script/validate';
|
||||||
|
export const EXECUTE_SCRIPT = 'diagnostic/script/execute';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchStatus = createThunk(FETCH_STATUS);
|
||||||
|
export const updateScript = createThunk(UPDATE_SCRIPT);
|
||||||
|
export const validateScript = createThunk(VALIDATE_SCRIPT);
|
||||||
|
export const executeScript = createThunk(EXECUTE_SCRIPT);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
[FETCH_STATUS]: createFetchHandler('diagnostic.status', '/diagnostic/status'),
|
||||||
|
|
||||||
|
[UPDATE_SCRIPT]: function(getState, payload, dispatch) {
|
||||||
|
const {
|
||||||
|
code
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section: scriptSection,
|
||||||
|
code
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
[VALIDATE_SCRIPT]: function(getState, payload, dispatch) {
|
||||||
|
const {
|
||||||
|
code
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section: scriptSection,
|
||||||
|
code,
|
||||||
|
isValidating: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
let ajaxOptions = null;
|
||||||
|
|
||||||
|
ajaxOptions = {
|
||||||
|
url: '/diagnostic/script/validate',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
code
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = createAjaxRequest(ajaxOptions).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(set({
|
||||||
|
section: scriptSection,
|
||||||
|
isValidating: false,
|
||||||
|
validation: data
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section: scriptSection,
|
||||||
|
isValidating: false,
|
||||||
|
validation: null,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[EXECUTE_SCRIPT]: function(getState, payload, dispatch) {
|
||||||
|
const {
|
||||||
|
code,
|
||||||
|
debug
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section: scriptSection,
|
||||||
|
code,
|
||||||
|
isExecuting: !debug,
|
||||||
|
isDebugging: debug
|
||||||
|
}));
|
||||||
|
|
||||||
|
let ajaxOptions = null;
|
||||||
|
|
||||||
|
ajaxOptions = {
|
||||||
|
url: '/diagnostic/script/execute',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
code,
|
||||||
|
debug
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = createAjaxRequest(ajaxOptions).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(set({
|
||||||
|
section: scriptSection,
|
||||||
|
isExecuting: false,
|
||||||
|
isDebugging: false,
|
||||||
|
result: (debug || data.error) ? data : data.returnValue,
|
||||||
|
validation: {
|
||||||
|
errorDiagnostics: data.errorDiagnostics
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section: scriptSection,
|
||||||
|
isExecuting: false,
|
||||||
|
isDebugging: false,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
}, defaultState, section);
|
|
@ -5,6 +5,7 @@ import * as calendar from './calendarActions';
|
||||||
import * as captcha from './captchaActions';
|
import * as captcha from './captchaActions';
|
||||||
import * as customFilters from './customFilterActions';
|
import * as customFilters from './customFilterActions';
|
||||||
import * as commands from './commandActions';
|
import * as commands from './commandActions';
|
||||||
|
import * as diagnostic from './diagnosticActions';
|
||||||
import * as episodes from './episodeActions';
|
import * as episodes from './episodeActions';
|
||||||
import * as episodeFiles from './episodeFileActions';
|
import * as episodeFiles from './episodeFileActions';
|
||||||
import * as episodeHistory from './episodeHistoryActions';
|
import * as episodeHistory from './episodeHistoryActions';
|
||||||
|
@ -36,6 +37,7 @@ export default [
|
||||||
captcha,
|
captcha,
|
||||||
commands,
|
commands,
|
||||||
customFilters,
|
customFilters,
|
||||||
|
diagnostic,
|
||||||
episodes,
|
episodes,
|
||||||
episodeFiles,
|
episodeFiles,
|
||||||
episodeHistory,
|
episodeHistory,
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
"mini-css-extract-plugin": "0.8.0",
|
"mini-css-extract-plugin": "0.8.0",
|
||||||
"mobile-detect": "1.4.3",
|
"mobile-detect": "1.4.3",
|
||||||
"moment": "2.24.0",
|
"moment": "2.24.0",
|
||||||
|
"monaco-editor": "0.20.0",
|
||||||
"mousetrap": "1.6.3",
|
"mousetrap": "1.6.3",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
|
@ -100,6 +101,7 @@
|
||||||
"react-google-recaptcha": "1.1.0",
|
"react-google-recaptcha": "1.1.0",
|
||||||
"react-lazyload": "2.6.2",
|
"react-lazyload": "2.6.2",
|
||||||
"react-measure": "1.4.7",
|
"react-measure": "1.4.7",
|
||||||
|
"react-monaco-editor": "0.36.0",
|
||||||
"react-popper": "1.3.3",
|
"react-popper": "1.3.3",
|
||||||
"react-redux": "7.1.0",
|
"react-redux": "7.1.0",
|
||||||
"react-router": "5.0.1",
|
"react-router": "5.0.1",
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Composition;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Core.Diagnostics;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.DiagnosticsTests
|
||||||
|
{
|
||||||
|
public class DiagnosticScriptRunnerFixture : CoreTest<DiagnosticScriptRunner>
|
||||||
|
{
|
||||||
|
[TestCase("1 ++ 2", "(1,6): error CS1002: ; expected")]
|
||||||
|
[TestCase("a = 2", "(1,1): error CS0103: The name 'a' does not exist in the current context")]
|
||||||
|
[TestCase("Logger.NoMethod()", "(1,8): error CS1061: 'Logger' does not contain a definition for 'NoMethod' and no accessible extension method 'NoMethod' accepting a first argument of type 'Logger' could be found (are you missing a using directive or an assembly reference?)")]
|
||||||
|
public void Validate_should_list_compiler_errors(string source, string message)
|
||||||
|
{
|
||||||
|
var result = Subject.Validate(new ScriptRequest { Code = source, Debug = true });
|
||||||
|
|
||||||
|
result.HasErrors.Should().BeTrue();
|
||||||
|
result.Messages.First().FullMessage.Should().Be("ScriptConsole.cs" + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Execute_should_show_context()
|
||||||
|
{
|
||||||
|
var result = Subject.Execute(new ScriptRequest { Code = "var a = 12;" });
|
||||||
|
|
||||||
|
result.ReturnValue.Should().BeNull();
|
||||||
|
result.Variables.Should().HaveCount(1);
|
||||||
|
result.Variables["a"].Should().Be(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Execute_should_allow_continuations()
|
||||||
|
{
|
||||||
|
var result = Subject.Execute(new ScriptRequest { Code = "var a = 12;", StoreState = true });
|
||||||
|
|
||||||
|
var result2 = Subject.Execute(new ScriptRequest { Code = "var b = a + 2;", StateId = result.StateId });
|
||||||
|
|
||||||
|
result2.Variables.Should().HaveCount(2);
|
||||||
|
result2.Variables["b"].Should().Be(14);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Execute_should_resolve_interfaces_Common()
|
||||||
|
{
|
||||||
|
Mocker.SetConstant<IContainer>(new AutoMoqerContainer(Mocker));
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(v => v.FolderExists("C:\test"))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var result = Subject.Execute(new ScriptRequest { Code = @"
|
||||||
|
var diskProvider = Resolve<IDiskProvider>();
|
||||||
|
|
||||||
|
return diskProvider.FolderExists(""C:\test"") ? ""yes"" : ""no"";
|
||||||
|
" });
|
||||||
|
|
||||||
|
result.ReturnValue.Should().Be("yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Execute_should_resolve_interfaces_Core()
|
||||||
|
{
|
||||||
|
Mocker.SetConstant<IContainer>(new AutoMoqerContainer(Mocker));
|
||||||
|
|
||||||
|
Mocker.GetMock<ISeriesService>()
|
||||||
|
.Setup(v => v.GetAllSeries())
|
||||||
|
.Returns(Builder<Series>.CreateListOfSize(5).BuildList());
|
||||||
|
|
||||||
|
var result = Subject.Execute(new ScriptRequest { Code = @"
|
||||||
|
var seriesService = Resolve<ISeriesService>();
|
||||||
|
|
||||||
|
foreach (var series in seriesService.GetAllSeries())
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
Logger.Debug($""Processing series {series.Title}"");
|
||||||
|
}
|
||||||
|
return ""done"";
|
||||||
|
" });
|
||||||
|
|
||||||
|
result.ReturnValue.Should().Be("done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ namespace NzbDrone.Core.Datastore
|
||||||
{
|
{
|
||||||
IDataMapper GetDataMapper();
|
IDataMapper GetDataMapper();
|
||||||
Version Version { get; }
|
Version Version { get; }
|
||||||
|
long Size { get; }
|
||||||
void Vacuum();
|
void Vacuum();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +40,16 @@ namespace NzbDrone.Core.Datastore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long Size
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var page_count = _datamapperFactory().ExecuteScalar("PRAGMA page_count;");
|
||||||
|
var page_size = _datamapperFactory().ExecuteScalar("PRAGMA page_size;");
|
||||||
|
return Convert.ToInt64(page_count) * Convert.ToInt64(page_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Vacuum()
|
public void Vacuum()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -23,6 +23,8 @@ namespace NzbDrone.Core.Datastore
|
||||||
}
|
}
|
||||||
|
|
||||||
public Version Version => _database.Version;
|
public Version Version => _database.Version;
|
||||||
|
public long Size => _database.Size;
|
||||||
|
|
||||||
|
|
||||||
public void Vacuum()
|
public void Vacuum()
|
||||||
{
|
{
|
||||||
|
|
|
@ -23,6 +23,7 @@ namespace NzbDrone.Core.Datastore
|
||||||
}
|
}
|
||||||
|
|
||||||
public Version Version => _database.Version;
|
public Version Version => _database.Version;
|
||||||
|
public long Size => _database.Size;
|
||||||
|
|
||||||
public void Vacuum()
|
public void Vacuum()
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System.IO;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Diagnostics
|
||||||
|
{
|
||||||
|
public interface IDiagnosticFeatureSwitches
|
||||||
|
{
|
||||||
|
bool ScriptConsoleEnabled { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiagnosticFeatureSwitches : IDiagnosticFeatureSwitches
|
||||||
|
{
|
||||||
|
private IDiskProvider _diskProvider;
|
||||||
|
private IAppFolderInfo _appFolderInfo;
|
||||||
|
|
||||||
|
public DiagnosticFeatureSwitches(IDiskProvider diskProvider, IAppFolderInfo appFolderInfo)
|
||||||
|
{
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
_appFolderInfo = appFolderInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ScriptConsoleEnabled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Only allow this if the 'debugscripts' config folder exists.
|
||||||
|
// Scripting is a significant security risk with only an api key for protection.
|
||||||
|
if (!_diskProvider.FolderExists(Path.Combine(_appFolderInfo.AppDataFolder, "debugscripts")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,397 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||||
|
using Microsoft.CodeAnalysis.Scripting;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.Composition;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Diagnostics
|
||||||
|
{
|
||||||
|
public interface IDiagnosticScriptRunner
|
||||||
|
{
|
||||||
|
ScriptValidationResult Validate(ScriptRequest request);
|
||||||
|
ScriptExecutionResult Execute(ScriptRequest request);
|
||||||
|
}
|
||||||
|
internal class CompilationContext
|
||||||
|
{
|
||||||
|
public HashSet<string> GlobalUsings { get; set; }
|
||||||
|
public ScriptOptions Options { get; set; }
|
||||||
|
public string Code { get; set; }
|
||||||
|
public Script Script { get; set; }
|
||||||
|
public Compilation LastCompilation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiagnosticScriptRunner : IDiagnosticScriptRunner
|
||||||
|
{
|
||||||
|
private static readonly Regex _regexResolve = new Regex(@"=\s+Resolve<(I\w+)>", RegexOptions.Compiled);
|
||||||
|
private static readonly Assembly[] _assemblies = new[] {
|
||||||
|
typeof(AppFolderInfo).Assembly,
|
||||||
|
typeof(DiagnosticScriptRunner).Assembly
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly IContainer _container;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
private readonly ICached<object> _scriptStateCache;
|
||||||
|
|
||||||
|
private WeakReference<CompilationContext> _lastCompilation;
|
||||||
|
|
||||||
|
public DiagnosticScriptRunner(IContainer container, ICacheManager cacheManager, Logger logger)
|
||||||
|
{
|
||||||
|
_container = container;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Note: using object instead of ScriptState to avoid the Scripting assembly to be loaded on startup.
|
||||||
|
_scriptStateCache = cacheManager.GetCache<object>(GetType());
|
||||||
|
|
||||||
|
_lastCompilation = new WeakReference<CompilationContext>(null);
|
||||||
|
|
||||||
|
CheckScriptingAssemblyDelayLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckScriptingAssemblyDelayLoad()
|
||||||
|
{
|
||||||
|
var scriptingLoaded = AppDomain.CurrentDomain.GetAssemblies().Any(v => v.FullName.Contains("Microsoft.CodeAnalysis"));
|
||||||
|
if (scriptingLoaded)
|
||||||
|
{
|
||||||
|
// If we reach this code, then the class has been changed and as a result the Microsoft.CodeAnalysis.CSharp.Scripting assembly
|
||||||
|
// was loaded on startup. This should be avoided since it takes more memory and is not used in normal situations.
|
||||||
|
if (!RuntimeInfo.IsProduction)
|
||||||
|
{
|
||||||
|
_logger.Error("Scripting assembly loaded prematurely.");
|
||||||
|
}
|
||||||
|
if (Debugger.IsAttached)
|
||||||
|
{
|
||||||
|
Debugger.Break();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptValidationResult Validate(ScriptRequest request)
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
var globalUsings = GetGlobalUsings(request.Code);
|
||||||
|
|
||||||
|
_lastCompilation.TryGetTarget(out var lastCompilation);
|
||||||
|
|
||||||
|
// Swapping SyntaxTree is significantly faster and uses less memory
|
||||||
|
if (lastCompilation != null && lastCompilation.Code == request.Code)
|
||||||
|
{
|
||||||
|
// Unchanged
|
||||||
|
}
|
||||||
|
else if (lastCompilation != null && lastCompilation.GlobalUsings == globalUsings)
|
||||||
|
{
|
||||||
|
var newSyntaxTree = CSharpSyntaxTree.ParseText(request.Code, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
|
||||||
|
|
||||||
|
lastCompilation.Script = null;
|
||||||
|
lastCompilation.Code = request.Code;
|
||||||
|
lastCompilation.LastCompilation = lastCompilation.LastCompilation.ReplaceSyntaxTree(lastCompilation.LastCompilation.SyntaxTrees.First(), newSyntaxTree);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var options = GetOptions(globalUsings, request.Debug);
|
||||||
|
|
||||||
|
var script = CSharpScript.Create(request.Code, options, globalsType: typeof(ScriptContext));
|
||||||
|
|
||||||
|
var compilation = script.GetCompilation();
|
||||||
|
|
||||||
|
lastCompilation = new CompilationContext
|
||||||
|
{
|
||||||
|
GlobalUsings = globalUsings,
|
||||||
|
Options = options,
|
||||||
|
Code = request.Code,
|
||||||
|
Script = script,
|
||||||
|
LastCompilation = compilation
|
||||||
|
};
|
||||||
|
|
||||||
|
_lastCompilation.SetTarget(lastCompilation);
|
||||||
|
}
|
||||||
|
|
||||||
|
var diagnostics = lastCompilation.LastCompilation.GetDiagnostics();
|
||||||
|
|
||||||
|
return new ScriptValidationResult
|
||||||
|
{
|
||||||
|
Messages = diagnostics.Select(v => new ScriptDiagnostic(v)).ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptExecutionResult Execute(ScriptRequest request)
|
||||||
|
{
|
||||||
|
if (request.StateId != null)
|
||||||
|
{
|
||||||
|
return ExecuteAsync(request, request.StateId).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ExecuteAsync(request).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ScriptExecutionResult> ExecuteAsync(ScriptRequest request)
|
||||||
|
{
|
||||||
|
Script script;
|
||||||
|
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
var globalUsings = GetGlobalUsings(request.Code);
|
||||||
|
|
||||||
|
_lastCompilation.TryGetTarget(out var lastCompilation);
|
||||||
|
|
||||||
|
if (lastCompilation != null && lastCompilation.Code == request.Code && lastCompilation.Script != null &&
|
||||||
|
lastCompilation.Options.EmitDebugInformation == request.Debug)
|
||||||
|
{
|
||||||
|
script = lastCompilation.Script;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = GetOptions(globalUsings, request.Debug);
|
||||||
|
|
||||||
|
// Note: Using classic Task pipeline since async-await early loads the Scripts assembly
|
||||||
|
script = CSharpScript.Create(request.Code, options, globalsType: typeof(ScriptContext));
|
||||||
|
|
||||||
|
var compilation = script.GetCompilation();
|
||||||
|
|
||||||
|
lastCompilation = new CompilationContext
|
||||||
|
{
|
||||||
|
GlobalUsings = globalUsings,
|
||||||
|
Options = options,
|
||||||
|
Code = request.Code,
|
||||||
|
Script = script,
|
||||||
|
LastCompilation = compilation
|
||||||
|
};
|
||||||
|
|
||||||
|
_lastCompilation.SetTarget(lastCompilation);
|
||||||
|
}
|
||||||
|
catch (CompilationErrorException ex)
|
||||||
|
{
|
||||||
|
return Task.FromResult(GetResult(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return script.RunAsync(new ScriptContext(_container, _logger), ex => true).ContinueWith(t =>
|
||||||
|
{
|
||||||
|
var state = t.Result;
|
||||||
|
|
||||||
|
if (state.Exception != null)
|
||||||
|
{
|
||||||
|
return GetResult(state.Exception, request.Code);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return GetResult(state, request.StoreState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (CompilationErrorException ex)
|
||||||
|
{
|
||||||
|
return Task.FromResult(GetResult(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ScriptExecutionResult> ExecuteAsync(ScriptRequest request, string stateId)
|
||||||
|
{
|
||||||
|
var options = GetOptions(GetGlobalUsings(request.Code), request.Debug);
|
||||||
|
|
||||||
|
var script = GetState(stateId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Note: Using classic Task pipeline since async-await early loads the Scripts assembly
|
||||||
|
return script.ContinueWithAsync(request.Code, options, ex => true).ContinueWith(t =>
|
||||||
|
{
|
||||||
|
var state = t.Result;
|
||||||
|
|
||||||
|
if (state.Exception != null)
|
||||||
|
{
|
||||||
|
return GetResult(state.Exception, request.Code);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return GetResult(state, request.StoreState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (CompilationErrorException ex)
|
||||||
|
{
|
||||||
|
return Task.FromResult(GetResult(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<string> GetGlobalUsings(string source)
|
||||||
|
{
|
||||||
|
var result = new HashSet<string>();
|
||||||
|
|
||||||
|
// Make the syntax easier by parsing Resolve<I..> and auto add using
|
||||||
|
var matches = _regexResolve.Matches(source);
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
foreach (var ns in ResolveNamespaces(match.Groups[1].Value))
|
||||||
|
{
|
||||||
|
result.Add(ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptOptions GetOptions(HashSet<string> globalUsings, bool debug = false)
|
||||||
|
{
|
||||||
|
var options = ScriptOptions.Default
|
||||||
|
.AddReferences(_assemblies)
|
||||||
|
.AddImports(typeof(Task).Namespace)
|
||||||
|
.AddImports(typeof(Enumerable).Namespace);
|
||||||
|
|
||||||
|
if (debug)
|
||||||
|
{
|
||||||
|
options = options.WithEmitDebugInformation(true)
|
||||||
|
.WithFilePath("ScriptConsole.cs")
|
||||||
|
.WithFileEncoding(Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the syntax easier by parsing Resolve<I..> and auto add using
|
||||||
|
foreach (var ns in globalUsings)
|
||||||
|
{
|
||||||
|
options = options.AddImports(ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> ResolveNamespaces(string type)
|
||||||
|
{
|
||||||
|
var types = _assemblies
|
||||||
|
.SelectMany(v => v.GetExportedTypes())
|
||||||
|
.Where(v => v.Name == type);
|
||||||
|
|
||||||
|
var namespaces = types.Select(v => v.Namespace)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return namespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptExecutionResult GetResult(ScriptState state, bool storeState)
|
||||||
|
{
|
||||||
|
var variables = state.Variables.Where(v => !v.Type.IsInterface || !v.Type.Namespace.StartsWith("NzbDrone")).ToDictionary(v => v.Name, v => v.Value);
|
||||||
|
|
||||||
|
var result = new ScriptExecutionResult
|
||||||
|
{
|
||||||
|
StateId = storeState ? StoreState(state) : null,
|
||||||
|
ReturnValue = state.ReturnValue,
|
||||||
|
Variables = variables.Any() ? variables : null
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptExecutionResult GetResult(CompilationErrorException ex)
|
||||||
|
{
|
||||||
|
return new ScriptExecutionResult
|
||||||
|
{
|
||||||
|
Exception = ex,
|
||||||
|
Validation = new ScriptValidationResult { Messages = ex.Diagnostics.Select(v => new ScriptDiagnostic(v)).ToArray() }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptExecutionResult GetResult(Exception ex, string code)
|
||||||
|
{
|
||||||
|
var result = new ScriptExecutionResult
|
||||||
|
{
|
||||||
|
Exception = ex
|
||||||
|
};
|
||||||
|
|
||||||
|
StackFrame firstScriptFrame = null;
|
||||||
|
var stackTrace = new StackTrace(ex, true);
|
||||||
|
|
||||||
|
for (int i = 0; i < stackTrace.FrameCount; i++)
|
||||||
|
{
|
||||||
|
var frame = stackTrace.GetFrame(i);
|
||||||
|
if (frame.GetFileName() == "ScriptConsole.cs" && result.Validation == null)
|
||||||
|
{
|
||||||
|
firstScriptFrame = frame;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the full message text till the scripting runtime
|
||||||
|
var fullMessage = ex.ToString();
|
||||||
|
var fullMessageLines = fullMessage.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
||||||
|
var idx = Array.FindIndex(fullMessageLines, 1, v => v.Contains("RunSubmissionsAsync")) - 2;
|
||||||
|
if (idx > 2 && fullMessageLines[idx - 1].StartsWith("---"))
|
||||||
|
{
|
||||||
|
idx--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx > 1)
|
||||||
|
{
|
||||||
|
fullMessage = string.Join("\n", fullMessageLines.Take(idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = code.Split('\n');
|
||||||
|
ScriptDiagnostic diagnostic;
|
||||||
|
if (firstScriptFrame != null)
|
||||||
|
{
|
||||||
|
var startLineNumber = firstScriptFrame.GetFileLineNumber();
|
||||||
|
var startColumn = firstScriptFrame.GetFileColumnNumber();
|
||||||
|
var endLineNumber = startLineNumber;
|
||||||
|
var endColumn = startColumn == 1 ? lines[startLineNumber - 1].Length : startColumn;
|
||||||
|
diagnostic = new ScriptDiagnostic(ex, startLineNumber, startColumn, endLineNumber, endColumn, fullMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
diagnostic = new ScriptDiagnostic(ex, 1, 1, lines.Length, lines.Last().Length, fullMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Validation = new ScriptValidationResult
|
||||||
|
{
|
||||||
|
Messages = new[]
|
||||||
|
{
|
||||||
|
diagnostic
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptState GetState(string stateID)
|
||||||
|
{
|
||||||
|
var state = _scriptStateCache.Find(stateID);
|
||||||
|
|
||||||
|
if (state == null)
|
||||||
|
throw new KeyNotFoundException($"ScriptState {stateID} no longer exists");
|
||||||
|
|
||||||
|
return state as ScriptState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveState(string stateID)
|
||||||
|
{
|
||||||
|
_scriptStateCache.Remove(stateID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string StoreState(ScriptState state)
|
||||||
|
{
|
||||||
|
var key = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
_scriptStateCache.Set(key, state, TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Composition;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Diagnostics
|
||||||
|
{
|
||||||
|
public class ScriptContext
|
||||||
|
{
|
||||||
|
private readonly IContainer _container;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public ScriptContext(IContainer container, Logger logger)
|
||||||
|
{
|
||||||
|
_container = container;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Logger Logger => _logger;
|
||||||
|
|
||||||
|
public T Resolve<T>()
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return _container.Resolve<T>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Diagnostics
|
||||||
|
{
|
||||||
|
public enum ScriptDiagnosticSeverity
|
||||||
|
{
|
||||||
|
Info = 1,
|
||||||
|
Warning = 2,
|
||||||
|
Error = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScriptDiagnostic
|
||||||
|
{
|
||||||
|
public int StartLineNumber { get; set; }
|
||||||
|
public int StartColumn { get; set; }
|
||||||
|
public int EndLineNumber { get; set; }
|
||||||
|
public int EndColumn { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public ScriptDiagnosticSeverity Severity { get; set; }
|
||||||
|
public string FullMessage { get; set; }
|
||||||
|
|
||||||
|
public ScriptDiagnostic()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptDiagnostic(Exception ex, int startLineNumber, int startColumn, int endLineNumber, int endColumn, string fullMessage)
|
||||||
|
{
|
||||||
|
StartLineNumber = startLineNumber;
|
||||||
|
StartColumn = startColumn;
|
||||||
|
EndLineNumber = endLineNumber;
|
||||||
|
EndColumn = endColumn;
|
||||||
|
Message = ex.Message;
|
||||||
|
Severity = ScriptDiagnosticSeverity.Error;
|
||||||
|
FullMessage = fullMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptDiagnostic(Diagnostic diagnostic)
|
||||||
|
{
|
||||||
|
var lineSpan = diagnostic.Location.GetLineSpan();
|
||||||
|
|
||||||
|
StartLineNumber = lineSpan.StartLinePosition.Line + 1;
|
||||||
|
StartColumn = lineSpan.StartLinePosition.Character + 1;
|
||||||
|
EndLineNumber = lineSpan.EndLinePosition.Line + 1;
|
||||||
|
EndColumn = lineSpan.EndLinePosition.Character + 1;
|
||||||
|
Message = diagnostic.GetMessage();
|
||||||
|
Severity = (ScriptDiagnosticSeverity)diagnostic.Severity;
|
||||||
|
FullMessage = diagnostic.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Diagnostics
|
||||||
|
{
|
||||||
|
public class ScriptExecutionResult
|
||||||
|
{
|
||||||
|
public string StateId { get; set; }
|
||||||
|
|
||||||
|
public Exception Exception { get; set; }
|
||||||
|
public object ReturnValue { get; set; }
|
||||||
|
public Dictionary<string, object> Variables { get; set; }
|
||||||
|
public ScriptValidationResult Validation { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace NzbDrone.Core.Diagnostics
|
||||||
|
{
|
||||||
|
public class ScriptRequest
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Code { get; set; }
|
||||||
|
public string StateId { get; set; }
|
||||||
|
public bool Debug { get; set; }
|
||||||
|
public bool StoreState { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Diagnostics
|
||||||
|
{
|
||||||
|
public class ScriptValidationResult
|
||||||
|
{
|
||||||
|
public ScriptDiagnostic[] Messages { get; set; }
|
||||||
|
|
||||||
|
public bool HasWarnings => Messages.Any(v => v.Severity == ScriptDiagnosticSeverity.Warning);
|
||||||
|
public bool HasErrors => Messages.Any(v => v.Severity == ScriptDiagnosticSeverity.Error);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@
|
||||||
<PackageReference Include="FluentValidation" Version="8.4.0" />
|
<PackageReference Include="FluentValidation" Version="8.4.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0007" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0007" />
|
||||||
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.6.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||||
<PackageReference Include="NLog" Version="4.6.6" />
|
<PackageReference Include="NLog" Version="4.6.6" />
|
||||||
<PackageReference Include="OAuth" Version="1.0.3" />
|
<PackageReference Include="OAuth" Version="1.0.3" />
|
||||||
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Diagnostics;
|
||||||
|
using NzbDrone.Integration.Test.Client;
|
||||||
|
using RestSharp;
|
||||||
|
using Sonarr.Api.V3.Diagnostics;
|
||||||
|
|
||||||
|
namespace NzbDrone.Integration.Test.DiagnosticsTests
|
||||||
|
{
|
||||||
|
public class DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
public string Code { get; set; }
|
||||||
|
public bool? Debug { get; set; }
|
||||||
|
|
||||||
|
public object ReturnValue { get; set; }
|
||||||
|
public Dictionary<string, object> DebugVariables { get; set; }
|
||||||
|
|
||||||
|
public string Error { get; set; }
|
||||||
|
public List<ScriptDiagnostic> ErrorDiagnostics { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiagnosticsScriptClient : ClientBase
|
||||||
|
{
|
||||||
|
public DiagnosticsScriptClient(IRestClient restClient, string apiKey)
|
||||||
|
: base(restClient, apiKey, "v3/diagnostic/script")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiagnosticScriptResource Execute(DiagnosticScriptResource body, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
var request = BuildRequest("execute");
|
||||||
|
request.Method = Method.POST;
|
||||||
|
request.AddJsonBody(body);
|
||||||
|
return Execute<DiagnosticScriptResource>(request, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiagnosticsScriptModuleFixture : IntegrationTest
|
||||||
|
{
|
||||||
|
DiagnosticsScriptClient DiagScript { get; set; }
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
DiagScript = new DiagnosticsScriptClient(RestClient, ApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenEnabledFeature(bool enabled = true)
|
||||||
|
{
|
||||||
|
var debugscripts = Path.Combine(_runner.AppData, "debugscripts");
|
||||||
|
|
||||||
|
if (enabled && !Directory.Exists(debugscripts))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(debugscripts);
|
||||||
|
}
|
||||||
|
else if (!enabled && Directory.Exists(debugscripts))
|
||||||
|
{
|
||||||
|
Directory.Delete(debugscripts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_allow_access_without_debugscripts_dir()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature(false);
|
||||||
|
|
||||||
|
DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "return \"abc\";"
|
||||||
|
}, HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_allow_access_with_debugscripts_dir()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature(true);
|
||||||
|
|
||||||
|
var result = DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "return \"abc\";"
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ReturnValue.Should().Be("abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_variables_for_debug()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature();
|
||||||
|
|
||||||
|
var result = DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "var a = \"abc\";",
|
||||||
|
Debug = true
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ReturnValue.Should().BeNull();
|
||||||
|
result.DebugVariables.Should().Contain("a", "abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_include_variables_without_debug()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature();
|
||||||
|
|
||||||
|
var result = DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "var a = \"abc\";"
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ReturnValue.Should().BeNull();
|
||||||
|
result.DebugVariables.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_report_compile_errors_with_debug()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature();
|
||||||
|
|
||||||
|
var result = DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "var a = \"abc\" + b;",
|
||||||
|
Debug = true
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ReturnValue.Should().BeNull();
|
||||||
|
result.DebugVariables.Should().BeNull();
|
||||||
|
result.Error.Should().Be("ScriptConsole.cs(1,17): error CS0103: The name 'b' does not exist in the current context");
|
||||||
|
result.ErrorDiagnostics.Should().NotBeNull();
|
||||||
|
result.ErrorDiagnostics.First().Message.Should().Be("The name 'b' does not exist in the current context");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_report_compile_errors_without_debug()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature();
|
||||||
|
|
||||||
|
var result = DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "var a = \"abc\" + b;"
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ReturnValue.Should().BeNull();
|
||||||
|
result.DebugVariables.Should().BeNull();
|
||||||
|
result.Error.Should().Be("(1,17): error CS0103: The name 'b' does not exist in the current context");
|
||||||
|
result.ErrorDiagnostics.Should().NotBeNull();
|
||||||
|
result.ErrorDiagnostics.First().Message.Should().Be("The name 'b' does not exist in the current context");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_report_execution_errors_with_debug()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature();
|
||||||
|
|
||||||
|
var result = DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "var seriesService = Resolve<ISeriesService>();\nseriesService.AddSeries((Series)null);",
|
||||||
|
Debug = true
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ReturnValue.Should().BeNull();
|
||||||
|
result.DebugVariables.Should().BeNull();
|
||||||
|
result.Error.Should().StartWith("Object reference not set to an instance of an object");
|
||||||
|
result.ErrorDiagnostics.Should().NotBeNull();
|
||||||
|
result.ErrorDiagnostics.First().StartLineNumber.Should().Be(2);
|
||||||
|
result.ErrorDiagnostics.First().StartColumn.Should().Be(1);
|
||||||
|
result.ErrorDiagnostics.First().Message.Should().StartWith("Object reference not set to an instance of an object");
|
||||||
|
result.ErrorDiagnostics.First().FullMessage.Should().StartWith("System.NullReferenceException: Object reference not set to an instance of an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_report_execution_errors_without_debug()
|
||||||
|
{
|
||||||
|
GivenEnabledFeature();
|
||||||
|
|
||||||
|
var result = DiagScript.Execute(new DiagnosticScriptResource
|
||||||
|
{
|
||||||
|
Code = "var seriesService = Resolve<ISeriesService>();\nseriesService.AddSeries((Series)null);",
|
||||||
|
});
|
||||||
|
|
||||||
|
result.ReturnValue.Should().BeNull();
|
||||||
|
result.DebugVariables.Should().BeNull();
|
||||||
|
result.Error.Should().StartWith("Object reference not set to an instance of an object");
|
||||||
|
result.ErrorDiagnostics.Should().NotBeNull();
|
||||||
|
result.ErrorDiagnostics.First().StartLineNumber.Should().Be(1);
|
||||||
|
result.ErrorDiagnostics.First().EndLineNumber.Should().Be(2);
|
||||||
|
result.ErrorDiagnostics.First().EndColumn.Should().Be(38);
|
||||||
|
result.ErrorDiagnostics.First().Message.Should().StartWith("Object reference not set to an instance of an object");
|
||||||
|
result.ErrorDiagnostics.First().FullMessage.Should().StartWith("System.NullReferenceException: Object reference not set to an instance of an object");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,5 +9,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Api\Sonarr.Api.csproj" />
|
<ProjectReference Include="..\NzbDrone.Api\Sonarr.Api.csproj" />
|
||||||
<ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" />
|
<ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Sonarr.Api.V3\Sonarr.Api.V3.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Common.Composition;
|
||||||
|
|
||||||
|
namespace NzbDrone.Test.Common
|
||||||
|
{
|
||||||
|
public class AutoMoqerContainer : IContainer
|
||||||
|
{
|
||||||
|
private readonly AutoMoq.AutoMoqer _autoMoqer;
|
||||||
|
|
||||||
|
public AutoMoqerContainer(AutoMoq.AutoMoqer autoMoqer)
|
||||||
|
{
|
||||||
|
_autoMoqer = autoMoqer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Type> GetImplementations(Type contractType)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTypeRegistered(Type type)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register<T>(T instance) where T : class
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(Type serviceType, Type implementationType)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register<TService>(Func<IContainer, TService> factory) where TService : class
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterAllAsSingleton(Type registrationType, IEnumerable<Type> implementationList)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSingleton(Type service, Type implementation)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Resolve<T>() where T : class
|
||||||
|
{
|
||||||
|
return _autoMoqer.Resolve<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Resolve(Type type)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<T> ResolveAll<T>() where T : class
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
void IContainer.Register<TService, TImplementation>()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Nancy;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Diagnostics;
|
||||||
|
using Sonarr.Api.V3;
|
||||||
|
using Sonarr.Http.Extensions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.V3.Diagnostics
|
||||||
|
{
|
||||||
|
public class DiagnosticStatusModule : SonarrV3Module
|
||||||
|
{
|
||||||
|
private readonly IDiagnosticFeatureSwitches _featureSwitches;
|
||||||
|
private readonly IMainDatabase _mainDatabase;
|
||||||
|
private readonly ILogDatabase _logDatabase;
|
||||||
|
|
||||||
|
public DiagnosticStatusModule(IDiagnosticFeatureSwitches featureSwitches,
|
||||||
|
IMainDatabase mainDatabase,
|
||||||
|
ILogDatabase logDatabase)
|
||||||
|
: base("diagnostic")
|
||||||
|
{
|
||||||
|
_featureSwitches = featureSwitches;
|
||||||
|
_mainDatabase = mainDatabase;
|
||||||
|
_logDatabase = logDatabase;
|
||||||
|
|
||||||
|
Get("/status", x => GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetStatus()
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Process = GetProcessStats(),
|
||||||
|
DatabaseMain = GetDatabaseStats(_mainDatabase),
|
||||||
|
DatabaseLog = GetDatabaseStats(_logDatabase),
|
||||||
|
CommandsExecuted = (long?)null,
|
||||||
|
ScriptConsoleEnabled = _featureSwitches.ScriptConsoleEnabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetProcessStats()
|
||||||
|
{
|
||||||
|
var process = Process.GetCurrentProcess();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
StartTime = process.StartTime,
|
||||||
|
TotalProcessorTime = process.TotalProcessorTime.TotalMilliseconds,
|
||||||
|
WorkingSet = process.WorkingSet64,
|
||||||
|
VirtualMemorySize = process.VirtualMemorySize64,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetDatabaseStats(IDatabase database)
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Size = database.Size,
|
||||||
|
Version = database.Version
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Nancy;
|
||||||
|
using Nancy.Extensions;
|
||||||
|
using NzbDrone.Core.Diagnostics;
|
||||||
|
using Sonarr.Http.Extensions;
|
||||||
|
|
||||||
|
namespace Sonarr.Api.V3.Diagnostics
|
||||||
|
{
|
||||||
|
public class ScriptRunnerModule : SonarrV3Module
|
||||||
|
{
|
||||||
|
private readonly IDiagnosticScriptRunner _scriptRunner;
|
||||||
|
private readonly IDiagnosticFeatureSwitches _featureSwitches;
|
||||||
|
|
||||||
|
public ScriptRunnerModule(IDiagnosticScriptRunner scriptRunner,
|
||||||
|
IDiagnosticFeatureSwitches featureSwitches)
|
||||||
|
: base("diagnostic/script")
|
||||||
|
{
|
||||||
|
_scriptRunner = scriptRunner;
|
||||||
|
_featureSwitches = featureSwitches;
|
||||||
|
|
||||||
|
Post("/validate", x => ValidateScript(x));
|
||||||
|
Post("/execute", x => ExecuteScript(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptRequest ParseRequest()
|
||||||
|
{
|
||||||
|
if (Request.Headers.ContentType == "application/json")
|
||||||
|
{
|
||||||
|
return Request.Body.FromJson<ScriptRequest>();
|
||||||
|
}
|
||||||
|
else if (Request.Headers.ContentType == "text/plain")
|
||||||
|
{
|
||||||
|
return new ScriptRequest { Code = Context.Request.Body.AsString() };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Request.Body.FromJson<ScriptRequest>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ValidateScript(dynamic options)
|
||||||
|
{
|
||||||
|
if (!_featureSwitches.ScriptConsoleEnabled)
|
||||||
|
{
|
||||||
|
return new NotFoundResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = ParseRequest();
|
||||||
|
|
||||||
|
var result = _scriptRunner.Validate(request);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
ErrorDiagnostics = result.Messages?.ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ExecuteScript(dynamic options)
|
||||||
|
{
|
||||||
|
if (!_featureSwitches.ScriptConsoleEnabled)
|
||||||
|
{
|
||||||
|
return new NotFoundResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = ParseRequest();
|
||||||
|
|
||||||
|
var result = _scriptRunner.Execute(request);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
ResultStateId = result.StateId,
|
||||||
|
ReturnValue = result.ReturnValue,
|
||||||
|
DebugVariables = request.Debug ? result.Variables : null,
|
||||||
|
Error = result.Exception?.Message,
|
||||||
|
ErrorDiagnostics = result.Validation?.Messages?.ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
yarn.lock
22
yarn.lock
|
@ -1080,6 +1080,14 @@
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
csstype "^2.2.0"
|
csstype "^2.2.0"
|
||||||
|
|
||||||
|
"@types/react@^16.x":
|
||||||
|
version "16.9.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
|
||||||
|
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
csstype "^2.2.0"
|
||||||
|
|
||||||
"@types/shallowequal@^1.1.1":
|
"@types/shallowequal@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/shallowequal/-/shallowequal-1.1.1.tgz#aad262bb3f2b1257d94c71d545268d592575c9b1"
|
resolved "https://registry.yarnpkg.com/@types/shallowequal/-/shallowequal-1.1.1.tgz#aad262bb3f2b1257d94c71d545268d592575c9b1"
|
||||||
|
@ -5922,6 +5930,11 @@ moment@2.24.0:
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
||||||
|
|
||||||
|
monaco-editor@*, monaco-editor@0.20.0:
|
||||||
|
version "0.20.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
|
||||||
|
integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==
|
||||||
|
|
||||||
mousetrap@1.6.3:
|
mousetrap@1.6.3:
|
||||||
version "1.6.3"
|
version "1.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a"
|
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a"
|
||||||
|
@ -7381,6 +7394,15 @@ react-measure@1.4.7:
|
||||||
prop-types "^15.5.4"
|
prop-types "^15.5.4"
|
||||||
resize-observer-polyfill "^1.4.1"
|
resize-observer-polyfill "^1.4.1"
|
||||||
|
|
||||||
|
react-monaco-editor@0.36.0:
|
||||||
|
version "0.36.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.36.0.tgz#ac085c14f25fb072514c925596f6a06a711ee078"
|
||||||
|
integrity sha512-JVA5SZhOoYZ0DCdTwYgagtRb3jHo4KN7TVFiJauG+ZBAJWfDSTzavPIrwzWbgu8ahhDqDk4jUcYlOJL2BC/0UA==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "^16.x"
|
||||||
|
monaco-editor "*"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
react-popper@1.3.3:
|
react-popper@1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
|
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
|
||||||
|
|
Loading…
Reference in New Issue