shell script runner

This commit is contained in:
Jan Prochazka
2020-06-07 09:51:53 +02:00
parent 617548cd50
commit f37524f76f
14 changed files with 203 additions and 89 deletions

View File

@@ -0,0 +1,93 @@
const _ = require('lodash');
const path = require('path');
const fs = require('fs');
const uuidv1 = require('uuid/v1');
const socket = require('../utility/socket');
const { fork } = require('child_process');
const { rundir, uploadsdir } = require('../utility/directories');
const scriptTemplate = (script) => `
const dbgateApi = require(process.env.DBGATE_API || "@dbgate/api");
require=null;
async function run() {
${script}
}
dbgateApi.runScript(run);
`;
module.exports = {
/** @type {import('@dbgate/types').OpenedRunner[]} */
opened: [],
dispatchMessage(runid, message) {
console.log('DISPATCHING', message);
if (_.isString(message)) {
socket.emit(`runner-info-${runid}`, {
message,
time: new Date(),
severity: 'info',
});
}
if (_.isPlainObject(message)) {
socket.emit(`runner-info-${runid}`, {
time: new Date(),
severity: 'info',
...message,
});
}
},
handle_ping() {},
start_meta: 'post',
async start({ script }) {
const runid = uuidv1();
const directory = path.join(rundir(), runid);
const scriptFile = path.join(uploadsdir(), runid + '.js');
fs.writeFileSync(`${scriptFile}`, scriptTemplate(script));
fs.mkdirSync(directory);
console.log(`RUNNING SCRIPT ${scriptFile}`);
const subprocess = fork(scriptFile, ['--checkParent'], {
cwd: directory,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
env: {
DBGATE_API: process.argv[1],
},
});
subprocess.stdout.on('data', (data) => this.dispatchMessage(runid, data.toString()));
subprocess.stderr.on('data', (data) =>
data.toString.split('\n').forEach((message) => {
this.dispatchMessage(runid, { severity: 'error', message });
})
);
subprocess.on('exit', (code) => {
socket.emit(`runner-done-${runid}`, code);
});
subprocess.on('error', (error) => {
this.dispatchMessage({
severity: 'error',
message: error.toString(),
});
});
const newOpened = {
runid,
subprocess,
};
this.opened.push(newOpened);
// @ts-ignore
subprocess.on('message', ({ msgtype, ...message }) => {
this[`handle_${msgtype}`](runid, message);
});
return newOpened;
},
cancel_meta: 'post',
async cancel({ runid }) {
const session = this.opened.find((x) => x.runid == runid);
if (!session) {
throw new Error('Invalid runner');
}
session.subprocess.kill();
return { state: 'ok' };
},
};

View File

@@ -15,11 +15,12 @@ const serverConnections = require('./controllers/serverConnections');
const databaseConnections = require('./controllers/databaseConnections'); const databaseConnections = require('./controllers/databaseConnections');
const metadata = require('./controllers/metadata'); const metadata = require('./controllers/metadata');
const sessions = require('./controllers/sessions'); const sessions = require('./controllers/sessions');
const runners = require('./controllers/runners');
const jsldata = require('./controllers/jsldata'); const jsldata = require('./controllers/jsldata');
const config = require('./controllers/config'); const config = require('./controllers/config');
function start(argument = null) { function start(argument = null) {
console.log('process.argv', process.argv); // console.log('process.argv', process.argv);
const app = express(); const app = express();
@@ -34,6 +35,7 @@ function start(argument = null) {
useController(app, '/database-connections', databaseConnections); useController(app, '/database-connections', databaseConnections);
useController(app, '/metadata', metadata); useController(app, '/metadata', metadata);
useController(app, '/sessions', sessions); useController(app, '/sessions', sessions);
useController(app, '/runners', runners);
useController(app, '/jsldata', jsldata); useController(app, '/jsldata', jsldata);
useController(app, '/config', config); useController(app, '/config', config);

View File

@@ -1,4 +1,9 @@
const childProcessChecker = require('../utility/childProcessChecker');
async function runScript(func) { async function runScript(func) {
if (process.argv.includes('--checkParent')) {
childProcessChecker();
}
try { try {
await func(); await func();
process.exit(0); process.exit(0);

View File

@@ -9,7 +9,7 @@ function childProcessChecker() {
// One way can be to check for error code ERR_IPC_CHANNEL_CLOSED // One way can be to check for error code ERR_IPC_CHANNEL_CLOSED
// and call process.exit() // and call process.exit()
console.log('parent died', ex.toString()); console.log('parent died', ex.toString());
process.exit(); process.exit(1);
} }
}, 1000); }, 1000);
} }

