diff --git a/packages/api/src/controllers/runners.js b/packages/api/src/controllers/runners.js new file mode 100644 index 000000000..1c3e618f1 --- /dev/null +++ b/packages/api/src/controllers/runners.js @@ -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' }; + }, +}; diff --git a/packages/api/src/index.js b/packages/api/src/index.js index 366b28250..7fad01bb6 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -18,4 +18,4 @@ if (argument && argument.endsWith('Process')) { main.start(argument); } -module.exports = shell; \ No newline at end of file +module.exports = shell; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index f1fae6bc1..9c91c33e8 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -15,11 +15,12 @@ const serverConnections = require('./controllers/serverConnections'); const databaseConnections = require('./controllers/databaseConnections'); const metadata = require('./controllers/metadata'); const sessions = require('./controllers/sessions'); +const runners = require('./controllers/runners'); const jsldata = require('./controllers/jsldata'); const config = require('./controllers/config'); function start(argument = null) { - console.log('process.argv', process.argv); + // console.log('process.argv', process.argv); const app = express(); @@ -34,6 +35,7 @@ function start(argument = null) { useController(app, '/database-connections', databaseConnections); useController(app, '/metadata', metadata); useController(app, '/sessions', sessions); + useController(app, '/runners', runners); useController(app, '/jsldata', jsldata); useController(app, '/config', config); diff --git a/packages/api/src/shell/runScript.js b/packages/api/src/shell/runScript.js index 49803e8bb..859dfd7b8 100644 --- a/packages/api/src/shell/runScript.js +++ b/packages/api/src/shell/runScript.js @@ -1,4 +1,9 @@ +const childProcessChecker = require('../utility/childProcessChecker'); + async function runScript(func) { + if (process.argv.includes('--checkParent')) { + childProcessChecker(); + } try { await func(); process.exit(0); diff --git a/packages/api/src/utility/childProcessChecker.js b/packages/api/src/utility/childProcessChecker.js index 1870897cc..e065949f8 100644 --- a/packages/api/src/utility/childProcessChecker.js +++ b/packages/api/src/utility/childProcessChecker.js @@ -9,7 +9,7 @@ function childProcessChecker() { // One way can be to check for error code ERR_IPC_CHANNEL_CLOSED // and call process.exit() console.log('parent died', ex.toString()); - process.exit(); + process.exit(1); } }, 1000); } diff --git a/packages/api/src/utility/directories.js b/packages/api/src/utility/directories.js index 044b6e4e8..067a4562b 100644 --- a/packages/api/src/utility/directories.js +++ b/packages/api/src/utility/directories.js @@ -3,7 +3,7 @@ const path = require('path'); const fs = require('fs'); let createdDatadir = false; -let createdJsldir = false; +const createDirectories = {}; function datadir() { const dir = path.join(os.homedir(), 'dbgate-data'); @@ -18,20 +18,26 @@ function datadir() { return dir; } -function jsldir() { - const dir = path.join(datadir(), 'jsl'); - if (!createdJsldir) { +const dirFunc = (dirname) => () => { + const dir = path.join(datadir(), dirname); + if (!createDirectories[dirname]) { if (!fs.existsSync(dir)) { console.log(`Creating jsl directory ${dir}`); fs.mkdirSync(dir); } - createdJsldir = true; + createDirectories[dirname] = true; } return dir; -} +}; + +const jsldir = dirFunc('jsl'); +const rundir = dirFunc('run'); +const uploadsdir = dirFunc('uploads'); module.exports = { datadir, jsldir, + rundir, + uploadsdir, }; diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 504fb2f81..c8c9c73d8 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -19,6 +19,11 @@ export interface OpenedSession { subprocess: ChildProcess; } +export interface OpenedRunner { + runid: string; + subprocess: ChildProcess; +} + export interface StoredConnection { engine: string; server: string; diff --git a/packages/web/src/impexp/ScriptCreator.js b/packages/web/src/impexp/ScriptCreator.js deleted file mode 100644 index 8067fe55b..000000000 --- a/packages/web/src/impexp/ScriptCreator.js +++ /dev/null @@ -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, - }); - } -} diff --git a/packages/web/src/impexp/ScriptWriter.js b/packages/web/src/impexp/ScriptWriter.js index 486f39e95..936404367 100644 --- a/packages/web/src/impexp/ScriptWriter.js +++ b/packages/web/src/impexp/ScriptWriter.js @@ -1,9 +1,12 @@ export default class ScriptWriter { constructor() { this.s = ''; - this.put('const dbgateApi = require("@dbgate/api");'); - this.put(); - this.put('async function run() {'); + this.varCount = 0; + } + + allocVariable(prefix = 'var') { + this.varCount += 1; + return `${prefix}${this.varCount}`; } put(s = '') { @@ -11,17 +14,11 @@ export default class ScriptWriter { this.s += '\n'; } - finish() { - this.put('}'); - this.put(); - this.put('dbgateApi.runScript(run);'); - } - 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) { - this.put(` await dbgateApi.copyStream(${sourceVar}, ${targetVar});`); + this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`); } } diff --git a/packages/web/src/impexp/createImpExpScript.js b/packages/web/src/impexp/createImpExpScript.js index f2b9f3b02..90fb6eca2 100644 --- a/packages/web/src/impexp/createImpExpScript.js +++ b/packages/web/src/impexp/createImpExpScript.js @@ -1,12 +1,12 @@ import _ from 'lodash'; -import ScriptCreator from './ScriptCreator'; +import ScriptWriter from './ScriptWriter'; import getAsArray from '../utility/getAsArray'; import { getConnectionInfo } from '../utility/metadataLoaders'; import engines from '@dbgate/engines'; import { quoteFullName, fullNameFromString } from '@dbgate/datalib'; export default async function createImpExpScript(values) { - const script = new ScriptCreator(); + const script = new ScriptWriter(); if (values.sourceStorageType == 'database') { const tables = getAsArray(values.sourceTables); for (const table of tables) { @@ -29,7 +29,8 @@ export default async function createImpExpScript(values) { }); script.copyStream(sourceVar, targetVar); + script.put(); } } - return script.getCode(); + return script.s; } diff --git a/packages/web/src/query/ShellToolbar.js b/packages/web/src/query/ShellToolbar.js new file mode 100644 index 000000000..f3a54c617 --- /dev/null +++ b/packages/web/src/query/ShellToolbar.js @@ -0,0 +1,15 @@ +import React from 'react'; +import ToolbarButton from '../widgets/ToolbarButton'; + +export default function ShellToolbar({ execute, cancel, busy}) { + return ( + <> + + Execute + + + Cancel + + + ); +} diff --git a/packages/web/src/query/SessionMessagesView.js b/packages/web/src/query/SocketMessagesView.js similarity index 76% rename from packages/web/src/query/SessionMessagesView.js rename to packages/web/src/query/SocketMessagesView.js index 9153342f6..616368b0f 100644 --- a/packages/web/src/query/SessionMessagesView.js +++ b/packages/web/src/query/SocketMessagesView.js @@ -3,7 +3,7 @@ import React from 'react'; import MessagesView from './MessagesView'; 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 cachedMessagesRef = React.useRef([]); const socket = useSocket(); @@ -27,13 +27,13 @@ export default function SessionMessagesView({ sessionId, onMessageClick, execute }, [executeNumber]); React.useEffect(() => { - if (sessionId && socket) { - socket.on(`session-info-${sessionId}`, handleInfo); + if (eventName && socket) { + socket.on(eventName, handleInfo); return () => { - socket.off(`session-info-${sessionId}`, handleInfo); + socket.off(eventName, handleInfo); }; } - }, [sessionId, socket]); + }, [eventName, socket]); return ; } diff --git a/packages/web/src/tabs/QueryTab.js b/packages/web/src/tabs/QueryTab.js index 5dade4009..7cede062d 100644 --- a/packages/web/src/tabs/QueryTab.js +++ b/packages/web/src/tabs/QueryTab.js @@ -8,7 +8,7 @@ import { useConnectionInfo, getTableInfo, getConnectionInfo, getSqlObjectInfo } import SqlEditor from '../sqleditor/SqlEditor'; import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState'; import QueryToolbar from '../query/QueryToolbar'; -import SessionMessagesView from '../query/SessionMessagesView'; +import SocketMessagesView from '../query/SocketMessagesView'; import { TabPage } from '../widgets/TabControl'; import ResultTabs from '../sqleditor/ResultTabs'; import { VerticalSplitter } from '../widgets/Splitter'; @@ -143,6 +143,7 @@ export default function QueryTab({ }; const handleExecute = async () => { + if (busy) return; setExecuteNumber((num) => num + 1); const selectedText = editorRef.current.editor.getSelectedText(); @@ -204,8 +205,8 @@ export default function QueryTab({ {sessionId && ( - diff --git a/packages/web/src/tabs/ShellTab.js b/packages/web/src/tabs/ShellTab.js index b3652dc58..fd729462c 100644 --- a/packages/web/src/tabs/ShellTab.js +++ b/packages/web/src/tabs/ShellTab.js @@ -8,7 +8,7 @@ import { useConnectionInfo, getTableInfo, getConnectionInfo, getSqlObjectInfo } import SqlEditor from '../sqleditor/SqlEditor'; import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState'; import QueryToolbar from '../query/QueryToolbar'; -import SessionMessagesView from '../query/SessionMessagesView'; +import SocketMessagesView from '../query/SocketMessagesView'; import { TabPage } from '../widgets/TabControl'; import ResultTabs from '../sqleditor/ResultTabs'; import { VerticalSplitter } from '../widgets/Splitter'; @@ -19,11 +19,10 @@ import SaveSqlFileModal from '../modals/SaveSqlFileModal'; import useModalState from '../modals/useModalState'; import sqlFormatter from 'sql-formatter'; import JavaScriptEditor from '../sqleditor/JavaScriptEditor'; +import ShellToolbar from '../query/ShellToolbar'; export default function ShellTab({ tabid, - conid, - database, initialArgs, tabVisible, toolbarPortalRef, @@ -43,6 +42,11 @@ export default function ShellTab({ const saveToStorageDebounced = React.useMemo(() => _.debounce(saveToStorage, 5000), [saveToStorage]); const setOpenedTabs = useSetOpenedTabs(); + const [executeNumber, setExecuteNumber] = React.useState(0); + const [runnerId, setRunnerId] = React.useState(null); + + const socket = useSocket(); + React.useEffect(() => { window.addEventListener('beforeunload', saveToStorage); return () => { @@ -68,8 +72,18 @@ export default function ShellTab({ const editorRef = React.useRef(null); - useUpdateDatabaseForTab(tabVisible, conid, database); - const connection = useConnectionInfo({ conid }); + const handleRunnerDone = React.useCallback(() => { + 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) => { if (text != null) shellTextRef.current = text; @@ -77,12 +91,24 @@ export default function ShellTab({ 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 = () => { - // axios.post('sessions/cancel', { - // sesid: sessionId, - // }); + axios.post('runners/cancel', { + runid: runnerId, + }); }; const handleKeyDown = (data, hash, keyString, keyCode, event) => { @@ -102,7 +128,19 @@ export default function ShellTab({ onKeyDown={handleKeyDown} editorRef={editorRef} /> + + {toolbarPortalRef && + toolbarPortalRef.current && + tabVisible && + ReactDOM.createPortal( + , + toolbarPortalRef.current + )} ); }