diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 49378307f..347d3258a 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -7,6 +7,7 @@ const DatabaseAnalyser = require('@dbgate/engines/default/DatabaseAnalyser'); module.exports = { /** @type {import('@dbgate/types').OpenedDatabaseConnection[]} */ opened: [], + closed: [], requests: {}, handle_structure(conid, database, { structure }) { @@ -24,6 +25,14 @@ module.exports = { resolve(response); delete this.requests[msgid]; }, + handle_status(conid, database, { status }) { + const existing = this.opened.find((x) => x.conid == conid && x.database == database); + if (!existing) return; + existing.status = status; + socket.emitChanged(`database-status-changed-${conid}-${database}`); + socket.emitChanged(`database-structure-changed-${conid}-${database}`); + }, + handle_ping() {}, async ensureOpened(conid, database) { @@ -31,19 +40,31 @@ module.exports = { if (existing) return existing; const connection = await connections.get({ conid }); const subprocess = fork(process.argv[1], ['databaseConnectionProcess']); + const lastClosed = this.closed.find((x) => x.conid == conid && x.database == database); const newOpened = { conid, database, subprocess, - structure: DatabaseAnalyser.createEmptyStructure(), + structure: lastClosed ? lastClosed.structure : DatabaseAnalyser.createEmptyStructure(), connection, + status: { name: 'pending' }, }; this.opened.push(newOpened); // @ts-ignore subprocess.on('message', ({ msgtype, ...message }) => { + if (newOpened.disconnected) return; this[`handle_${msgtype}`](conid, database, message); }); - subprocess.send({ msgtype: 'connect', ...connection, database }); + subprocess.on('exit', () => { + if (newOpened.disconnected) return; + this.close(conid, database, false); + }); + + subprocess.send({ + msgtype: 'connect', + connection: { ...connection, database }, + structure: lastClosed ? lastClosed.structure : null, + }); return newOpened; }, @@ -65,6 +86,41 @@ module.exports = { return res; }, + ping_meta: 'post', + async ping({ conid, database }) { + const existing = this.opened.find((x) => x.conid == conid && x.database == database); + if (existing) { + existing.subprocess.send({ msgtype: 'ping' }); + } + return { status: 'ok' }; + }, + + refresh_meta: 'post', + async refresh({ conid, database }) { + this.close(conid, database); + + await this.ensureOpened(conid, database); + return { status: 'ok' }; + }, + + close(conid, database, kill = true) { + const existing = this.opened.find((x) => x.conid == conid && x.database == database); + if (existing) { + existing.disconnected = true; + if (kill) existing.subprocess.kill(); + this.opened = this.opened.filter((x) => x.conid != conid || x.database != database); + this.closed[conid] = { + status: { + ...existing.status, + name: 'error', + }, + structure: existing.structure, + }; + socket.emitChanged(`database-status-changed-${conid}-${database}`); + socket.emitChanged(`database-structure-changed-${conid}-${database}`); + } + }, + // runCommand_meta: 'post', // async runCommand({ conid, database, sql }) { // console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`); diff --git a/packages/api/src/controllers/metadata.js b/packages/api/src/controllers/metadata.js index 093914a7f..5be4cea06 100644 --- a/packages/api/src/controllers/metadata.js +++ b/packages/api/src/controllers/metadata.js @@ -22,6 +22,7 @@ module.exports = { (res, type) => ({ ...res, [type]: pickObjectNames(opened.structure[type]), + status: opened.status, }), {} ); diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index b30f977bc..ff2a3274f 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -60,10 +60,9 @@ module.exports = { if (existing) { existing.disconnected = true; if (kill) existing.subprocess.kill(); - const last = this.opened.find((x) => x.conid == conid); this.opened = this.opened.filter((x) => x.conid != conid); this.closed[conid] = { - ...(last && last.status), + ...existing.status, name: 'error', }; socket.emitChanged(`server-status-changed`); diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index 8e3ba92d1..18fc6522d 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -1,4 +1,5 @@ const engines = require('@dbgate/engines'); +const stableStringify = require('json-stable-stringify'); const driverConnect = require('../utility/driverConnect'); const childProcessChecker = require('../utility/childProcessChecker'); @@ -6,11 +7,14 @@ let systemConnection; let storedConnection; let afterConnectCallbacks = []; let analysedStructure = null; +let lastPing = null; +let lastStatus = null; async function handleFullRefresh() { const driver = engines(storedConnection); analysedStructure = await driver.analyseFull(systemConnection); process.send({ msgtype: 'structure', structure: analysedStructure }); + setStatusName('ok'); } async function handleIncrementalRefresh() { @@ -22,9 +26,23 @@ async function handleIncrementalRefresh() { } } -async function handleConnect(connection) { - storedConnection = connection; +function setStatus(status) { + const statusString = stableStringify(status); + if (lastStatus != statusString) { + process.send({ msgtype: 'status', status }); + lastStatus = statusString; + } +} +function setStatusName(name) { + setStatus({ name }); +} + +async function handleConnect({ connection, structure }) { + storedConnection = connection; + lastPing = new Date().getTime(); + + setStatusName('pending'); const driver = engines(storedConnection); systemConnection = await driverConnect(driver, storedConnection); handleFullRefresh(); @@ -56,9 +74,14 @@ async function handleQueryData({ msgid, sql }) { // process.send({ msgtype: 'response', msgid, ...res }); // } +function handlePing() { + lastPing = new Date().getTime(); +} + const messageHandlers = { connect: handleConnect, queryData: handleQueryData, + ping: handlePing, // runCommand: handleRunCommand, }; @@ -69,6 +92,14 @@ async function handleMessage({ msgtype, ...other }) { function start() { childProcessChecker(); + + setInterval(() => { + const time = new Date().getTime(); + if (time - lastPing > 60 * 1000) { + process.exit(0); + } + }, 60 * 1000); + process.on('message', async (message) => { try { await handleMessage(message); diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index 552ca422f..ceb9ba9af 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -1,7 +1,7 @@ const engines = require('@dbgate/engines'); +const stableStringify = require('json-stable-stringify'); const driverConnect = require('../utility/driverConnect'); const childProcessChecker = require('../utility/childProcessChecker'); -const stableStringify = require('json-stable-stringify'); let systemConnection; let storedConnection; diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 1313c6153..504fb2f81 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -1,10 +1,15 @@ -import { ChildProcess } from "child_process"; -import { DatabaseInfo } from "./dbinfo"; +import { ChildProcess } from 'child_process'; +import { DatabaseInfo } from './dbinfo'; export interface OpenedDatabaseConnection { conid: string; database: string; structure: DatabaseInfo; subprocess: ChildProcess; + disconnected?: boolean; + status?: { + name: string; + message?: string; + }; } export interface OpenedSession { @@ -23,9 +28,9 @@ export interface StoredConnection { displayName: string; } -export * from "./engines"; -export * from "./dbinfo"; -export * from "./query"; -export * from "./dialect"; -export * from "./dumper"; -export * from "./dbtypes"; +export * from './engines'; +export * from './dbinfo'; +export * from './query'; +export * from './dialect'; +export * from './dumper'; +export * from './dbtypes'; diff --git a/packages/web/src/App.js b/packages/web/src/App.js index fdbf5e5a5..f8383bf80 100644 --- a/packages/web/src/App.js +++ b/packages/web/src/App.js @@ -9,7 +9,7 @@ import { OpenedConnectionsProvider, } from './utility/globalState'; import { SocketProvider } from './utility/SocketProvider'; -import OpenedConnectionsPinger from './utility/OpnedConnectionsPinger'; +import ConnectionsPinger from './utility/ConnectionsPinger'; function App() { return ( @@ -19,9 +19,9 @@ function App() { - + - + diff --git a/packages/web/src/utility/ConnectionsPinger.js b/packages/web/src/utility/ConnectionsPinger.js new file mode 100644 index 000000000..d9c5cd514 --- /dev/null +++ b/packages/web/src/utility/ConnectionsPinger.js @@ -0,0 +1,22 @@ +import React from 'react'; +import _ from 'lodash'; +import { useOpenedConnections, useCurrentDatabase } from './globalState'; +import axios from './axios'; + +export default function ConnectionsPinger({ children }) { + const openedConnections = useOpenedConnections(); + const currentDatabase = useCurrentDatabase(); + React.useEffect(() => { + const handle = window.setInterval(() => { + axios.post('server-connections/ping', { connections: openedConnections }); + + const database = _.get(currentDatabase, 'name'); + const conid = _.get(currentDatabase, 'connection._id'); + if (conid && database) { + axios.post('database-connections/ping', { conid }); + } + }, 30 * 1000); + return () => window.clearInterval(handle); + }, [openedConnections, currentDatabase]); + return children; +} diff --git a/packages/web/src/utility/OpnedConnectionsPinger.js b/packages/web/src/utility/OpnedConnectionsPinger.js deleted file mode 100644 index a2c9a371f..000000000 --- a/packages/web/src/utility/OpnedConnectionsPinger.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { useOpenedConnections } from './globalState'; -import axios from './axios'; - -export default function OpenedConnectionsPinger({ children }) { - const openedConnections = useOpenedConnections(); - React.useEffect(() => { - const handle = window.setInterval(() => { - axios.post('server-connections/ping', { connections: openedConnections }); - }, 30 * 1000); - return () => window.clearInterval(handle); - }, [openedConnections]); - return children; -} diff --git a/packages/web/src/widgets/DatabaseWidget.js b/packages/web/src/widgets/DatabaseWidget.js index acfbe76bc..992fe53ce 100644 --- a/packages/web/src/widgets/DatabaseWidget.js +++ b/packages/web/src/widgets/DatabaseWidget.js @@ -10,6 +10,7 @@ import databaseObjectAppObject from '../appobj/databaseObjectAppObject'; import { useSqlObjectList, useDatabaseList, useConnectionList, useServerStatus } from '../utility/metadataLoaders'; import { SearchBoxWrapper, InnerContainer, Input, MainContainer, OuterContainer, WidgetTitle } from './WidgetStyles'; import axios from '../utility/axios'; +import LoadingInfo from './LoadingInfo'; function SubDatabaseList({ data }) { const setDb = useSetCurrentDatabase(); @@ -68,6 +69,11 @@ function ConnectionList() { function SqlObjectList({ conid, database }) { const objects = useSqlObjectList({ conid, database }); + const { status } = objects || {}; + + const handleRefreshDatabase = () => { + axios.post('database-connections/refresh', { conid, database }); + }; const [filter, setFilter] = React.useState(''); const objectList = _.flatten( @@ -85,15 +91,19 @@ function SqlObjectList({ conid, database }) { value={filter} onChange={(e) => setFilter(e.target.value)} /> - Refresh + Refresh - ({ ...x, conid, database }))} - makeAppObj={databaseObjectAppObject()} - groupFunc={(appobj) => appobj.groupTitle} - filter={filter} - /> + {status && status.name == 'pending' ? ( + + ) : ( + ({ ...x, conid, database }))} + makeAppObj={databaseObjectAppObject()} + groupFunc={(appobj) => appobj.groupTitle} + filter={filter} + /> + )} ); diff --git a/packages/web/src/widgets/LoadingInfo.js b/packages/web/src/widgets/LoadingInfo.js new file mode 100644 index 000000000..818eb5399 --- /dev/null +++ b/packages/web/src/widgets/LoadingInfo.js @@ -0,0 +1,24 @@ +import React from 'react'; + +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + align-items: center; +`; + +const Spinner = styled.div` + font-size: 20pt; + margin: 10px; +`; + +export default function LoadingInfo({ message }) { + return ( + + + + + {message} + + ); +}