View File

@@ -3,7 +3,7 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
let createdDatadir = false; let createdDatadir = false;
let createdJsldir = false; const createDirectories = {};
function datadir() { function datadir() {
const dir = path.join(os.homedir(), 'dbgate-data'); const dir = path.join(os.homedir(), 'dbgate-data');
@@ -18,20 +18,26 @@ function datadir() {
return dir; return dir;
} }
function jsldir() { const dirFunc = (dirname) => () => {
const dir = path.join(datadir(), 'jsl'); const dir = path.join(datadir(), dirname);
if (!createdJsldir) { if (!createDirectories[dirname]) {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
console.log(`Creating jsl directory ${dir}`); console.log(`Creating jsl directory ${dir}`);
fs.mkdirSync(dir); fs.mkdirSync(dir);
} }
createdJsldir = true; createDirectories[dirname] = true;
} }
return dir; return dir;
} };
const jsldir = dirFunc('jsl');
const rundir = dirFunc('run');
const uploadsdir = dirFunc('uploads');
module.exports = { module.exports = {
datadir, datadir,
jsldir, jsldir,
rundir,
uploadsdir,
}; };

View File

@@ -19,6 +19,11 @@ export interface OpenedSession {
subprocess: ChildProcess; subprocess: ChildProcess;
} }
export interface OpenedRunner {
runid: string;
subprocess: ChildProcess;
}
export interface StoredConnection { export interface StoredConnection {
engine: string; engine: string;
server: string; server: string;

View File

@@ -1,49 +0,0 @@
import ScriptWriter from './ScriptWriter';
export default class ScriptCreator {
constructor() {
this.varCount = 0;
this.commands = [];
}
allocVariable(prefix = 'var') {
this.varCount += 1;
return `${prefix}${this.varCount}`;
}
getCode() {
const writer = new ScriptWriter();
for (const command of this.commands) {
const { type } = command;
switch (type) {
case 'assign':
{
const { variableName, functionName, props } = command;
writer.assign(variableName, functionName, props);
}
break;
case 'copyStream':
{
const { sourceVar, targetVar } = command;
writer.copyStream(sourceVar, targetVar);
}
break;
}
}
writer.finish();
return writer.s;
}
assign(variableName, functionName, props) {
this.commands.push({
type: 'assign',
variableName,
functionName,
props,
});
}
copyStream(sourceVar, targetVar) {
this.commands.push({
type: 'copyStream',
sourceVar,
targetVar,
});
}
}

View File

@@ -1,9 +1,12 @@
export default class ScriptWriter { export default class ScriptWriter {
constructor() { constructor() {
this.s = ''; this.s = '';
this.put('const dbgateApi = require("@dbgate/api");'); this.varCount = 0;
this.put(); }
this.put('async function run() {');
allocVariable(prefix = 'var') {
this.varCount += 1;
return `${prefix}${this.varCount}`;
} }
put(s = '') { put(s = '') {
@@ -11,17 +14,11 @@ export default class ScriptWriter {
this.s += '\n'; this.s += '\n';
} }
finish() {
this.put('}');
this.put();
this.put('dbgateApi.runScript(run);');
}
assign(variableName, functionName, props) { assign(variableName, functionName, props) {
this.put(` const ${variableName} = await dbgateApi.${functionName}(${JSON.stringify(props)});`); this.put(`const ${variableName} = await dbgateApi.${functionName}(${JSON.stringify(props)});`);
} }
copyStream(sourceVar, targetVar) { copyStream(sourceVar, targetVar) {
this.put(` await dbgateApi.copyStream(${sourceVar}, ${targetVar});`); this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`);
} }
} }

View File

@@ -1,12 +1,12 @@
import _ from 'lodash'; import _ from 'lodash';
import ScriptCreator from './ScriptCreator'; import ScriptWriter from './ScriptWriter';
import getAsArray from '../utility/getAsArray'; import getAsArray from '../utility/getAsArray';
import { getConnectionInfo } from '../utility/metadataLoaders'; import { getConnectionInfo } from '../utility/metadataLoaders';
import engines from '@dbgate/engines'; import engines from '@dbgate/engines';
import { quoteFullName, fullNameFromString } from '@dbgate/datalib'; import { quoteFullName, fullNameFromString } from '@dbgate/datalib';
export default async function createImpExpScript(values) { export default async function createImpExpScript(values) {
const script = new ScriptCreator(); const script = new ScriptWriter();
if (values.sourceStorageType == 'database') { if (values.sourceStorageType == 'database') {
const tables = getAsArray(values.sourceTables); const tables = getAsArray(values.sourceTables);
for (const table of tables) { for (const table of tables) {
@@ -29,7 +29,8 @@ export default async function createImpExpScript(values) {
}); });
script.copyStream(sourceVar, targetVar); script.copyStream(sourceVar, targetVar);
script.put();
} }
} }
return script.getCode(); return script.s;
} }

View File

@@ -0,0 +1,15 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function ShellToolbar({ execute, cancel, busy}) {
return (
<>
<ToolbarButton disabled={busy} onClick={execute} icon="fas fa-play">
Execute
</ToolbarButton>
<ToolbarButton disabled={!busy} onClick={cancel} icon="fas fa-times">
Cancel
</ToolbarButton>
</>
);
}

View File

@@ -3,7 +3,7 @@ import React from 'react';
import MessagesView from './MessagesView'; import MessagesView from './MessagesView';
import useSocket from '../utility/SocketProvider'; import useSocket from '../utility/SocketProvider';
export default function SessionMessagesView({ sessionId, onMessageClick, executeNumber }) { export default function SocketMessagesView({ eventName, onMessageClick = undefined, executeNumber }) {
const [displayedMessages, setDisplayedMessages] = React.useState([]); const [displayedMessages, setDisplayedMessages] = React.useState([]);
const cachedMessagesRef = React.useRef([]); const cachedMessagesRef = React.useRef([]);
const socket = useSocket(); const socket = useSocket();
@@ -27,13 +27,13 @@ export default function SessionMessagesView({ sessionId, onMessageClick, execute
}, [executeNumber]); }, [executeNumber]);
React.useEffect(() => { React.useEffect(() => {
if (sessionId && socket) { if (eventName && socket) {
socket.on(`session-info-${sessionId}`, handleInfo); socket.on(eventName, handleInfo);
return () => { return () => {
socket.off(`session-info-${sessionId}`, handleInfo); socket.off(eventName, handleInfo);
}; };
} }
}, [sessionId, socket]); }, [eventName, socket]);
return <MessagesView items={displayedMessages} onMessageClick={onMessageClick} />; return <MessagesView items={displayedMessages} onMessageClick={onMessageClick} />;
} }

View File

@@ -8,7 +8,7 @@ import { useConnectionInfo, getTableInfo, getConnectionInfo, getSqlObjectInfo }
import SqlEditor from '../sqleditor/SqlEditor'; import SqlEditor from '../sqleditor/SqlEditor';
import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState'; import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState';
import QueryToolbar from '../query/QueryToolbar'; import QueryToolbar from '../query/QueryToolbar';
import SessionMessagesView from '../query/SessionMessagesView'; import SocketMessagesView from '../query/SocketMessagesView';
import { TabPage } from '../widgets/TabControl'; import { TabPage } from '../widgets/TabControl';
import ResultTabs from '../sqleditor/ResultTabs'; import ResultTabs from '../sqleditor/ResultTabs';
import { VerticalSplitter } from '../widgets/Splitter'; import { VerticalSplitter } from '../widgets/Splitter';
@@ -143,6 +143,7 @@ export default function QueryTab({
}; };
const handleExecute = async () => { const handleExecute = async () => {
if (busy) return;
setExecuteNumber((num) => num + 1); setExecuteNumber((num) => num + 1);
const selectedText = editorRef.current.editor.getSelectedText(); const selectedText = editorRef.current.editor.getSelectedText();
@@ -204,8 +205,8 @@ export default function QueryTab({
{sessionId && ( {sessionId && (
<ResultTabs sessionId={sessionId} executeNumber={executeNumber}> <ResultTabs sessionId={sessionId} executeNumber={executeNumber}>
<TabPage label="Messages" key="messages"> <TabPage label="Messages" key="messages">
<SessionMessagesView <SocketMessagesView
sessionId={sessionId} eventName={sessionId ? `session-info-${sessionId}` : null}
onMessageClick={handleMesageClick} onMessageClick={handleMesageClick}
executeNumber={executeNumber} executeNumber={executeNumber}
/> />

View File

@@ -8,7 +8,7 @@ import { useConnectionInfo, getTableInfo, getConnectionInfo, getSqlObjectInfo }
import SqlEditor from '../sqleditor/SqlEditor'; import SqlEditor from '../sqleditor/SqlEditor';
import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState'; import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState';
import QueryToolbar from '../query/QueryToolbar'; import QueryToolbar from '../query/QueryToolbar';
import SessionMessagesView from '../query/SessionMessagesView'; import SocketMessagesView from '../query/SocketMessagesView';
import { TabPage } from '../widgets/TabControl'; import { TabPage } from '../widgets/TabControl';
import ResultTabs from '../sqleditor/ResultTabs'; import ResultTabs from '../sqleditor/ResultTabs';
import { VerticalSplitter } from '../widgets/Splitter'; import { VerticalSplitter } from '../widgets/Splitter';
@@ -19,11 +19,10 @@ import SaveSqlFileModal from '../modals/SaveSqlFileModal';
import useModalState from '../modals/useModalState'; import useModalState from '../modals/useModalState';
import sqlFormatter from 'sql-formatter'; import sqlFormatter from 'sql-formatter';
import JavaScriptEditor from '../sqleditor/JavaScriptEditor'; import JavaScriptEditor from '../sqleditor/JavaScriptEditor';
import ShellToolbar from '../query/ShellToolbar';
export default function ShellTab({ export default function ShellTab({
tabid, tabid,
conid,
database,
initialArgs, initialArgs,
tabVisible, tabVisible,
toolbarPortalRef, toolbarPortalRef,
@@ -43,6 +42,11 @@ export default function ShellTab({
const saveToStorageDebounced = React.useMemo(() => _.debounce(saveToStorage, 5000), [saveToStorage]); const saveToStorageDebounced = React.useMemo(() => _.debounce(saveToStorage, 5000), [saveToStorage]);
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const [executeNumber, setExecuteNumber] = React.useState(0);
const [runnerId, setRunnerId] = React.useState(null);
const socket = useSocket();
React.useEffect(() => { React.useEffect(() => {
window.addEventListener('beforeunload', saveToStorage); window.addEventListener('beforeunload', saveToStorage);
return () => { return () => {
@@ -68,8 +72,18 @@ export default function ShellTab({
const editorRef = React.useRef(null); const editorRef = React.useRef(null);
useUpdateDatabaseForTab(tabVisible, conid, database); const handleRunnerDone = React.useCallback(() => {
const connection = useConnectionInfo({ conid }); setBusy(false);
}, []);
React.useEffect(() => {
if (runnerId && socket) {
socket.on(`runner-done-${runnerId}`, handleRunnerDone);
return () => {
socket.off(`runner-done-${runnerId}`, handleRunnerDone);
};
}
}, [runnerId, socket]);
const handleChange = (text) => { const handleChange = (text) => {
if (text != null) shellTextRef.current = text; if (text != null) shellTextRef.current = text;
@@ -77,12 +91,24 @@ export default function ShellTab({
saveToStorageDebounced(); saveToStorageDebounced();
}; };
const handleExecute = async () => {}; const handleExecute = async () => {
if (busy) return;
setExecuteNumber((num) => num + 1);
const selectedText = editorRef.current.editor.getSelectedText();
let runid = runnerId;
const resp = await axios.post('runners/start', {
script: selectedText || shellText,
});
runid = resp.data.runid;
setRunnerId(runid);
setBusy(true);
};
const handleCancel = () => { const handleCancel = () => {
// axios.post('sessions/cancel', { axios.post('runners/cancel', {
// sesid: sessionId, runid: runnerId,
// }); });
}; };
const handleKeyDown = (data, hash, keyString, keyCode, event) => { const handleKeyDown = (data, hash, keyString, keyCode, event) => {
@@ -102,7 +128,19 @@ export default function ShellTab({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
editorRef={editorRef} editorRef={editorRef}
/> />
<SocketMessagesView eventName={runnerId ? `runner-info-${runnerId}` : null} executeNumber={executeNumber} />
</VerticalSplitter> </VerticalSplitter>
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<ShellToolbar
execute={handleExecute}
busy={busy}
cancel={handleCancel}
/>,
toolbarPortalRef.current
)}
</> </>
); );
} }