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
+ )}
>
);
}