From 89f7ba50da14a8bb2cfb5e7b703181f0c412a312 Mon Sep 17 00:00:00 2001 From: Robert Strickland Date: Thu, 25 Jul 2024 14:44:33 -0500 Subject: [PATCH] Serverheartbeat now sent through session to UI --- src/dataEditor/core/editor/dataEditor.ts | 2 +- src/dataEditor/core/service/editService.ts | 3 +- src/dataEditor/index.ts | 34 ++--- src/dataEditor/omegaEdit/index.ts | 9 +- src/dataEditor/omegaEdit/server/config.ts | 25 ++++ .../omegaEdit/{ => server}/heartbeat.ts | 0 src/dataEditor/omegaEdit/server/index.ts | 0 src/dataEditor/omegaEdit/server/logging.ts | 83 +++++++++++ .../omegaEdit/{ => server}/server.ts | 133 +++--------------- .../omegaEdit/{ => service}/editService.ts | 116 +++++++-------- .../omegaEdit/service/requestHandler.ts | 0 src/dataEditor/omegaEdit/service/session.ts | 32 +++++ src/dataEditor/omegaEdit/session.ts | 8 -- src/dataEditor/standalone/standaloneEditor.ts | 17 +-- 14 files changed, 247 insertions(+), 215 deletions(-) create mode 100644 src/dataEditor/omegaEdit/server/config.ts rename src/dataEditor/omegaEdit/{ => server}/heartbeat.ts (100%) create mode 100644 src/dataEditor/omegaEdit/server/index.ts create mode 100644 src/dataEditor/omegaEdit/server/logging.ts rename src/dataEditor/omegaEdit/{ => server}/server.ts (68%) rename src/dataEditor/omegaEdit/{ => service}/editService.ts (59%) create mode 100644 src/dataEditor/omegaEdit/service/requestHandler.ts create mode 100644 src/dataEditor/omegaEdit/service/session.ts delete mode 100644 src/dataEditor/omegaEdit/session.ts diff --git a/src/dataEditor/core/editor/dataEditor.ts b/src/dataEditor/core/editor/dataEditor.ts index a3c764214..1a8f23b8e 100644 --- a/src/dataEditor/core/editor/dataEditor.ts +++ b/src/dataEditor/core/editor/dataEditor.ts @@ -20,7 +20,7 @@ export abstract class DataEditor implements ServiceUser { protected serviceClient: EditServiceClient, protected ui: DataEditorUI ) { - serviceClient.onDidProcessRequest = (response) => { + serviceClient.onDidProcess = (response) => { this.ui.updateUI(response) } ui.onInputEvent = (input) => { diff --git a/src/dataEditor/core/service/editService.ts b/src/dataEditor/core/service/editService.ts index 866790b15..253396864 100644 --- a/src/dataEditor/core/service/editService.ts +++ b/src/dataEditor/core/service/editService.ts @@ -9,13 +9,12 @@ export interface EditHandler { } export interface EditService { register(source: DataSource): Promise - activeUsers(): number } export interface EditServiceClient { close(): void id(): string request: (request: any) => any - onDidProcessRequest: (response: any) => any + onDidProcess: (response: any) => any } export interface DataSource {} diff --git a/src/dataEditor/index.ts b/src/dataEditor/index.ts index 461b3332a..88931130a 100644 --- a/src/dataEditor/index.ts +++ b/src/dataEditor/index.ts @@ -10,17 +10,13 @@ import { StandaloneEditor, StandaloneInitializer, } from './standalone/standaloneEditor' -import { OmegaEditServer } from './omegaEdit/tests/utils/fixtures' -import { OmegaEditServerManager } from './omegaEdit/server' +import { OmegaEditServerManager } from './omegaEdit/server/server' const editorCommands: Map< EditorCommand['command'], EditorCommand['initializer'] > = new Map() -export function RegisterEditor(command: EditorCommand): void { - editorCommands.set(command.command, command.initializer) -} class DataEditorManager implements vscode.Disposable { private editors: DataEditor[] = [] private disposables: vscode.Disposable[] = [] @@ -45,6 +41,22 @@ class DataEditorManager implements vscode.Disposable { }) } } + +let Manager: DataEditorManager + +function registerAllEditorCommands(ctx: vscode.ExtensionContext) { + editorCommands.forEach((initer, command) => { + ctx.subscriptions.push( + vscode.commands.registerCommand(command, async () => { + await Manager.Run(initer) + }) + ) + }) +} + +export function RegisterEditor(command: EditorCommand): void { + editorCommands.set(command.command, command.initializer) +} export const VSCodeFileSelector: FilePathSourceStrategy = { get: () => { return new Promise(async (resolve, reject) => { @@ -66,19 +78,7 @@ const DefaultEditorCommand: EditorCommand = { initializer: new StandaloneInitializer(VSCodeFileSelector), } -let Manager: DataEditorManager - -function registerAllEditorCommands(ctx: vscode.ExtensionContext) { - editorCommands.forEach((initer, command) => { - ctx.subscriptions.push( - vscode.commands.registerCommand(command, async () => { - await Manager.Run(initer) - }) - ) - }) -} export function activate(ctx: vscode.ExtensionContext) { - const config = editor_config.extractConfigurationVariables() // Omega Edit Server specific configurations Manager = new DataEditorManager(ctx) registerAllEditorCommands(ctx) diff --git a/src/dataEditor/omegaEdit/index.ts b/src/dataEditor/omegaEdit/index.ts index ac31cd1ec..7a49c2c31 100644 --- a/src/dataEditor/omegaEdit/index.ts +++ b/src/dataEditor/omegaEdit/index.ts @@ -1,6 +1,7 @@ import path from 'path' import * as os from 'os' import { DataSource, GetDataSourceStrategy } from '../core/service/editService' + export class FilePath implements DataSource { private baseName: string constructor(private filepath: string) { @@ -13,9 +14,7 @@ export class FilePath implements DataSource { return this.baseName } } -export interface FilePathSourceStrategy extends GetDataSourceStrategy { - get(): Promise -} + export namespace FilePath { export const SystemTmpDirectory = (): FilePath => { return new FilePath(os.tmpdir()) @@ -24,3 +23,7 @@ export namespace FilePath { return new FilePath('.') } } + +export interface FilePathSourceStrategy extends GetDataSourceStrategy { + get(): Promise +} diff --git a/src/dataEditor/omegaEdit/server/config.ts b/src/dataEditor/omegaEdit/server/config.ts new file mode 100644 index 000000000..a46235f05 --- /dev/null +++ b/src/dataEditor/omegaEdit/server/config.ts @@ -0,0 +1,25 @@ +import { FilePath } from '..' +import { IConfig } from '../../config' + +export class Connection { + constructor( + readonly host: string, + readonly port: number + ) {} +} + +export type GetServerConfigStrategy = () => Promise +export class ServerConfig { + readonly conn: Connection + readonly logFile: FilePath + readonly logLevel: string + readonly checkpointPath: string + + constructor(config: () => IConfig) { + const { checkpointPath, logFile, logLevel, port } = config() + this.conn = new Connection('127.0.0.1', port) + this.logFile = new FilePath(logFile) + this.logLevel = logLevel + this.checkpointPath = checkpointPath + } +} diff --git a/src/dataEditor/omegaEdit/heartbeat.ts b/src/dataEditor/omegaEdit/server/heartbeat.ts similarity index 100% rename from src/dataEditor/omegaEdit/heartbeat.ts rename to src/dataEditor/omegaEdit/server/heartbeat.ts diff --git a/src/dataEditor/omegaEdit/server/index.ts b/src/dataEditor/omegaEdit/server/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/dataEditor/omegaEdit/server/logging.ts b/src/dataEditor/omegaEdit/server/logging.ts new file mode 100644 index 000000000..c8f2715a5 --- /dev/null +++ b/src/dataEditor/omegaEdit/server/logging.ts @@ -0,0 +1,83 @@ +import { createSimpleFileLogger, setLogger } from '@omega-edit/client' +import { ServerConfig } from './config' + +import * as fs from 'fs' +import path from 'path' +import XDGAppPaths from 'xdg-app-paths' +export const APP_DATA_PATH: string = XDGAppPaths({ name: 'omega_edit' }).data() + +const MAX_LOG_FILES = 5 +function rotateLogFiles(logFile: string): void { + interface LogFile { + path: string + ctime: Date + } + + // assert( + // MAX_LOG_FILES > 0, + // 'Maximum number of log files must be greater than 0' + // ) + + if (fs.existsSync(logFile)) { + const logDir = path.dirname(logFile) + const logFileName = path.basename(logFile) + + // Get list of existing log files + const logFiles: LogFile[] = fs + .readdirSync(logDir) + .filter((file) => file.startsWith(logFileName) && file !== logFileName) + .map((file) => ({ + path: path.join(logDir, file), + ctime: fs.statSync(path.join(logDir, file)).ctime, + })) + .sort((a, b) => b.ctime.getTime() - a.ctime.getTime()) + + // Delete oldest log files if maximum number of log files is exceeded + while (logFiles.length >= MAX_LOG_FILES) { + const fileToDelete = logFiles.pop() as LogFile + fs.unlinkSync(fileToDelete.path) + } + + // Rename current log file with timestamp and create a new empty file + const timestamp = new Date().toISOString().replace(/:/g, '-') + fs.renameSync(logFile, path.join(logDir, `${logFileName}.${timestamp}`)) + } +} + +export function setupLogging(config: ServerConfig): Promise { + return new Promise((res, rej) => { + const filePath = config.logFile.fullPath() + const level = config.logLevel + rotateLogFiles(filePath) + setLogger(createSimpleFileLogger(filePath, level)) + res() + }) +} + +export function generateLogbackConfigFile(server: ServerConfig): string { + const serverLogFile = path.join(APP_DATA_PATH, `serv-${server.conn.port}.log`) + const dirname = path.dirname(server.logFile.fullPath()) + if (!fs.existsSync(dirname)) { + fs.mkdirSync(dirname, { recursive: true }) + } + const logbackConfig = `\n + + + ${serverLogFile} + + [%date{ISO8601}] [%level] [%logger] [%marker] [%thread] - %msg MDC: {%mdc}%n + + + + + + +` + const logbackConfigFile = path.join( + APP_DATA_PATH, + `serv-${server.conn.port}.logconf.xml` + ) + rotateLogFiles(server.logFile.fullPath()) + fs.writeFileSync(logbackConfigFile, logbackConfig) + return logbackConfigFile // Return the path to the logback config file +} diff --git a/src/dataEditor/omegaEdit/server.ts b/src/dataEditor/omegaEdit/server/server.ts similarity index 68% rename from src/dataEditor/omegaEdit/server.ts rename to src/dataEditor/omegaEdit/server/server.ts index fb058adde..6eaca3ece 100644 --- a/src/dataEditor/omegaEdit/server.ts +++ b/src/dataEditor/omegaEdit/server/server.ts @@ -1,37 +1,43 @@ -import { FilePath } from '.' -import { OmegaEditService } from './editService' +import { FilePath } from '..' +import { OmegaEditService } from '../service/editService' import path from 'path' import * as fs from 'fs' import * as os from 'os' -import XDGAppPaths from 'xdg-app-paths' import { - createSimpleFileLogger, getClientVersion, getServerHeartbeat, getServerInfo, IServerHeartbeat, IServerInfo, - setLogger, startServer, stopProcessUsingPID, - stopServerGraceful, } from '@omega-edit/client' -import { IConfig } from '../config' -import { IHeartbeatInfo } from '../include/server/heartbeat/HeartBeatInfo' -export const APP_DATA_PATH: string = XDGAppPaths({ name: 'omega_edit' }).data() +import { IConfig } from '../../config' +import { IHeartbeatInfo } from '../../include/server/heartbeat/HeartBeatInfo' +import { ServerConfig } from './config' +import { + APP_DATA_PATH, + generateLogbackConfigFile, + setupLogging, +} from './logging' -export class Connection { - constructor( - readonly host: string, - readonly port: number - ) {} +const activeServers: Map = new Map() +const ServerDisposeAll = { + dispose: () => { + activeServers.forEach((server) => { + server.dispose() + }) + }, } export class OmegaEditServer { private service: OmegaEditService + private heartbeatIntervalId: NodeJS.Timeout | undefined = undefined constructor(readonly config: ServerConfig) { this.service = new OmegaEditService(new FilePath(config.checkpointPath)) + this.service.onAllSessionsClosed(() => { + clearInterval(this.heartbeatIntervalId) serverStop(this.config) }) } @@ -45,40 +51,6 @@ export class OmegaEditServer { } } -function setupLogging(config: ServerConfig): Promise { - return new Promise((res, rej) => { - const filePath = config.logFile.fullPath() - const level = config.logLevel - rotateLogFiles(filePath) - setLogger(createSimpleFileLogger(filePath, level)) - res() - }) -} -export class ServerConfig { - readonly conn: Connection - readonly logFile: FilePath - readonly logLevel: string - readonly checkpointPath: string - - constructor(config: () => IConfig) { - const { checkpointPath, logFile, logLevel, port } = config() - this.conn = new Connection('127.0.0.1', port) - this.logFile = new FilePath(logFile) - this.logLevel = logLevel - this.checkpointPath = checkpointPath - } -} - -export type GetServerConfigStrategy = () => Promise - -const activeServers: Map = new Map() -const ServerDisposeAll = { - dispose: () => { - activeServers.forEach((server) => { - server.dispose() - }) - }, -} export class OmegaEditServerManager { static Connect(config: () => IConfig): Promise { return new Promise(async (res, rej) => { @@ -119,71 +91,6 @@ function removeDirectory(dirPath: string): void { fs.rmdirSync(dirPath) } } -const MAX_LOG_FILES = 5 -function rotateLogFiles(logFile: string): void { - interface LogFile { - path: string - ctime: Date - } - - // assert( - // MAX_LOG_FILES > 0, - // 'Maximum number of log files must be greater than 0' - // ) - - if (fs.existsSync(logFile)) { - const logDir = path.dirname(logFile) - const logFileName = path.basename(logFile) - - // Get list of existing log files - const logFiles: LogFile[] = fs - .readdirSync(logDir) - .filter((file) => file.startsWith(logFileName) && file !== logFileName) - .map((file) => ({ - path: path.join(logDir, file), - ctime: fs.statSync(path.join(logDir, file)).ctime, - })) - .sort((a, b) => b.ctime.getTime() - a.ctime.getTime()) - - // Delete oldest log files if maximum number of log files is exceeded - while (logFiles.length >= MAX_LOG_FILES) { - const fileToDelete = logFiles.pop() as LogFile - fs.unlinkSync(fileToDelete.path) - } - - // Rename current log file with timestamp and create a new empty file - const timestamp = new Date().toISOString().replace(/:/g, '-') - fs.renameSync(logFile, path.join(logDir, `${logFileName}.${timestamp}`)) - } -} - -function generateLogbackConfigFile(server: ServerConfig): string { - const serverLogFile = path.join(APP_DATA_PATH, `serv-${server.conn.port}.log`) - const dirname = path.dirname(server.logFile.fullPath()) - if (!fs.existsSync(dirname)) { - fs.mkdirSync(dirname, { recursive: true }) - } - const logbackConfig = `\n - - - ${serverLogFile} - - [%date{ISO8601}] [%level] [%logger] [%marker] [%thread] - %msg MDC: {%mdc}%n - - - - - - -` - const logbackConfigFile = path.join( - APP_DATA_PATH, - `serv-${server.conn.port}.logconf.xml` - ) - rotateLogFiles(server.logFile.fullPath()) - fs.writeFileSync(logbackConfigFile, logbackConfig) - return logbackConfigFile // Return the path to the logback config file -} async function serverStop(server: ServerConfig) { const serverPidFile = getPidFile(server.conn.port) diff --git a/src/dataEditor/omegaEdit/editService.ts b/src/dataEditor/omegaEdit/service/editService.ts similarity index 59% rename from src/dataEditor/omegaEdit/editService.ts rename to src/dataEditor/omegaEdit/service/editService.ts index d2d93c2c6..f42fb10cb 100644 --- a/src/dataEditor/omegaEdit/editService.ts +++ b/src/dataEditor/omegaEdit/service/editService.ts @@ -1,62 +1,74 @@ -import { createSession, getCounts, IServerHeartbeat } from '@omega-edit/client' -import { FilePath, FilePathSourceStrategy } from '.' import { - EditService, - EditServiceClient, - ServiceUser, -} from '../core/service/editService' -import { Session } from './session' + createSession, + getCounts, + getServerHeartbeat, + IServerHeartbeat, +} from '@omega-edit/client' +import { FilePath } from '..' +import { EditService } from '../../core/service/editService' +import { OmegaEditSession, SessionIdType } from './session' import EventEmitter from 'events' -export class OmegaEditSession implements EditServiceClient { - constructor( - private sessionId: SessionIdType, - private serviceRequestHandler: ( - session: SessionIdType, - request: any - ) => Promise, - readonly close: () => void - ) {} - onDidProcessRequest: (response: any) => any = () => { - throw 'Not Implemented' - } - id(): string { - return this.sessionId - } - async request(request: any) { - const response = await this.serviceRequestHandler(this.sessionId, request) - this.onDidProcessRequest(response) - } +const InitialHeartbeat: IServerHeartbeat = { + latency: 0, + sessionCount: 0, + serverTimestamp: 0, + serverUptime: 0, + serverCpuCount: 0, + serverCpuLoadAverage: 0, + serverMaxMemory: 0, + serverCommittedMemory: 0, + serverUsedMemory: 0, } - -type SessionIdType = ReturnType // Service handles multiple ServiceUsers (Session) export class OmegaEditService implements EditService { + private Events: EventEmitter = new EventEmitter() + private activeSessions: Map = new Map() + private heartbeat: IServerHeartbeat = InitialHeartbeat + private heartbeatInterval: NodeJS.Timeout | undefined = undefined + constructor( readonly checkpointDirectory: FilePath = FilePath.SystemTmpDirectory() ) {} - private Events: EventEmitter = new EventEmitter() - private activeSessions: Map = new Map() onAllSessionsClosed(listener: () => void) { + clearInterval(this.heartbeatInterval) this.Events.on('allSessionsClosed', () => { listener() }) } register(source: FilePath): Promise { - /* register client to receive heartbeats */ + if (this.activeSessions.size == 0) { + const intervalMsMultiplier = + this.activeSessions.size <= 0 ? 1 : this.activeSessions.size + + this.heartbeatInterval = setInterval(() => { + getServerHeartbeat(this.sessionIds(), 1000) + .catch((err) => { + throw err + }) + .then((heartbeat) => { + this.heartbeat = heartbeat + }) + }, intervalMsMultiplier * 1000) + } + console.log(this.heartbeatInterval) return new Promise(async (res, rej) => { const session = await this.createSession(source, this.checkpointDirectory) - // this.activeSessions.set(client, session) - // this.activeSessions.push(session) + res(session) }) } - activeUsers(): number { - throw new Error('Method not implemented.') - } + sessionCount() { + return this.activeSessions.size + } + private sessionIds() { + const ret: SessionIdType[] = [] + for (const id in this.activeSessions.keys()) ret.push(id) + return ret + } private createSession( file: FilePath, checkpointPath: FilePath @@ -69,17 +81,13 @@ export class OmegaEditService implements EditService { ) const id = response.getSessionId() this.activeSessions.set(id, file) - res( - new OmegaEditSession( - id, - async (id, req) => { - return this.requestHandler(id, req) - }, - () => { - this.removeSession(id) - } - ) - ) + const requestHandlerFn = async (id, req) => { + return this.requestHandler(id, req) + } + const sessionCloseCallback = () => { + this.removeSession(id) + } + res(new OmegaEditSession(id, requestHandlerFn, sessionCloseCallback)) }) } @@ -88,9 +96,13 @@ export class OmegaEditService implements EditService { if (this.activeSessions.size == 0) this.Events.emit('allSessionsClosed') } + /** Extract this functionality to another class */ private requestHandler(sessionId: SessionIdType, request: any): Promise { console.log(`received request ${{ ...request }}`) return new Promise(async (res, rej) => { + if (request.command === 'getServerHeartbeat') { + res({ command: 4, data: { ...this.heartbeat } }) + } if (request.command === 'getFileInfo') { const count = await getCounts(sessionId, [ 1, //CountKind.COUNT_COMPUTED_FILE_SIZE, @@ -122,14 +134,4 @@ export class OmegaEditService implements EditService { rej({ msg: `no ${request.command} handler found` }) }) } - // sessionCount() { - // return this.activeSessions.length - // } - // activeSessionIds(): string[] { - // let ret: string[] = [] - // this.activeSessions.forEach((session) => { - // ret.push(session.id) - // }) - // return ret - // } } diff --git a/src/dataEditor/omegaEdit/service/requestHandler.ts b/src/dataEditor/omegaEdit/service/requestHandler.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/dataEditor/omegaEdit/service/session.ts b/src/dataEditor/omegaEdit/service/session.ts new file mode 100644 index 000000000..3acc79263 --- /dev/null +++ b/src/dataEditor/omegaEdit/service/session.ts @@ -0,0 +1,32 @@ +import { EditServiceClient } from '../../core/service/editService' + +export type SessionIdType = ReturnType +export class OmegaEditSession implements EditServiceClient { + heartbeatInterval: NodeJS.Timeout | undefined = undefined + constructor( + private sessionId: SessionIdType, + private serviceRequestHandler: ( + session: SessionIdType, + request: any + ) => Promise, + readonly close: () => void + ) { + this.heartbeatInterval = setInterval(() => { + this.request({ command: 'getServerHeartbeat' }) + }, 1000) + } + onDidProcess: (response: any) => any = () => { + throw 'Not Implemented' + } + id(): string { + return this.sessionId + } + async request(request: any) { + const response = await this.serviceRequestHandler(this.sessionId, request) + this.onDidProcess(response) + } +} + +export class OmegaEditSessionNotifier { + static Notify(sessionId: SessionIdType, notification: any) {} +} diff --git a/src/dataEditor/omegaEdit/session.ts b/src/dataEditor/omegaEdit/session.ts deleted file mode 100644 index bbaf1f04d..000000000 --- a/src/dataEditor/omegaEdit/session.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ServiceUser } from '../core/service/editService' - -export class Session { - constructor(readonly id: string) {} - createViewport(): Promise { - throw '' - } -} diff --git a/src/dataEditor/standalone/standaloneEditor.ts b/src/dataEditor/standalone/standaloneEditor.ts index ce3ea291e..0cc38929b 100644 --- a/src/dataEditor/standalone/standaloneEditor.ts +++ b/src/dataEditor/standalone/standaloneEditor.ts @@ -1,13 +1,10 @@ import * as vscode from 'vscode' -// import { DataEditorUI } from './core/editor/editorUI' -// import { OmegaEditService } from './omegaEdit/editService' -import { OmegaEditServerManager } from '../omegaEdit/server' -import { FilePath, FilePathSourceStrategy } from '../omegaEdit' +import { OmegaEditServerManager } from '../omegaEdit/server/server' +import { FilePathSourceStrategy } from '../omegaEdit' import { DataEditor, DataEditorInitializer } from '../core/editor/dataEditor' -import { OmegaEditService, OmegaEditSession } from '../omegaEdit/editService' import { extractConfigurationVariables } from '../config' -import { EditServiceClient } from '../core/service/editService' import { WebviewPanelEditorUI } from '../webview/editorWebviewPanel' +import { OmegaEditSession } from '../omegaEdit/service/session' export class StandaloneEditor extends DataEditor implements vscode.Disposable { static readonly commandStr = 'extension.data.edit' @@ -17,14 +14,6 @@ export class StandaloneEditor extends DataEditor implements vscode.Disposable { this.serviceClient.close() }) this.serviceClient.request({ command: 'getFileInfo' }) - // const ui: UI = { - // onDidReceiveMessage: (msg) => { - // if(msg.type === 'edit') service.process(msg, (response) => { - // ui.send(response) - // }) - // }, - // send: (msg) => { console.log(msg) } - // } } dispose() { // this.editService.dispose()