From 7d9d4d3c660617f38b2a00ac5f7d715d17e5ec35 Mon Sep 17 00:00:00 2001 From: jfwang Date: Mon, 21 Oct 2024 05:35:35 +0100 Subject: [PATCH] feat: Add LXD AE instance backend and frontend, and Job executor logic for instance --- package.json | 10 +- packages/itmat-apis/src/index.ts | 4 +- .../itmat-apis/src/trpc/instanceProcedure.ts | 160 ++ packages/itmat-apis/src/trpc/lxdProcedure.ts | 149 ++ packages/itmat-apis/src/trpc/tRPCRouter.ts | 13 +- packages/itmat-commons/jest.config.ts | 3 +- packages/itmat-commons/src/utils/poller.ts | 162 +- .../itmat-cores/config/config.sample.json | 19 +- .../itmat-cores/src/coreFunc/instanceCore.ts | 843 ++++++++++ packages/itmat-cores/src/coreFunc/jobCore.ts | 141 +- packages/itmat-cores/src/coreFunc/userCore.ts | 43 + packages/itmat-cores/src/database/database.ts | 8 +- packages/itmat-cores/src/index.ts | 3 + packages/itmat-cores/src/lxd/lxd.util.ts | 35 + packages/itmat-cores/src/lxd/lxdManager.ts | 363 +++++ .../itmat-cores/src/utils/configManager.ts | 8 + .../itmat-interface/config/config.sample.json | 17 +- packages/itmat-interface/src/index.ts | 6 +- packages/itmat-interface/src/lxd/index.ts | 451 ++++++ .../src/server/commonMiddleware.ts | 4 + packages/itmat-interface/src/server/helper.ts | 14 +- packages/itmat-interface/src/server/router.ts | 101 +- .../test/trpcTests/instance.test.ts | 289 ++++ .../config/config.sample.json | 39 +- packages/itmat-job-executor/express-user.d.ts | 27 + .../src/database/database.ts | 51 +- .../itmat-job-executor/src/emailer/emailer.ts | 4 + .../src/jobDispatch/dispatcher.ts | 42 +- .../src/jobExecutorRunner.ts | 19 +- .../src/jobHandlers/apiJobHandler.ts | 55 + .../src/jobHandlers/jobHandlerInterface.ts | 10 +- .../src/jobHandlers/lxdJobHandler.ts | 169 ++ .../src/jobHandlers/lxdMonitorHandler.ts | 97 ++ .../src/jobHandlers/lxdPollOperation.ts | 62 + .../src/query/pipeLineGenerator.ts | 268 ---- .../src/query/queryController.ts | 88 -- .../src/query/queryHandler.ts | 98 -- .../itmat-job-executor/src/server/server.ts | 16 +- .../src/utils/configManager.ts | 36 +- packages/itmat-job-executor/tsconfig.json | 10 + .../databaseSetup/collectionsAndIndexes.ts | 7 + .../src/databaseSetup/seed/config.ts | 34 +- packages/itmat-types/src/types/config.ts | 51 +- packages/itmat-types/src/types/index.ts | 6 +- packages/itmat-types/src/types/instance.ts | 57 + packages/itmat-types/src/types/job.ts | 104 +- packages/itmat-types/src/types/lxd.ts | 416 +++++ packages/itmat-ui-react/proxy.conf.js | 2 +- packages/itmat-ui-react/src/Fence.tsx | 2 +- .../src/components/drive/file.tsx | 17 +- .../src/components/instance/index.tsx | 8 + .../components/instance/instance.module.css | 25 + .../src/components/instance/instance.tsx | 434 ++++++ .../src/components/lxd/index.tsx | 19 + .../src/components/lxd/instanceOptions.tsx | 62 + .../src/components/lxd/instanceTable.tsx | 26 + .../lxd/lxd.Instance.statusIcon.tsx | 35 + .../components/lxd/lxd.instance.console.tsx | 178 +++ .../src/components/lxd/lxd.instance.list.tsx | 373 +++++ .../lxd/lxd.instance.text.console.tsx | 350 +++++ .../src/components/lxd/lxd.module.css | 171 +++ .../components/lxd/spice/src/atKeynames.js | 189 +++ .../src/components/lxd/spice/src/bitmap.js | 65 + .../lxd/spice/src/code_to_scancode.js | 149 ++ .../src/components/lxd/spice/src/cursor.js | 142 ++ .../src/components/lxd/spice/src/display.js | 1276 ++++++++++++++++ .../src/components/lxd/spice/src/enums.js | 372 +++++ .../src/components/lxd/spice/src/filexfer.js | 95 ++ .../src/components/lxd/spice/src/inputs.js | 325 ++++ .../src/components/lxd/spice/src/lz.js | 192 +++ .../src/components/lxd/spice/src/main.js | 523 +++++++ .../src/components/lxd/spice/src/playback.js | 411 +++++ .../src/components/lxd/spice/src/png.js | 265 ++++ .../src/components/lxd/spice/src/port.js | 96 ++ .../src/components/lxd/spice/src/quic.js | 1353 +++++++++++++++++ .../src/components/lxd/spice/src/resize.js | 94 ++ .../lxd/spice/src/simulatecursor.js | 212 +++ .../lxd/spice/src/spicearraybuffer.js | 59 + .../src/components/lxd/spice/src/spiceconn.js | 476 ++++++ .../components/lxd/spice/src/spicedataview.js | 126 ++ .../src/components/lxd/spice/src/spicemsg.js | 1218 +++++++++++++++ .../src/components/lxd/spice/src/spicetype.js | 502 ++++++ .../lxd/spice/src/thirdparty/jsbn.js | 595 ++++++++ .../lxd/spice/src/thirdparty/prng4.js | 84 + .../lxd/spice/src/thirdparty/rng.js | 109 ++ .../lxd/spice/src/thirdparty/rsa.js | 156 ++ .../lxd/spice/src/thirdparty/sha1.js | 356 +++++ .../src/components/lxd/spice/src/ticket.js | 262 ++++ .../src/components/lxd/spice/src/utils.js | 349 +++++ .../src/components/lxd/spice/src/webm.js | 673 ++++++++ .../src/components/lxd/spice/src/wire.js | 134 ++ .../src/components/lxd/util/formatUtils.ts | 36 + .../components/lxd/util/updateMaxHeight.tsx | 23 + .../src/components/scaffold/mainMenuBar.tsx | 14 +- .../src/components/scaffold/mainPanel.tsx | 4 + yarn.lock | 10 + 96 files changed, 16639 insertions(+), 620 deletions(-) create mode 100644 packages/itmat-apis/src/trpc/instanceProcedure.ts create mode 100644 packages/itmat-apis/src/trpc/lxdProcedure.ts create mode 100644 packages/itmat-cores/src/coreFunc/instanceCore.ts create mode 100644 packages/itmat-cores/src/lxd/lxd.util.ts create mode 100644 packages/itmat-cores/src/lxd/lxdManager.ts create mode 100644 packages/itmat-interface/src/lxd/index.ts create mode 100644 packages/itmat-interface/test/trpcTests/instance.test.ts create mode 100644 packages/itmat-job-executor/express-user.d.ts create mode 100644 packages/itmat-job-executor/src/emailer/emailer.ts create mode 100644 packages/itmat-job-executor/src/jobHandlers/apiJobHandler.ts create mode 100644 packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts create mode 100644 packages/itmat-job-executor/src/jobHandlers/lxdMonitorHandler.ts create mode 100644 packages/itmat-job-executor/src/jobHandlers/lxdPollOperation.ts delete mode 100644 packages/itmat-job-executor/src/query/pipeLineGenerator.ts delete mode 100644 packages/itmat-job-executor/src/query/queryController.ts delete mode 100644 packages/itmat-job-executor/src/query/queryHandler.ts create mode 100644 packages/itmat-types/src/types/instance.ts create mode 100644 packages/itmat-types/src/types/lxd.ts create mode 100644 packages/itmat-ui-react/src/components/instance/index.tsx create mode 100644 packages/itmat-ui-react/src/components/instance/instance.module.css create mode 100644 packages/itmat-ui-react/src/components/instance/instance.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/index.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/instanceOptions.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/instanceTable.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/lxd.Instance.statusIcon.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx create mode 100644 packages/itmat-ui-react/src/components/lxd/lxd.module.css create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/atKeynames.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/bitmap.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/code_to_scancode.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/cursor.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/display.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/enums.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/filexfer.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/inputs.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/lz.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/main.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/playback.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/png.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/port.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/quic.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/resize.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/simulatecursor.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/spicearraybuffer.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/spiceconn.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/spicedataview.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/spicemsg.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/spicetype.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/jsbn.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/prng4.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rng.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rsa.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/sha1.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/ticket.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/utils.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/webm.js create mode 100644 packages/itmat-ui-react/src/components/lxd/spice/src/wire.js create mode 100644 packages/itmat-ui-react/src/components/lxd/util/formatUtils.ts create mode 100644 packages/itmat-ui-react/src/components/lxd/util/updateMaxHeight.tsx diff --git a/package.json b/package.json index f65c10270..8e51934ba 100644 --- a/package.json +++ b/package.json @@ -104,19 +104,21 @@ "@apollo/client": "3.11.8", "@apollo/server": "4.11.0", "@ideafast/idgen": "0.2.1", - "@simplewebauthn/browser": "^10.0.0", - "@simplewebauthn/server": "^10.0.1", - "@nivo/calendar": "0.87.0", "@nivo/bar": "0.87.0", + "@nivo/calendar": "0.87.0", "@nivo/core": "0.87.0", "@nivo/line": "0.87.0", "@nivo/pie": "0.87.0", "@nivo/treemap": "0.87.0", + "@simplewebauthn/browser": "^10.0.0", + "@simplewebauthn/server": "^10.0.1", "@swc/helpers": "0.5.13", "@tanstack/react-query": "4.36.1", "@trpc/client": "10.45.2", "@trpc/react-query": "10.45.2", "@trpc/server": "10.45.2", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "JSONStream": "1.3.5", "antd": "5.21.4", "antd-img-crop": "4.23.0", @@ -191,4 +193,4 @@ "**/jest-util": "^29", "**/pretty-format": "^29" } -} \ No newline at end of file +} diff --git a/packages/itmat-apis/src/index.ts b/packages/itmat-apis/src/index.ts index b9b824754..e67875602 100644 --- a/packages/itmat-apis/src/index.ts +++ b/packages/itmat-apis/src/index.ts @@ -22,4 +22,6 @@ export * from './trpc/trpc'; export * from './trpc/tRPCRouter'; export * from './trpc/userProcedure'; export * from './trpc/middleware'; -export * from './trpc/webauthnProcedure'; \ No newline at end of file +export * from './trpc/webauthnProcedure'; +export * from './trpc/instanceProcedure'; +export * from './trpc/lxdProcedure'; \ No newline at end of file diff --git a/packages/itmat-apis/src/trpc/instanceProcedure.ts b/packages/itmat-apis/src/trpc/instanceProcedure.ts new file mode 100644 index 000000000..a3c9790c8 --- /dev/null +++ b/packages/itmat-apis/src/trpc/instanceProcedure.ts @@ -0,0 +1,160 @@ +import { z } from 'zod'; +import { TRPCBaseProcedure, TRPCRouter } from './trpc'; +import { InstanceCore } from '@itmat-broker/itmat-cores'; +import { enumAppType, enumInstanceStatus, LXDInstanceTypeEnum, CoreError, enumCoreErrors, enumOpeType, enumUserTypes} from '@itmat-broker/itmat-types'; + +export class InstanceRouter { + baseProcedure: TRPCBaseProcedure; + router: TRPCRouter; + instanceCore: InstanceCore; + + constructor(baseProcedure: TRPCBaseProcedure, router: TRPCRouter, instanceCore: InstanceCore) { + this.baseProcedure = baseProcedure; + this.router = router; + this.instanceCore = instanceCore; + } + + _router() { + return this.router({ + /** + * Create an instance + */ + createInstance: this.baseProcedure.input( + z.object({ + name: z.string(), + type: z.nativeEnum(LXDInstanceTypeEnum), + appType: z.nativeEnum(enumAppType), + lifeSpan: z.number(), + cpuLimit: z.number().optional(), + memoryLimit: z.string().optional(), + diskLimit: z.string().optional() + }) + ).mutation(async ({ input, ctx }) => { + + if (!ctx.req.user || !ctx.req.user.id) { + throw new CoreError( + enumCoreErrors.NOT_LOGGED_IN, + 'User must be authenticated.'); + } + const userId = ctx.req.user.id; + + // Check if requested resources exceed the user's quota + await this.instanceCore.checkQuotaBeforeCreation(userId, input.cpuLimit ?? 0, input.memoryLimit ?? '0', input.diskLimit ?? '0', 1); + + + return await this.instanceCore.createInstance( + userId, + ctx.req.user.username, + input.name, + input.type, + input.appType, + input.lifeSpan, + input.cpuLimit, + input.memoryLimit, + input.diskLimit + ); + }), + + /** + * Start or stop an instance + */ + startStopInstance: this.baseProcedure.input( + z.object({ + instanceId: z.string(), + action: z.enum([enumOpeType.START, enumOpeType.STOP]) + }) + ).mutation(async ({ input, ctx }) => { + if (!ctx.req.user || !ctx.req.user.id) { + throw new CoreError(enumCoreErrors.NOT_LOGGED_IN, 'User must be authenticated.'); + } + const userId = ctx.req.user.id; + + return await this.instanceCore.startStopInstance(userId, input.instanceId, input.action); + }), + + /** + * Restart an instance with a new lifespan + */ + restartInstance: this.baseProcedure.input( + z.object({ + instanceId: z.string(), + lifeSpan: z.number() + }) + ).mutation(async ({ input, ctx }) => { + if (!ctx.req.user || !ctx.req.user.id) { + throw new CoreError(enumCoreErrors.NOT_LOGGED_IN, 'User must be authenticated.'); + } + const userId = ctx.req.user.id; + + return await this.instanceCore.restartInstance(userId, input.instanceId, input.lifeSpan); + }), + + /** + * Get all instances for an user + */ + getInstances: this.baseProcedure.query(async ({ ctx }) => { + const user = ctx.req.user; + if (!user) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + 'Insufficient permissions.' + ); + } + return await this.instanceCore.getInstances(user.id); + }), + + /** + * Edit an instance + */ + editInstance: this.baseProcedure.input( + z.object({ + instanceId: z.string().optional(), + instanceName: z.string().optional(), + // set the update to the type LxdConfiguration + updates: z.object({ + name: z.string().optional(), + type: z.nativeEnum(LXDInstanceTypeEnum).optional(), + appType: z.nativeEnum(enumAppType).optional(), + lifeSpan: z.number().optional(), + project: z.string().optional(), + status: z.nativeEnum(enumInstanceStatus).optional(), + cpuLimit: z.number().optional(), + memoryLimit: z.string().optional() + }).passthrough() + }) + ).mutation(async ({ input, ctx }) => { + if (!ctx.req.user || !ctx.req.user.id) { + throw new CoreError(enumCoreErrors.NOT_LOGGED_IN, 'User must be authenticated.'); + } + + return await this.instanceCore.editInstance(ctx.req.user, input.instanceId, input.instanceName, input.updates); + }), + + /** + * Delete an instance + */ + deleteInstance: this.baseProcedure.input( + z.object({ + instanceId: z.string() + }) + ).mutation(async ({ input, ctx }) => { + const user = ctx.req.user; + const instance = await this.instanceCore.getInstanceById(input.instanceId); + if (user.type !== enumUserTypes.ADMIN || user.id !== instance.userId) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + 'Insufficient permissions.' + ); + } + + return await this.instanceCore.deleteInstance(user.id, input.instanceId); + }), + getQuotaAndFlavors: this.baseProcedure.query(async ({ ctx }) => { + if (!ctx.req.user || !ctx.req.user.id) { + throw new CoreError(enumCoreErrors.NOT_LOGGED_IN, 'User must be authenticated.'); + } + return await this.instanceCore.getQuotaAndFlavors(ctx.req.user); + }) + }); + } +} diff --git a/packages/itmat-apis/src/trpc/lxdProcedure.ts b/packages/itmat-apis/src/trpc/lxdProcedure.ts new file mode 100644 index 000000000..c5b8df42d --- /dev/null +++ b/packages/itmat-apis/src/trpc/lxdProcedure.ts @@ -0,0 +1,149 @@ +import { z } from 'zod'; +import { TRPCBaseProcedure, TRPCRouter } from './trpc'; +import {LxdManager} from '@itmat-broker/itmat-cores'; +import { CoreError, enumCoreErrors , LXDInstanceTypeEnum} from '@itmat-broker/itmat-types'; + +export class LXDRouter { + baseProcedure: TRPCBaseProcedure; + router: TRPCRouter; + lxdManager: LxdManager; + + + constructor(baseProcedure: TRPCBaseProcedure, router: TRPCRouter, lxdManager: LxdManager) { + this.baseProcedure = baseProcedure; + this.router = router; + this.lxdManager = lxdManager; + } + + _router() { + return this.router({ + getResources: this.baseProcedure.query(async ({ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.getResources(); + }), + + getInstances: this.baseProcedure.query(async ({ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.getInstances(); + }), + + getInstanceState: this.baseProcedure.input(z.object({ + container: z.string(), + project: z.string() + })).query(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.getInstanceState(input.container, input.project); + }), + + getOperations: this.baseProcedure.input(z.object({})).query(async ({ ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.getOperations(); + }), + + getOperationStatus: this.baseProcedure.input(z.object({ + operationId: z.string() + })).query(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.getOperationStatus(`/1.0/operations/${input.operationId}`); + }), + + getInstanceConsole: this.baseProcedure.input(z.object({ + container: z.string(), + options: z.object({ + height: z.number(), + width: z.number(), + type: z.string() + }) + })).mutation(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.getInstanceConsole(input.container, input.options); + }), + + getInstanceConsoleLog: this.baseProcedure.input(z.object({ + container: z.string() + })).mutation(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.getInstanceConsoleLog(input.container); + }), + + createInstance: this.baseProcedure.input(z.object({ + // set the config to LxdConfiguration + name: z.string(), + architecture: z.literal('x86_64'), + config: z.object({ + 'limits.cpu': z.string(), + 'limits.memory': z.string(), + 'user.username': z.string(), + 'user.user-data': z.string() + }), + source: z.object({ + type: z.string(), + alias: z.string() + }), + profiles: z.array(z.string()), + // use LXDInstanceTypeEnum + type: z.nativeEnum(LXDInstanceTypeEnum), + project: z.string() + })).mutation(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.createInstance({ + name: input.name, + architecture: input.architecture, + config: input.config, + source: input.source, + profiles: input.profiles, + type: input.type + } + , input.project); + }), + + updateInstance: this.baseProcedure.input(z.object({ + instanceName: z.string(), + payload: z.any(), + project: z.string() + })).mutation(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.updateInstance(input.instanceName, input.payload, input.project); + }), + + startStopInstance: this.baseProcedure.input(z.object({ + instanceName: z.string(), + action: z.enum(['start', 'stop']), + project: z.string() + })).mutation(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.startStopInstance(input.instanceName, input.action, input.project); + }), + + deleteInstance: this.baseProcedure.input(z.object({ + instanceName: z.string(), + project: z.string() + })).mutation(async ({ input, ctx }) => { + if (!ctx.req?.user) { + throw new CoreError(enumCoreErrors.AUTHENTICATION_ERROR, 'User must be authenticated.'); + } + return await this.lxdManager.deleteInstance(input.instanceName, input.project); + }) + }); + } +} \ No newline at end of file diff --git a/packages/itmat-apis/src/trpc/tRPCRouter.ts b/packages/itmat-apis/src/trpc/tRPCRouter.ts index a8d4acec3..108358ad4 100644 --- a/packages/itmat-apis/src/trpc/tRPCRouter.ts +++ b/packages/itmat-apis/src/trpc/tRPCRouter.ts @@ -9,6 +9,8 @@ import { StudyRouter } from './studyProcedure'; import { TRPCRouter } from './trpc'; import { UserRouter } from './userProcedure'; import { WebAuthnRouter } from './webauthnProcedure'; +import { InstanceRouter } from './instanceProcedure'; +import { LXDRouter } from './lxdProcedure'; export class TRPCAggRouter { router: TRPCRouter; @@ -22,7 +24,10 @@ export class TRPCAggRouter { domainRouter: DomainRouter; organisationRouter: OrganisationRouter; webAuthnRouter: WebAuthnRouter; - constructor(router: TRPCRouter, userRouter: UserRouter, driveRouter: DriveRouter, studyRouter: StudyRouter, dataRouter: DataRouter, roleRouter: RoleRouter, configRouter: ConfigRouter, logRouter: LogRouter, domainRouter: DomainRouter, organisationRouter: OrganisationRouter, webAuthnRouter: WebAuthnRouter) { + instanceRouter: InstanceRouter; + lxdRouter: LXDRouter; + + constructor(router: TRPCRouter, userRouter: UserRouter, driveRouter: DriveRouter, studyRouter: StudyRouter, dataRouter: DataRouter, roleRouter: RoleRouter, configRouter: ConfigRouter, logRouter: LogRouter, domainRouter: DomainRouter, organisationRouter: OrganisationRouter, webAuthnRouter: WebAuthnRouter, instanceRouter: InstanceRouter, lxdRouter: LXDRouter) { this.router = router; this.userRouter = userRouter; this.driveRouter = driveRouter; @@ -34,6 +39,8 @@ export class TRPCAggRouter { this.domainRouter = domainRouter; this.organisationRouter = organisationRouter; this.webAuthnRouter = webAuthnRouter; + this.instanceRouter = instanceRouter; + this.lxdRouter = lxdRouter; } _routers() { @@ -47,7 +54,9 @@ export class TRPCAggRouter { log: this.logRouter._router(), domain: this.domainRouter._router(), organisation: this.organisationRouter._router(), - webauthn: this.webAuthnRouter._router() + webauthn: this.webAuthnRouter._router(), + instance: this.instanceRouter._router(), + lxd: this.lxdRouter._router() }); } } diff --git a/packages/itmat-commons/jest.config.ts b/packages/itmat-commons/jest.config.ts index 3ee4a1fdb..cb4630206 100644 --- a/packages/itmat-commons/jest.config.ts +++ b/packages/itmat-commons/jest.config.ts @@ -19,5 +19,6 @@ export default { "node_modules", "\\.pnp\\.[^\\\/]+$", "test[\\/]fixtures[\\/]_minio" - ] + ], + moduleNameMapper:{"^uuid$": "uuid"} }; diff --git a/packages/itmat-commons/src/utils/poller.ts b/packages/itmat-commons/src/utils/poller.ts index 73536e35a..8eb6efe26 100644 --- a/packages/itmat-commons/src/utils/poller.ts +++ b/packages/itmat-commons/src/utils/poller.ts @@ -1,23 +1,17 @@ import type * as mongodb from 'mongodb'; -import { IJobEntry } from '@itmat-broker/itmat-types'; +import { enumJobHistoryStatus, enumJobStatus, IJob, IJobPollerConfig, IJobSchedulerConfig, IJobActionReturn } from '@itmat-broker/itmat-types'; import { Logger } from './logger'; -export interface IJobPollerConfig { - identity: string; // a string identifying the server; this is just to keep track in mongo - jobType?: string; // if undefined, matches all jobs - jobCollection: mongodb.Collection; // collection to poll - pollingInterval: number; // in ms - action: (document: IJobEntry) => Promise; // gets called every time there is new document -} - export class JobPoller { - private intervalObj?: NodeJS.Timeout; - private readonly matchObj: Partial; + private intervalObj?: NodeJS.Timer; + private readonly matchObj: unknown; + private readonly identity: string; - private readonly jobType?: string; - private readonly jobCollection: mongodb.Collection; + private readonly jobType?: string;w; + private readonly jobCollection: mongodb.Collection; private readonly pollingInterval: number; - private readonly action: (document: IJobEntry) => Promise; + private readonly action: (document: IJob) => Promise; + private readonly jobScheduler: JobScheduler; constructor(config: IJobPollerConfig) { this.identity = config.identity; @@ -25,46 +19,132 @@ export class JobPoller { this.jobCollection = config.jobCollection; this.pollingInterval = config.pollingInterval; this.action = config.action; - this.setInterval = this.setInterval.bind(this); this.checkForJobs = this.checkForJobs.bind(this); - this.matchObj = { - claimedBy: undefined, - status: 'QUEUED' - /*, lastClaimed: more then 0 */ - }; + // this.matchObj = { + // status: enumJobStatus.PENDING + // /*, lastClaimed: more then 0 */ + // }; + this.jobScheduler = new JobScheduler({ + ...config.jobSchedulerConfig, + jobCollection: config.jobCollection + }); - /* if this.jobType = config.jobType is undefined that this poller polls every job type */ - if (this.jobType !== undefined) { this.matchObj.jobType = this.jobType; } } public setInterval(): void { - this.intervalObj = setInterval(() => { this.checkForJobs().catch(() => { return; }); }, this.pollingInterval); + this.intervalObj = setInterval(() => { + void this.checkForJobs(); // Wrap the async call + }, this.pollingInterval); } private async checkForJobs() { - // Logger.log(`${this.identity} polling for new jobs of type ${this.jobType || 'ALL'}.`); + let job: IJob | null; try { - const updateResult = await this.jobCollection.findOneAndUpdate(this.matchObj, { - $set: { - claimedBy: this.identity, - lastClaimed: new Date().valueOf(), - status: 'PROCESSING' - } - }); - - if (updateResult !== null) { - Logger.log(`${this.identity} Claimed job of type ${updateResult.jobType} - id: ${updateResult.id}`); - if (this.intervalObj) - clearInterval(this.intervalObj); - await this.action(updateResult).catch(() => { return; }); - Logger.log(`${this.identity} Finished processing job of type ${updateResult.jobType} - id: ${updateResult.id}.`); - this.setInterval(); - } + // implement the scheduler here + job = await this.jobScheduler.findNextJob(); } catch (err) { - //TODO Handle error recording Logger.error(`${this.identity} Errored picking up a job: ${err}`); return; } + if (job) { + // update log status + const setObj: mongodb.UpdateFilter = {}; + try { + + const result = await this.action(job); + // Logger.log(`[JOB] Job Execution finished: ${new Date((Date.now())).toISOString()}, ${JSON.stringify(result?.error)}, ${JSON.stringify(result)}`); + + if (job.period) { + setObj['status'] = enumJobStatus.PENDING; + setObj['nextExecutionTime'] = Date.now() + job.period; + } + let newHistoryEntry; + if (result) { + if (!result.successful) { + newHistoryEntry = { + time: Date.now(), + status: enumJobHistoryStatus.FAILED, + errors: [result.error] + }; + // update the job status to failed if not periodic + setObj['status'] = job.period ? enumJobStatus.PENDING : enumJobStatus.ERROR; + } else { + newHistoryEntry = { + time: Date.now(), + status: enumJobHistoryStatus.SUCCESS, + errors: [] + }; + // update the job status to success + // numJobStatus.FINISHED if !job.period else enumJobStatus.PENDING; + setObj['status'] = job.period ? enumJobStatus.PENDING : enumJobStatus.FINISHED; + } + } + + const jobUpdate = await this.jobCollection.findOne({ id: job.id }); + if (jobUpdate) { + const currentHistory = jobUpdate.history || []; + setObj['history'] = [...currentHistory]; + if (newHistoryEntry) { + setObj['history'].push(newHistoryEntry); + } + await this.jobCollection.findOneAndUpdate({ id: job.id }, { + $set: setObj + }); + } + } catch (error) { + const currentHistory = job.history || []; + setObj['history'] = [...currentHistory, { + time: Date.now(), + status: enumJobHistoryStatus.FAILED, + errors: [error] + }]; + await this.jobCollection.findOneAndUpdate({ id: job.id }, { + $set: setObj + }); + + } + } } } + +export class JobScheduler { + private config: Required; + constructor(config: Required) { + this.config = config; + } + + public async findNextJob() { + let availableJobs = await this.config.jobCollection.find({ + status: enumJobStatus.PENDING + }).toArray(); + // we sort jobs based on the config + availableJobs = availableJobs.filter(el => { + if (this.config.reExecuteFailedJobs && el.history.filter(ek => ek.status === enumJobHistoryStatus.FAILED).length > this.config.maxAttempts) { + // console.log('Job failed more than max attempts: ', el.id); + return false; + } + if (Date.now() < el.nextExecutionTime) { + return false; + } + return true; + }).sort((a, b) => { + if (this.config.usePriority) { + if (a.priority > b.priority) { + return 1; + } else if (a.nextExecutionTime < b.nextExecutionTime) { + return 1; + } else { + return -1; + } + } else { + return a.nextExecutionTime - b.nextExecutionTime; + } + }); + const job = availableJobs[0]; + if (!job) { + return null; + } + return job; + } +} \ No newline at end of file diff --git a/packages/itmat-cores/config/config.sample.json b/packages/itmat-cores/config/config.sample.json index b56199779..a1588f92c 100644 --- a/packages/itmat-cores/config/config.sample.json +++ b/packages/itmat-cores/config/config.sample.json @@ -24,7 +24,8 @@ "drives_collection": "DRIVE_COLLECTION", "colddata_collection": "COLDDATA_COLLECTION", "domains_collection": "DOMAIN_COLLECTION", - "webauthn_collection": "WEBAUTHN_COLLECTION" + "webauthn_collection": "WEBAUTHN_COLLECTION", + "instance_collection": "INSTANCE_COLLECTION" } }, "server": { @@ -52,5 +53,19 @@ "adminEmail": "admin@example.com", "aeEndpoint": "http://localhost:9090", "useWebdav": true, - "webdavPort": 1900 + "webdavPort": 1900, + "lxdEndpoint": "https://localhost:8443", + "lxdStoragePool": "newpool", + "lxdProject": "default", + "webdavServer": "http://localhost", + "systemKey":{ + "pubkey": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnKXlNUDuKSAAClMe2cXf\n7maUnwcS2qsPYuG/gjZn8/Y+ICguPia5c+xlcQgHFYP7bBLeohsPbnp5bG0YTgeg\nGlLPoy+z/rZKBdKTXnv3fAy6ko9SnjpxfC/+kIkNfMcQe7YiwyGsvhVRYUErn5zZ\nQdxjot+DHXrrOsX00fu/E7mU0VqeISouRKkeYSzx4kJVYxGE89hrya8RyOmnyAay\nP5rhPicde4Oiag4J4K+t6eyNS3cel4JiKct8p+fPs+ryw/yzhYOffpOhkAJZvVcR\n/FRB1dUio2pn6BrqYZGWEJllZbOEbhKep8VKCQgqhI74Ab9Bl/hZU6n6+FFJbJfe\nFUaFqdkIMrEy5lKA7K50prhvkjokvf3XeDl+2WnfndIGVjcDMag1KXoQqdqdEScq\nT9DCQuib+JIdq4P81krtqxMOKg4c5cPhzTydACluAr6FLNjStCIMcI0uDHlmfIt/\nfd3/VdYoCxl0paZlA6Ku406HvpBk9oGz7i7eLQ2G6Wby3OP2EPA3JNW6zqe+AnfJ\nszLc39MrBeR4BMmj4++9QK5EsF+hg9KVVLwbaVs0HxlRqvnCN99M0EVPZL1DhnmQ\nQCn5O5vVAvmeDnTb+keI79qA6JIABdoEZSjFB2pWrwC/EQQj2nzL56EPT1oCNvLF\nBp5cqMAVb4vGCe2LAZ8MNxECAwEAAQ==\n-----END PUBLIC KEY-----\n", + "privkey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIJtTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQAxpn0LAybkjSkRMr\nK6XcqgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAKKAQmCpD6ZU0rz\nsu4s6q4EgglQVD13OfDhXnqrJT2v3CHq5rr5bip3P+j6yacgeB2l+s/IlY+0MM2f\n8JtOojUV9RYclV1dAi8krIOv2KVEH81Olb/mse5/Zr7BKBe5g+9jHF6EtWuTWqzX\n2TTkatULeJDnqUd+tBPBmuY3CUEl/6nUepTNzrRoVG4Rp/z2IbtaMx3IIpEZ9Fgt\n3iMkY0O8rtattwJp2k+eIFFf2c63J+NlXzIbuvLqNuPMq/GaUUCLtVsljZdtS/nw\n+tPQJfxW0hfawq1erIMu32Cnm2Tp0ZR2tGFIPMXT+zB/m4TEjbNEwS1abGqZ0y/l\nzJUckj5i5MCSmiF6Mxv9AVAXixlMWbvzhC9isyJKJY+3fde99/h71ZPMaO5NBJW6\nAzguuMc8DbVuGbWKUIkRMyj5Tm63YRasQIzTdrrZ6MR5QA4090RC8n3AsCguVzYB\n7+7fKhDNUIUsvuIXQ95WYpndJF/UbUIGtI8u1DpRZd7U/0lFkl2oX5UPIXgEY1Kv\n8JuWKhH/sYpMhmSWJqBmKV0ZhzDdMRJJtRJpSnLwEO1xchvTtjw9ySjdWZvGLF5z\n7uJwUKCu7aeo18YH4YpW66ladoBWXWN3bfZ5+d3mTMgV6d4z8Opg/8NzC1fsdfzm\n9uqrRFm9+dedCmnQ7w4wfKnDrmeg4oOMgXnbzyxVabYfade2Qk73bxdzUh66pocW\ngis6Q6MSwhHGXIShUKhcFtZAuON9Yedfe6ycSNrYP8Z2aU3Zmg8QcNMJnk3RXtms\nFPTt5SCz/re0ZsckgYD1CfQHB2zrp1PgV0O0wMGAw5FBDxbfEW+rA7qp21UJJBG8\n14YtepW+7sOFYZX164JZQrpCzZ4Qkdx9MeRMc6z670rOh132DpZ/vZLp9JEZt46I\nEXse7fcp3mfl2s4lMQERqYnphQ45ZUheNZge7qrPXKhE26Eqk4O8yE3EPPGzlIrf\nWLzUxCF/k19d+r7NLa6i5uHZ1RnB/kepTB9tdaVMY9CX68kIMKUN8HDNLxKzB4bC\nv6/VLjWt14P0OUnJ/uuZ3TwwXeAmBVJJb99UwKoUp6SKYyH1tShzMKNNNXZwdhhr\nCGf1T+1f1lnqDQX+YcQbmyV9EhPNpOtsSNDawJgN8qSs+fOY4vvpgPsLMmzlFsL3\nG8beIQaJ1Kryip9lJ7bKMZQzy/JTDHnH4+RHn57GcZtZTumFcHkhtexJXhWUxgQB\ncny0myAkvqt9/iF6hRGZYmcP2BWHRg1bXotETcdBk4gvpWf6byt95GA7OK1n+K7J\nrnit9DYG1Mq8K0t0Dju2FpMzOQMy3r+EciiAXwpU9Alio7lWbnYpZwIM5DtCTc07\nwwyFiMdOtku5Hg54VXSMAYJpY1+37bNtSlKkT/G24j7rT9Vx1DMeZwLY97bBZ9OE\napbmRWLUHi+jKdjkxId4+Bx46WIqYyawdKP7N6W+TCd+W7ChBm0/3L6E+tQYyt+z\nL5xfyNVZ3Cr3zzqHgrN33LFWAtEhVUzIxUOzXlWA7QdTSQsOYaERH+An01kRdqTA\nwg+pBNtd/4DskZdKMikGNsvOGhfx1b51S2zD/Jv7j/SvaWIiXkDtB7f/TBkYBBMB\nc3otTTtd8F6GnmuBQnRRK84pk3jhJmW5vEle6ok1tr/mWGU658ToreCWXZZ+FnLe\nmzY9z7qPFZu0P5tM4dxDrASwkxpC7ed3UDbzdL23TTL6naSW7SpGqOhgZ+sUrUAg\nLl7s5BrILXgqx+ymLFtaFW5Pk1h2bTJPQgBNP7VaUx6a5GI048uGS1loGKMzDjGl\n6mNVIAAaI5W7cJel/jph8JJivGav1rZGr5TGmMJtTkE4PBRYdhOfpIKeHY/Ylu3R\n43cKvN3ci3jvzdTkW577Msd2TFk150Mn0hP47stpNWqb0WWieNGjeiN4Wj270iqT\n+ifAkq1hBkU4Ytt1MQOwbpKOmrl51SZO8zUhVIgGPk3UaUtrm/T9usVAi7oyLdmO\ntKLNFPJp9CIotwwQybbHlMWQ862SgT6LOJkLsrPwxfRz3XajnBKH2ABMXRCHE5ZO\nqvTuZoxa4azIp+EATqroPkjzJn+VQRC//7X4kaL44Ijga8JzFedgyvIVyGdMMmAu\nyxHjY1xEryhUEefWA+FnSB47mJSxRKS9h1upsaR2k8toyMRHDKTVgQViyGq5O6Sj\n7Tl0fYnUM6hqRI+M9A54CFwouoeIQybbAC6cFs4ka087HKa53fe8ZfaAUmWX0iuW\nnrhwIe2Z2o48pxNWENduWWHsnBnPZnJwZtOoLRU4QHwYfBGxzBFfXPbhVb4XY9/9\npQICA+nBCZSNylnIiw7CFfFvfvgMYls0xcj+0jakSsyoVj01ojw7crcOGTULQs8Q\nbvjnkL+5Vk3BLdiKS9EGzn/4STJYbQOJ2VzgvAwTxYccrlOjiLAfj9pOZW56MV0F\n4vyxrWXqKGvKP7xXFaw2C3Aud4fII5gVGn3SDVg8em/jlOch+78fGVnprRALbOEv\nEh5NoG6m06hzvf1/90kKZd7ADzlFG7rLXhU2yazt+Om6Jnb/+Yf+iElTaBRv3BcF\nBHgeCZqMhJYtI6bkUJ7OpX3TkOP+s8qRI6BPUaIQPTdo5VQZwHS2OiligWSxnY4r\nXeCKcz6mT65vLcc4zkNHiv1r6nQmZb1wW2JFqWPOXzt+zK4ABJMv39b9JHney9Lm\nBhajnBmlYLbQCrF8u/W/hrkcNwUeezF79pThX6M4OSLs0pfGIaJytDOjYI8YpAmf\n8RZJJbdjCg5IqQqRBAjnt4hHVgvVIaFyb55OVa0VixbHXTs+zuZds0lyTiPNam3Q\n1JEJRy9JM//nxCwO4Lvpgfz1xNija3zVHISvlTWiiBrq8XgeTI4ZYKfw9CKMIUle\n49w9HGCYwfJltrmfjkAODsTGTABeVuKYTx2YgCUdUemlfdTVOTgjouhOjWQZELJs\nmmiXksmId/NP8B+jBnDs/ycvSCcqp4c+TAgeWnKF7ot6y/JaO2jLNVV4bjajfYY8\nd2OnZyJRWa56f+szPM+1kAaiJm2q6VQz29W2KD//Ng7noFC11RfzvwVBzKHsNojW\nbIfQQwIYwqz1RTmLeY23aaKiHXfIH6Av3qfpUxRlzMcAt7QoweY52dFd+TFjG99+\n615Zg5U22Wra3541u9Cj2P7RRhLH+gBqtfRmsl/6KZVFZfr1pnrxgQA=\n-----END ENCRYPTED PRIVATE KEY-----\n" + }, + "lxdCertFile": { + "cert": "-----BEGIN CERTIFICATE-----\nMIIFETCCAvmgAwIBAgIUQQ5zBtfZBlNWzwy9HJB4VV2BgTswDQYJKoZIhvcNAQEL\nBQAwGDEWMBQGA1UEAwwNbXktbHhkLWNsaWVudDAeFw0yMzEyMjAxNDQyMzlaFw0y\nNDEyMTkxNDQyMzlaMBgxFjAUBgNVBAMMDW15LWx4ZC1jbGllbnQwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCbwsuJRGSa5UbtYdjqQD0fo9pBwHSgHB14\n0zezRxlzgI8272y0/1Zwie4O1DxRPu/85zP/q2IyB9pr8RP4744a+ICPD3pdM56Y\ntHP34Pd6OxWemN+YRfst9VRHQ54hsz0lWD8qeNuuRL5MPgaaomNNC4LBJKRTC6Iy\nSmj1Mc7hbiTW7IVqwmFCmNtlW1iOY6JpJfhL+Zi13g0SBJwkoO7RoPKv9B3jee4x\nU6P7mqtUSOYvmO3i5hfI+cTPpnEBrlCZ2/5DzL8l4e78r6qCCwzhnSYYVcAgLIYY\nlcL47qjx5f8czrT/gZrOC010K0snsRV1sQ4ovsfKhc9qhZNAILIxfPoObsmIpRdD\n3b6JsfJlHVrWgiLiqwFMC/Fa+caKfPt+qhBTelPvwggST7y0/0k5hUrgX69NZe09\n2GH0Dr7I6IOyls9NDxMz7ZgOoY3bHsb47U6yTV57qjWwkbv2EkY/Nseqf5G0ZLE8\nyT5VgLhii+/zZb3Ch+T6dFjwNHfEWPmUY+9L9AHOYvU713NoH9gScGtWQtVGs3l3\nQ8z2pbAnQiRkYfRi2/4gQ5jMKnATztBx09tXx0vzspGWHB7j+9nq0fFgqmtBDulh\nfamZf/09ge1vbjbR4GrmWysKjrxGK6jezMa8yyK2jFs8+a5OatCV7n+MTRnn1Bib\n9/R/9At0dQIDAQABo1MwUTAdBgNVHQ4EFgQUke2sAkbSJmeXS3t+xlW2/JGGqCIw\nHwYDVR0jBBgwFoAUke2sAkbSJmeXS3t+xlW2/JGGqCIwDwYDVR0TAQH/BAUwAwEB\n/zANBgkqhkiG9w0BAQsFAAOCAgEAgQ7O3gY62ZDeJtLEIlp+orAG1n6LHBh2Z2Lc\nrSW/YbTIXSVF9FbKGBU6MTrxCdBNOEebSXQToYTKgz7KfUa6LnWmuqHMU7UYmVc8\ncOVVc40H5Pyyz1AHdCN4HxpRZgOzy5/95bYdsFN6JRfLqnIG+EW6XjK13arMshRR\n7E9JmLTnikmfdARUdALSHqQd+jRS4MfpU+3kvnaeSz48UwdN8/GxqFtWoRdmqBcV\ngJ8tzLzKM6MaUHum5H14dYISN2BOnAINHPqt0y+Lh7HJtmiBtLpoSbofUtIsAgYn\nDW0uNtL9gQONa5kKo8KHSoX5nmLdSQycj1Bhm84Z7qw83XNTgAjjcAf0eTWU2gjH\nbjX5XqdiQ+d4RmPEJsPoAKNVcvbJBT6ywG5P91GW8kbMCuQ/ieJw/fc0UdQRye+D\nZwbNoKez6Pfu8H8EbTa9sL7n51lu4VIgvNujpl5oK/MP6pt4lQzoqEmWe8V+8TN4\nVv/Ez+UpJqH6KHINruYU3OHO4CCH1Cj+zSdY7mKjeflOM268JD0mcZwN1Tyxsf16\nah7Xfs3kyRapi1+dQnrXphM39HcXudUpDoa6+n+aeMd+ly/Q7ZdpP0nbUrc7M2DK\n9VgTkGup7F20Yn1d/JU/taS2yKG7rwrX3swEQ/KgqMoTFkIES1hfiVE5z2Dj94BE\njnKGDYg=\n-----END CERTIFICATE-----", + "key": "-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCbwsuJRGSa5Ubt\nYdjqQD0fo9pBwHSgHB140zezRxlzgI8272y0/1Zwie4O1DxRPu/85zP/q2IyB9pr\n8RP4744a+ICPD3pdM56YtHP34Pd6OxWemN+YRfst9VRHQ54hsz0lWD8qeNuuRL5M\nPgaaomNNC4LBJKRTC6IySmj1Mc7hbiTW7IVqwmFCmNtlW1iOY6JpJfhL+Zi13g0S\nBJwkoO7RoPKv9B3jee4xU6P7mqtUSOYvmO3i5hfI+cTPpnEBrlCZ2/5DzL8l4e78\nr6qCCwzhnSYYVcAgLIYYlcL47qjx5f8czrT/gZrOC010K0snsRV1sQ4ovsfKhc9q\nhZNAILIxfPoObsmIpRdD3b6JsfJlHVrWgiLiqwFMC/Fa+caKfPt+qhBTelPvwggS\nT7y0/0k5hUrgX69NZe092GH0Dr7I6IOyls9NDxMz7ZgOoY3bHsb47U6yTV57qjWw\nkbv2EkY/Nseqf5G0ZLE8yT5VgLhii+/zZb3Ch+T6dFjwNHfEWPmUY+9L9AHOYvU7\n13NoH9gScGtWQtVGs3l3Q8z2pbAnQiRkYfRi2/4gQ5jMKnATztBx09tXx0vzspGW\nHB7j+9nq0fFgqmtBDulhfamZf/09ge1vbjbR4GrmWysKjrxGK6jezMa8yyK2jFs8\n+a5OatCV7n+MTRnn1Bib9/R/9At0dQIDAQABAoICABQv0SHaK6Zo3qIMqLM7XhtH\n/hUVdJ1SPNOYSZjCNzSL+IOoe2XoeHMfZ9X41am7RsPNJyUUjiZpHtAXL30iIZWx\n7TTH+cDd3B5zDLCXMcZf+p5SYRrNabn8GKR1HqfEIMGX8p/LiBUe3aw97FcwLT8i\npcXoZ3pH1gk/MY54RRVcVAlIIBvE/hvxJEYPlPuR7Xjugy6S4Pgjw305UbnsODy8\n55cRmVS49xDiLoIrpSwdl+Tv+gIKdhIFpOc7QrFyvl7NGeKpmisLtrsoUn3bdsZA\nEAbes4a389fN2Qtayv8xeA3Aux5+KpUCS/3T+pU1/c+srGbFda6esade5nBM4dW/\nidLFEXDRjar3kYBq/4hfg/7MOIOZX9XFa3jg0yhzCHiTDFHmXKKGC+EABOFcOh1E\nb9UUunqb37tNiKthnLM+wLlXQNponmzyoEsooRCyzxjcjyCrdmRogwoswt+hOmdV\nl3dnvxYwo8SHUq9P4HCGk9QGuvpmszGtV3kCxfouT1kT70ZUPyLZclax/HDCDnq2\n4OMBln4pQk/EGsMLhzbQeQNbpOR2C5b4GEwPbjGExHJde3GBneJxQGSXJq+7Nrsx\nELtZgoKFa46dB+vm0NBz7v8JiRAxiRS/a0xvEQMXrYOf2UhhjxnBfVxGfNyxbEA6\nCIoK8pHWeua+OuzJHQFlAoIBAQDW0vKh3nfMxKV/LuJPJ4HgHB9Ld9xD5QQnpuUn\nVi91hBGgcRukWGUhsypBln41ChuI9O3ZrQApuNM7+2on5SLElwef+65G/W2IbHPb\nUKGpgcxyyPv7/qvQtWGV5U+OIdmxXlNQrruc6Neq8XhlEyHwPDpwQBKfbB8gzfOz\n3jQ6BOFXR+t3Nx+lwErXPRxWTN+7ccb6rOkCF4m2LJwdf7tjAvhHjtuUMASjaIt4\na/QXERuOSvUbp5LcZA9F/YgKEs+yV0AtwyjTnWfzl14rH8oh0AccDNDNX//z+wXv\nYwohdnQyX2mwfxJ3IMSBkAXlikc89PLgxapx8RjSsBcnOH4HAoIBAQC5nbiAjAHg\nJ5AYj8t/sDZnhTvsiLsi4Vo/2/Unrx599a9ChQSTv6gVMVweKtWI43TK7Ez8tsfQ\ne9psN6c9XCE6mc9S6KsQhEgbSict6hP/ayPeb+TWHpUju5qCw6PuXWy+rwZKbgza\nFtMTp4QbgJcCY4WF6YOxNRSTjlTQw0nOMc/VsqGdVDqkVeZ+A0bMlz2tXIXvJiIV\nUBgjgdt+JQnGZplS12ooL8V+rLoHDcUwKH3oeh9ON1HNe3vb7dAlyR+HsC1Mm4Pn\nfB4Xc86vMYX/5OVLwuSdN3VpsBPnDGmkH8+U5pwB+FhiIeS0947PDJwVsaiZXrwp\nRxzLvbPl5ZqjAoIBAQCd887Y+9VEJ1a0NAnMP3U8DhFokQHQngQ3D3ywNquAkZHQ\nUToM1b3OUIkCXp//aaYjRkvYYF6dTrtqAArmuJCe0ZmWpRxYMCCoTW3GVPv4wWpM\n/8BfYbp9I9BTwZ6EGBmTU5KY4VErJvzkQNXQI4gxtmcVf9bxhzNAEI5es0PdYRc6\n8LOOHWbUnZWputIqFi3vCdJPIHHWyu3Dl/tVqURjoZxiKQUEaWYPrF/YNC/uAfMr\n5athIQ5Xo+6i/K5ZEcnLDGIxA6zyI2t6bNKdjKs3v1hq5HVmfG6auvh7MmwRfKIl\nI4h3cIdoNhymUvoy80A77rLiWBRh4O7qgvUTLnNjAoIBAAiDgXjz8woTBnr57X2X\n2Yb6B3ub8elxqLARKLd/QsjIQhes/j7ApbcDIpSHpm+27x53pDhbMeMQKz6XduZL\nmYKUl3vYDDCfwKbvychDWlN22JhVTYu8r16KNlYVHynJwzkj0ggL8C74qQnXvyl7\nxnFnmzI/ObkhFCaIer9wlawNgNjubpdGy8HJ5t6Uy+SKc1vGSKZle16648CNLkIk\n9MPS5Ol10/qv5kEfLxEvwoGo+c11/IWb5/ai2VWHHOr+xKF2pT1ETNKLUN4Gg85p\nWRoZp6LH97B2YL5OQztvyFCs3NqZkUJN38/wegsK59P7YhVkprUSMVM7XcjClMPQ\nuj0CggEBAJVS6a3cxR5b05tVUrRw31MSkdj21JRLf3DUtkPY711+vJBZXCEjZU6c\nj2D4wMMFNqD4tc3D2C+ro6L/iyHtyUl2LC9h+Pfa/7UaFsqZI2XeyPUiyzkoWYZJ\n7KNuUefNxbhnJFM3kwW9Aga03SPhYiS7OiLzQzhSaiFd6WX4GsmNrKUUt12rp0NE\ndoSLqqmhUazwC97t8X/gUC7x54zM4WY4665ljQ8tP18/0pSPw0R+gutclygeK8Lz\nou9j96INWlOgMdFZXbvf5rXe79hFA4fOU64LdxSIXOD530wNOB0hZTuDTJWbho6P\na+jChGwRN78gwuxmniJraA5Hl6aaSyg=\n-----END PRIVATE KEY-----" + }, + "lxdRejectUnauthorized": false, + "jupyterPort": 8888 } \ No newline at end of file diff --git a/packages/itmat-cores/src/coreFunc/instanceCore.ts b/packages/itmat-cores/src/coreFunc/instanceCore.ts new file mode 100644 index 000000000..cd667e612 --- /dev/null +++ b/packages/itmat-cores/src/coreFunc/instanceCore.ts @@ -0,0 +1,843 @@ +import { IUserConfig, enumConfigType, CoreError, enumCoreErrors, IInstance, LXDInstanceTypeEnum, enumInstanceStatus, enumAppType, IUser, enumUserTypes, enumJobType, enumOpeType, enumMonitorType, enumJobStatus, ISystemConfig} from '@itmat-broker/itmat-types'; +import { v4 as uuid } from 'uuid'; +import { DBType } from '../database/database'; +import { Logger, Mailer} from '@itmat-broker/itmat-commons'; +import { IConfiguration} from '../utils'; +import { ConfigCore } from './configCore'; +import { JobCore} from './jobCore'; // Ensure you have the correct import path +import { UserCore } from './userCore'; + +export class InstanceCore { + db: DBType; + mailer: Mailer; + config: IConfiguration; + configCore: ConfigCore; + JobCore: JobCore; + UserCore: UserCore; + lxdProject: string; + private previousCpuUsage: Record = {}; + private previousCpuTimestamp: Record = {}; + + constructor(db: DBType, mailer: Mailer, config: IConfiguration, jobCore: JobCore, userCore: UserCore) { + this.db = db; + this.mailer = mailer; + this.config = config; + this.configCore = new ConfigCore(db); + this.JobCore = jobCore; + this.UserCore = userCore; + // set lxd project from config + this.lxdProject = this.config.lxdProject || 'default'; + } + /** + * Convert memory size in bytes to a formatted string like '4GB'. + * @param memory - The memory size in bytes. + * @returns Formatted memory size string. + */ + private formatMemory(memory: number): string { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let index = 0; + let size = memory; + + while (size >= 1024 && index < units.length - 1) { + size /= 1024; + index++; + } + + return `${Math.round(size)}${units[index]}`; + } + + /** + * Convert memory string like '4GB' to bytes. + * @param memoryStr - The memory string to parse. + * @returns Memory size in bytes. + */ + private parseMemory(memoryStr: string): number { + const units: Record = { + B: 1, + KB: 1024, + MB: 1024 * 1024, + GB: 1024 * 1024 * 1024, + TB: 1024 * 1024 * 1024 * 1024 + }; + // if memory string is undefined, return 0 + if (!memoryStr) { + return 0; + } + const match = memoryStr.match(/^(\d+(?:\.\d+)?)([KMGT]?B)$/); + if (!match) { + throw new Error(`Invalid memory string: ${memoryStr}`); + } + + const value = parseFloat(match[1]); + const unit = match[2]; + + return value * (units[unit] || 1); + } + /** + * Get an instance by ID. + * + * @param instanceId - The ID of the instance to retrieve. + * @return IInstance - The instance object. + */ + public async getInstanceById(instanceId: string): Promise { + const instance = await this.db.collections.instance_collection.findOne({ id: instanceId }); + if (!instance) { + throw new CoreError(enumCoreErrors.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY, + 'Instance not found.' + ); + } + return instance; + } + + /** + * Create an instance. + * + * @param userId - The id of the user creating the instance. + * @param name - The name of the instance. + * @param type - The type of the instance ('virtual-machine' or 'container'). + * @param appType - The application type of the instance (e.g., 'Jupyter', 'Matlab'). + * @param lifeSpan - The life span of the instance in seconds. + * @param project - The LXD project of the instance (optional, defaults to 'default'). + * + * @return IInstance + */ + public async createInstance(userId: string, username: string, name: string, type: LXDInstanceTypeEnum, + appType: enumAppType, lifeSpan: number, cpuLimit?: number, memoryLimit?: string, diskLimit?: string): Promise { + + const instance_id = uuid(); // Generate a unique ID for the instance + + // generate the token for instance + let instanceSystemToken; + try { + // fake Id + const data = await this.UserCore.issueSystemAccessToken(userId); + instanceSystemToken = data.accessToken; + } catch (error) { + Logger.error(`Error generating token: ${error}`); + throw new Error('Error generating instance token.'); + } + + const webdavServer = this.config.webdavServer; + const webdavMountPath = `/home/ubuntu/${username}_Drive`; + + const instanceProfile = type===LXDInstanceTypeEnum.VIRTUAL_MACHINE? 'matlab-profile' : 'jupyter-profile'; + + // Prepare user-data for cloud-init to initialize the instance + const cloudInitUserDataContainer = ` +#cloud-config +packages: + - davfs2 +users: + - name: ubuntu + groups: sudo + sudo: "ALL=(ALL) NOPASSWD:ALL" + shell: /bin/bash +write_files: + - path: /etc/profile.d/instance_token.sh + content: | + export DMP_TOKEN="${instanceSystemToken}" + permissions: '0755' + - path: /etc/davfs2/secrets + content: | + ${webdavServer} ubuntu ${instanceSystemToken} + permissions: '0600' + - path: /etc/systemd/system/webdav-mount.service + content: | + [Unit] + Description=Mount WebDAV on startup + After=network.target + [Service] + Type=oneshot + ExecStart=/bin/mount -t davfs ${webdavServer} ${webdavMountPath} -o rw,uid=ubuntu,gid=ubuntu + ExecStartPre=/bin/mkdir -p ${webdavMountPath} + RemainAfterExit=true + [Install] + WantedBy=multi-user.target + permissions: '0644' + - path: /root/.jupyter/jupyter_notebook_config.py + content: | + c.NotebookApp.ip = "0.0.0.0" + c.NotebookApp.port = ${this.config.jupyterPort} + c.NotebookApp.open_browser = False + c.NotebookApp.token = "" + c.NotebookApp.password = "" + c.NotebookApp.allow_root = True + c.NotebookApp.base_url = "/jupyter/${instance_id}" + c.NotebookApp.notebook_dir = "/home/ubuntu" + permissions: '0644' +runcmd: + - | + DEFAULT_USER="\${USERNAME:-ubuntu}" + if ! getent group nopasswdlogin > /dev/null; then + addgroup nopasswdlogin + fi + if ! id -u \${DEFAULT_USER} > /dev/null 2>&1; then + adduser \${DEFAULT_USER} nopasswdlogin || true + fi + passwd -d \${DEFAULT_USER} || true + echo "@reboot \${DEFAULT_USER} DISPLAY=:0 /home/\${DEFAULT_USER}/disable_autolock.sh" | crontab -u \${DEFAULT_USER} - + cat << 'EOF' > "/home/\${DEFAULT_USER}/disable_autolock.sh" + #!/bin/bash + if [ -z "\${DISPLAY}" ]; then + echo "No DISPLAY available. Skipping GUI settings." + else + dbus-launch gsettings set org.gnome.desktop.screensaver lock-enabled false + dbus-launch gsettings set org.gnome.desktop.session idle-delay 0 + fi + EOF + chmod +x "/home/\${DEFAULT_USER}/disable_autolock.sh" + chown \${DEFAULT_USER}: "/home/\${DEFAULT_USER}/disable_autolock.sh" + - sleep 10 + - systemctl daemon-reload + - systemctl enable webdav-mount.service + - systemctl start webdav-mount.service + - | + if [ -d "/home/\${DEFAULT_USER}" ]; then + ln -sf ${webdavMountPath} "/home/\${DEFAULT_USER}/${username}_Drive" + chown \${DEFAULT_USER}:\${DEFAULT_USER} "/home/\${DEFAULT_USER}/${username}_Drive" + fi + - source /etc/profile.d/dmpy.sh + - source /etc/profile.d/instance_token.sh +`; + + + const cloudInitUserDataVM = ` +#cloud-config +packages: + - davfs2 +users: + - name: ubuntu + groups: sudo + sudo: "ALL=(ALL) NOPASSWD:ALL" + shell: /bin/bash +write_files: + - path: /etc/profile.d/instance_token.sh + content: | + export DMP_TOKEN="${instanceSystemToken}" + permissions: '0755' + - path: /etc/davfs2/secrets + content: | + ${webdavServer} ubuntu ${instanceSystemToken} + permissions: '0600' + - path: /etc/systemd/system/webdav-mount.service + content: | + [Unit] + Description=Mount WebDAV on startup + After=network.target + [Service] + Type=oneshot + ExecStart=/bin/mount -t davfs ${webdavServer} ${webdavMountPath} -o rw,uid=ubuntu,gid=ubuntu + ExecStartPre=/bin/mkdir -p ${webdavMountPath} + RemainAfterExit=true + [Install] + WantedBy=multi-user.target + permissions: '0644' +runcmd: + # Removing MATLAB licenses + - rm -rf /usr/local/MATLAB/R2022b/licenses/ + - rm /home/ubuntu/.matlab/R2022b_licenses/license_matlab-ubuntu-vm_600177_R2022b.lic + - rm /usr/local/MATLAB/R2022b/licenses/license.dat + - rm /usr/local/MATLAB/R2022b/licenses/license*.lic + # New commands for user setup + - | + DEFAULT_USER="\${USERNAME:-ubuntu}" + if ! getent group nopasswdlogin > /dev/null; then + addgroup nopasswdlogin + fi + if ! id -u \${DEFAULT_USER} > /dev/null 2>&1; then + adduser \${DEFAULT_USER} nopasswdlogin || true + fi + passwd -d \${DEFAULT_USER} || true + echo "@reboot \${DEFAULT_USER} DISPLAY=:0 /home/\${DEFAULT_USER}/disable_autolock.sh" | crontab -u \${DEFAULT_USER} - + cat << 'EOF' > "/home/\${DEFAULT_USER}/disable_autolock.sh" + #!/bin/bash + if [ -z "\${DISPLAY}" ]; then + echo "No DISPLAY available. Skipping GUI settings." + else + dbus-launch gsettings set org.gnome.desktop.screensaver lock-enabled false + dbus-launch gsettings set org.gnome.desktop.session idle-delay 0 + fi + EOF + chmod +x "/home/\${DEFAULT_USER}/disable_autolock.sh" + chown \${DEFAULT_USER}: "/home/\${DEFAULT_USER}/disable_autolock.sh" + - sleep 10 + - systemctl daemon-reload + - systemctl enable webdav-mount.service + - systemctl start webdav-mount.service + - | + if [ -d "/home/\${DEFAULT_USER}/Desktop" ]; then + ln -sf ${webdavMountPath} "/home/\${DEFAULT_USER}/Desktop/${username}_Drive" + chown \${DEFAULT_USER}:\${DEFAULT_USER} "/home/\${DEFAULT_USER}/Desktop/${username}_Drive" + fi + - source /etc/profile.d/dmpy.sh + - source /etc/profile.d/instance_token.sh +`; + + const cloudInitUserData = type ===LXDInstanceTypeEnum.VIRTUAL_MACHINE? cloudInitUserDataVM : cloudInitUserDataContainer; + // add boot-time script, to be executed on first boot + const instaceConfig = { + 'limits.cpu': cpuLimit ? cpuLimit.toString() : '2', + 'limits.memory': memoryLimit ? memoryLimit : '16GB', + 'user.disk': diskLimit ? diskLimit : '20GB', + 'user.username': username, // store username to instance config + 'user.user-data': cloudInitUserData // set the cloud-init user-data + }; + + const instanceEntry: IInstance = { + id: instance_id, + name, + userId, + username, + status: enumInstanceStatus.PENDING, + type, + appType, + createAt: Date.now(), + lifeSpan, + instanceToken: instanceSystemToken, + project: this.lxdProject, // the project name by config, idea-fast staging or production + webDavToken: instanceSystemToken, // Assign or generate as needed, System token + life: { + createdTime: Date.now(), + createdUser: userId, + deletedTime: null, + deletedUser: null + }, + metadata: {}, + config: instaceConfig + }; + + await this.db.collections.instance_collection.insertOne(instanceEntry); + + // Create the job to create the LXD instance on LXD server + const jobName = `Create ${appType} Instance: ${name}`; + const jobType = enumJobType.LXD; + const executorPath = '/lxd'; // The executor path for LXD jobs + + // Override defaults if cpuLimit and memoryLimit are provided + if (cpuLimit) { + instaceConfig['limits.cpu'] = cpuLimit.toString(); // Ensure it's a string + } + if (memoryLimit) { + instaceConfig['limits.memory'] = memoryLimit; + } + + // Construct the payload from the job document parameters + // Prepare job data including the operation and instanceId + const lxd_metadata = { + operation: enumOpeType.CREATE, + instanceId: instanceEntry.id, + payload: { + name: name, + architecture: 'x86_64', + config: instaceConfig, + devices: { + eth0: { + name: 'eth0', + nictype: 'bridged', + parent: 'lxdbr0', // Ensure the correct bridge is used + type: 'nic' + }, + root: { + path: '/', + pool: this.config.lxdStoragePool, + size: diskLimit ? diskLimit : '20GB', + type: 'disk' + } + }, + source: { + type: 'image', + alias: type===LXDInstanceTypeEnum.VIRTUAL_MACHINE? 'ubuntu-matlab-image' : 'ubuntu-jupyter-container-image' + }, + profiles: [instanceProfile], + type: type, // 'virtual-machine' or 'container' + project: this.lxdProject // Ensure the correct project is used + } + }; + + // Call the createJob method of JobCore to create a new job + await this.JobCore.createJob( + userId, + jobName, + jobType, + undefined, + undefined, + { path: executorPath, type: 'lxd', id: instance_id }, + null, + null, + 1, + lxd_metadata); + + return instanceEntry; + } + + /** + * Start and Stop instance + */ + public async startStopInstance(userId: string, instanceId: string, action: enumOpeType.START | enumOpeType.STOP): Promise { + // Retrieve instance details from the database + const instance = await this.db.collections.instance_collection.findOne({ id: instanceId }); + if (!instance) { + throw new Error('Instance not found.'); + } + // update the instance status + // Optimistically update the instance status + const newStatus = action === 'start' ? enumInstanceStatus.STARTING : enumInstanceStatus.STOPPING; + await this.db.collections.instance_collection.updateOne({ id: instanceId }, { + $set: { status: newStatus } + }); + + + // Create the job to start/stop the LXD instance on the LXD server + const jobName = `${action.toUpperCase()} ${instance.appType} Instance: ${instance.name}`; + const jobType = enumJobType.LXD; + const executorPath = `/lxd/${action}`; + + const lxd_metadata = { + operation: action, + instanceId: instance.id + }; + + // Call the createJob method of JobCore to create a new job for starting/stopping the instance + await this.JobCore.createJob(userId, jobName, jobType, undefined, undefined, { id: instanceId, type: 'lxd', path: executorPath }, null, null, 1, lxd_metadata); + + // Optionally, immediately return the instance object or wait for the job to complete based on your application's needs + return instance; + } + + /** + * restartInstance with new lifespan, and update the instance's create time + * TODO: also generate and update the instance token + */ + public async restartInstance(userId: string, instanceId: string, lifeSpan: number): Promise { + + // Update the instance's create time and lifespan + const result = await this.db.collections.instance_collection.findOneAndUpdate({ id: instanceId }, { + $set: { + createAt: Date.now(), + lifeSpan: lifeSpan, + status: enumInstanceStatus.STARTING + + } + }, { + returnDocument: 'after' + }); + + if (!result) { // Check if a document was found and updated + throw new CoreError( + enumCoreErrors.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY, + 'Instance does not exist or update failed.'); + } + const instance = result; + + // Create the job to update the LXD instance on the LXD server + const jobName = `Restart ${instance.appType} Instance: ${instance.name} for user ${userId}`; + const jobType = enumJobType.LXD; + const executorPath = '/lxd/start'; + + const lxd_metadata = { + operation: enumOpeType.START, + instanceId: instance.id + }; + + // Call the createJob method of JobCore to create a new job for restarting the instance + await this.JobCore.createJob(userId, jobName, jobType, undefined, undefined, { id: instanceId, type: 'lxd', path: executorPath }, null, null, 1, lxd_metadata); + + + return instance; + } + + + /** + * Delete an instance. + */ + public async deleteInstance(UserId: string, instanceId: string): Promise { + const result = await this.db.collections.instance_collection.findOneAndUpdate({ id: instanceId }, { + $set: { + 'life.deletedTime': Date.now(), + 'life.deletedUser': UserId, + 'status': enumInstanceStatus.DELETED + } + }, { + returnDocument: 'after' + }); + + if (!result) { // Check if a document was found and updated + throw new CoreError( + enumCoreErrors.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY, + 'Instance does not exist or delete failed.'); + } + + const instance = result; // Access the updated document + const appType = instance.appType; + + // Create the job to delete the LXD instance on the LXD server + const jobName = `DELETE ${appType} Instance: ${instance.name}`; + const jobType = enumJobType.LXD; + const executorPath = '/lxd/delete'; + + const lxd_metadata = { + operation: enumOpeType.DELETE, + instanceId: instance.id + }; + + // Call the createJob method of JobCore to create a new job for deleting the instance + await this.JobCore.createJob(instance.userId, jobName, jobType, undefined, undefined, { id: instanceId, type: 'lxd', path: executorPath }, null, null, 1, lxd_metadata); + return true; + } + + /** + * Get all instances and update their status based on lifespan. + * + * @param userId The ID of the user managing instances. + * @return IInstance[] The list of instances. + */ + public async getInstances(userId: string): Promise { + // Retrieve all instances that haven't been deleted + const instances = await this.db.collections.instance_collection.find({ + // status is not DELETED + status: { $nin: [enumInstanceStatus.DELETED] }, + userId: userId // Ensure to only fetch instances related to the userId if necessary + }).toArray(); + + // create a LXD_MONITOR job to update the status of the instances + const jobName = `Update Instance Status of User: ${userId}`; + const jobType = enumJobType.LXD_MONITOR; + const executorPath = '/lxd/monitor'; + const period = 60 * 1000; // 1 minute + // Check if there is a pending job for the user instances update + const existingJobs = await this.JobCore.getJob({ name: jobName, type: jobType, status: enumJobStatus.PENDING }); + + if (existingJobs.length === 0) { + const metadata = { + operation: enumMonitorType.STATE, + userId: userId + }; + const instanceIds = instances.map(instance => instance.id).join('|'); + await this.JobCore.createJob(userId, jobName, jobType, undefined, period, { id: instanceIds, type: 'lxd', path: executorPath }, null, null, 1, metadata); + } + + const now = Date.now(); + + // Create a series of promises to handle lifespan and status updates + const updates = instances.map(async (instance) => { + const lifeDuration = now - instance.createAt; + const remainingLife = instance.lifeSpan * 1000 - lifeDuration; + + // Check if the lifespan has been exceeded + if (remainingLife <= 0) { + // Check if the instance is not already stopped + if (instance.status !== enumInstanceStatus.STOPPED && instance.status !== enumInstanceStatus.STOPPING + && instance.status !== enumInstanceStatus.FAILED + ) { + // Stop the instance and update status in the database + await this.startStopInstance(userId, instance.id, enumOpeType.STOP); + } + + await this.db.collections.instance_collection.updateOne( + { id: instance.id }, + { + $set: { + lifeSpan: 0 // Reset lifespan to zero as it's now considered ended + } + } + ); + + } + // Ensure instance config and limits exist + const cpuLimit = 'limits.cpu' in instance.config ? parseInt(instance.config['limits.cpu'] as string) : 1; + const memoryLimit = 'limits.memory' in instance.config ? this.parseMemory(instance.config['limits.memory'] as string) : 16 * 1024 * 1024 * 1024; // Default to 16GB + + let cpuUsage = 0; + let memoryUsage = 0; + + if (instance.status === enumInstanceStatus.RUNNING && instance.lxdState) { + const cpuUsageRaw = instance.lxdState.cpu.usage; + const memoryUsageRaw = instance.lxdState.memory.usage; + + // Retrieve previous values from local storage (or instance metadata if necessary) + const previousCpuUsageRaw = this.previousCpuUsage[instance.id] || 0; + const previousTimestamp = this.previousCpuTimestamp[instance.id] || now; + + + // Calculate the time interval in seconds + const intervalSeconds = (now - previousTimestamp) / 1000; + + // Calculate the difference in CPU usage + const cpuUsageDelta = cpuUsageRaw - previousCpuUsageRaw; + + // Convert CPU usage delta to seconds + const cpuUsageDeltaSeconds = cpuUsageDelta / 1e9; + + // Calculate CPU usage percentage + if (intervalSeconds > 0) { + cpuUsage = (cpuUsageDeltaSeconds / (cpuLimit * intervalSeconds)) * 100; + } + + // Calculate memory usage percentage + memoryUsage = (memoryUsageRaw / memoryLimit) * 100; + + // Store the current values for the next calculation + this.previousCpuUsage[instance.id] = cpuUsageRaw; + this.previousCpuTimestamp[instance.id] = now; + + } + + // Assign CPU and memory usage to instance metadata + instance.metadata = { + ...instance.metadata, + cpuUsage: cpuUsage > 100 ? 100 : (cpuUsage < 0 ? 0 : Math.round(cpuUsage)), // Clamp values between 0 and 100 + memoryUsage: memoryUsage > 100 ? 100 : (memoryUsage < 0 ? 0 : Math.round(memoryUsage)) // Clamp values between 0 and 100 + }; + + // resolve the promise with the instance object + return instance; + }); + + // Wait for all updates to complete + await Promise.all(updates); + + // Fetch and return the updated list of instances + return instances.map(instance => { + // calculate the cpu and memory usage percentage, only for running instances + // TODO, {cpuUsage: 0, memoryUsage: 0} + + // This will provide the updated remaining life span without persisting it + const lifeDuration = now - instance.createAt; + const remainingLifeHours = (instance.lifeSpan * 3600000 - lifeDuration) / 3600000; + return { + ...instance, + lifeSpan: remainingLifeHours > 0 ? remainingLifeHours : 0 + }; + }); + } + + /** + * Edit an instance. + * + * @param userId - The id of the user editing the instance. + * @param instanceId - The id of the instance to edit. + * @param updates - Object containing the fields to update. + * + * @return IInstance + */ + public async editInstance(requester: IUser, instanceId: string | null | undefined, instanceName: string | null | undefined, updates: Record): Promise { + + // Check that at least one of the identifier fields is provided + if (!instanceId && !instanceName) { + throw new CoreError( + enumCoreErrors.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY, + 'Instance ID or name must be provided.' + ); + } + + // Find the instance by either ID or name + const instanceQuery: Record = {}; + if (instanceId) instanceQuery['id'] = instanceId; + if (instanceName) instanceQuery['name'] = instanceName; + + const instance = await this.db.collections.instance_collection.findOne(instanceQuery); + + if (!instance) { + throw new CoreError( + enumCoreErrors.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY, + 'Instance does not exist.' + ); + } + + + // Check if the requester has permission to edit the instance + if (requester.username !== instance.username && requester.type !== enumUserTypes.ADMIN) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + 'User does not have permission to edit this instance.' + ); + } + + // Update the config object directly if cpuLimit or memoryLimit are provided + if (updates['cpuLimit'] || updates['memoryLimit']) { + const currentConfig = instance.config ?? {}; + + updates['config'] = { + ...currentConfig, // Preserve existing config values + 'limits.cpu': updates['cpuLimit']?.toString() || currentConfig['limits.cpu'], + 'limits.memory': updates['memoryLimit'] || currentConfig['limits.memory'] + }; + + // Remove top-level cpuLimit and memoryLimit after applying to config + delete updates['cpuLimit']; + delete updates['memoryLimit']; + } + // Update the instance in the database + const result = await this.db.collections.instance_collection.findOneAndUpdate( + { _id: instance._id }, // Use the unique `_id` from MongoDB + { $set: updates }, + { returnDocument: 'after' } + ); + + if (!result) { + throw new CoreError( + enumCoreErrors.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY, + 'Failed to update the instance.' + ); + } + + // Create the job to update the LXD instance on the LXD server + // Prepare job metadata for the LXD update operation + const metadata = { + operation: enumOpeType.UPDATE, + instanceToken: instance.instanceToken ?? '', + instanceId: result.id, + updates: updates + }; + const appType = result.appType; + + // Create the job for the LXD operation + // TODO: better to package all of them to the jobCore as a attch function + const jobName = `Update Config for ${appType} Instance: ${result.name}`; + const executorPath = '/lxd/update'; + await this.JobCore.createJob(requester.id, jobName, enumJobType.LXD, undefined, undefined, { id: instanceId || instanceName || '', type: 'lxd', path: executorPath }, null, null, 1, metadata); + + + return result; + } + + // get the ip of the instance by instanceId or update the state of the instance + + /** + * Get or update the container IP (state) from the database or trigger monitor if not available. + * + * @param instance_id - The ID of the instance. + * @param user_id - The user ID (optional, for ownership validation). + * @return string | null - The container IP, or null if not available. + */ + public async getContainerIP(instance_id: string, user_id?: string) { + // Retrieve the instance by the instance_id + const instance: IInstance = await this.getInstanceById(instance_id); + + // Check instance ownership + if (user_id && instance.userId !== user_id) { + Logger.error('User not authorized to access the instance'); + throw new Error('User not authorized to access the instance'); + } + + // If instance is not running, return null (no IP available) + if (instance.status !== 'RUNNING') { + Logger.warn(`Instance ${instance_id} is not running, no IP available.`); + return null; + } + + // Check if the instance state is available in the database (via the monitor job) + if (instance.lxdState && instance.lxdState.network && instance.lxdState.network['eth0']) { + const ipv4Address = instance.lxdState.network['eth0'].addresses + .filter((addr) => addr.family === 'inet') + .map((addr) => addr.address)[0]; + + if (ipv4Address) { + return {ip: ipv4Address, port: this.config.jupyterPort}; + } + } + + // If the state is not available, trigger the monitor job if it doesn't exist + const existingMonitorJob = await this.JobCore.getJob({ + name: `Update Instance Status of User: ${instance.userId}`, + type: enumJobType.LXD_MONITOR, + status: enumJobStatus.PENDING + }); + + if (existingMonitorJob.length === 0) { + // No pending monitor job, create one + const jobName = `Update Instance Status of User: ${instance.userId}`; + const jobType = enumJobType.LXD_MONITOR; + const executorPath = '/lxd/monitor'; + const period = 60 * 1000; // 1 minute + + const metadata = { + operation: enumMonitorType.STATE, + userId: user_id + }; + + await this.JobCore.createJob( + instance.userId, + jobName, + jobType, + undefined, + period, + { id: instance.id, type: 'lxd', path: executorPath }, + null, + null, + 1, + metadata + ); + + Logger.warn(`Monitor job created for instance ${instance_id} to update state.`); + } + // Return null IP for now, as the monitor job will update the instance state + return null; + } + + public async checkQuotaBeforeCreation(userId: string, requestedCpu: number, requestedMemory: string, requestedDisk: string, requestedInstances: number): Promise { + + const {properties: userQuota} = await this.configCore.getConfig(enumConfigType.USERCONFIG, userId, true )as { properties: IUserConfig }; + const instances: IInstance[] = await this.getInstances(userId); + + let currentCpu = 0, currentMemory = 0, currentDisk = 0; + instances.forEach(instance => { + currentCpu += parseInt(instance.config['limits.cpu'] as string); + + currentMemory += this.parseMemory(instance.config['limits.memory'] as string); + currentDisk += this.parseMemory(instance.config['user.disk'] as string); + }); + + if (currentCpu + requestedCpu > userQuota.defaultLXDMaximumInstanceCPUCores) { + throw new CoreError(enumCoreErrors.NO_PERMISSION_ERROR, 'Requested CPU exceeds available quota. Please delete some instances or contact an administrator.'); + } + if (this.parseMemory(requestedMemory) + currentMemory > userQuota.defaultLXDMaximumInstanceMemory) { + throw new CoreError(enumCoreErrors.NO_PERMISSION_ERROR, 'Requested memory exceeds available quota. Please delete some instances or contact an administrator.'); + } + if (this.parseMemory(requestedDisk) + currentDisk > userQuota.defaultLXDMaximumInstanceDiskSize) { + throw new CoreError(enumCoreErrors.NO_PERMISSION_ERROR, 'Requested disk space exceeds available quota. Please delete some instances or contact an administrator.'); + } + if (instances.length + requestedInstances > userQuota.defaultLXDMaximumInstances) { + throw new CoreError(enumCoreErrors.NO_PERMISSION_ERROR, 'Requested instance count exceeds available quota. Please delete some instances or contact an administrator.'); + } + } + + + public async getQuotaAndFlavors(requester: IUser) { + + // key: userId, + const {properties: userQuota} = await this.configCore.getConfig(enumConfigType.USERCONFIG, requester.id, true ); + const { properties: systemConfig } = await this.configCore.getConfig(enumConfigType.SYSTEMCONFIG, null, true); + + const isAdmin = requester.type === enumUserTypes.ADMIN; + // const userFlavors = isAdmin ? (systemConfig as ISystemConfig).defaultLXDFlavor.keys(): (userQuota as IUserConfig).defaultLXDflavor; + const userFlavors = isAdmin + ? Object.keys((systemConfig as ISystemConfig).defaultLXDFlavor) + : (userQuota as IUserConfig).defaultLXDflavor; + + + // Transform flavors into an object with flavor names as keys + const transformedFlavors: Record = {}; + userFlavors.forEach(flavor => { + const flavorDetails = (systemConfig as ISystemConfig).defaultLXDFlavor[flavor]; + if (!flavorDetails) { + Logger.warn(`Flavor ${flavor} is not defined in systemConfig.defaultLXDFlavor.`); + return; // Skip this flavor if it's not defined + } + + transformedFlavors[flavor] = { + ...flavorDetails, + memoryLimit: this.formatMemory(flavorDetails.memoryLimit), + diskLimit: this.formatMemory(flavorDetails.diskLimit) + }; + }); + + return { + userQuota: userQuota as IUserConfig, + userFlavors: transformedFlavors + }; + } + + + +} \ No newline at end of file diff --git a/packages/itmat-cores/src/coreFunc/jobCore.ts b/packages/itmat-cores/src/coreFunc/jobCore.ts index cd0eb2d36..97e482ffb 100644 --- a/packages/itmat-cores/src/coreFunc/jobCore.ts +++ b/packages/itmat-cores/src/coreFunc/jobCore.ts @@ -1,11 +1,7 @@ -import { CoreError, IJobEntry, enumCoreErrors } from '@itmat-broker/itmat-types'; +import { CoreError, IJob, enumCoreErrors, enumJobType, enumJobStatus, IExecutor} from '@itmat-broker/itmat-types'; +import { v4 as uuid } from 'uuid'; import { DBType } from '../database/database'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -enum JOB_TYPE { - QUERY_EXECUTION = 'QUERY_EXECUTION', - DATA_EXPORT = 'DATA_EXPORT' -} export class JobCore { db: DBType; @@ -13,10 +9,135 @@ export class JobCore { this.db = db; } - public async createJob(): Promise { - throw new CoreError( - enumCoreErrors.NOT_IMPLEMENTED, - enumCoreErrors.NOT_IMPLEMENTED + /** + * Create a job. + * + * @param requester - The ID of the requester creating the job. + * @param name - The name of the job. + * @param type - The type of the job (enumJobType). + * @param nextExecutionTime - When the job should start. Null for immediate execution. + * @param period - The period for recurring jobs. + * @param executor - The executor responsible for running the job. + * @param data - Any input data required for the job. + * @param parameters - Additional parameters for the job. + * @param priority - The job's priority level. + * @param metadata - Additional metadata for the job. + * + * @return IJobEntry + */ + public async createJob( + requester: string, + name: string, + type: enumJobType, + nextExecutionTime?: number, + period?: number, + executor?: IExecutor, + data?: JSON | null | undefined, + parameters?: JSON | null | undefined, + priority?: number, + metadata?: Record + ): Promise { + const jobEntry: IJob = { + id: uuid(), + name: name, + nextExecutionTime: nextExecutionTime ?? Date.now(), + period: period ?? null, + type: type, + executor: executor ?? null, + data: data ?? null, + parameters: parameters ?? null, + priority: priority ?? 0, + status: enumJobStatus.PENDING, + history: [], + counter: 0, + life: { + createdTime: Date.now(), + createdUser: requester, + deletedTime: null, + deletedUser: null + }, + metadata: metadata ?? {} + }; + + await this.db.collections.jobs_collection.insertOne(jobEntry); + return jobEntry; + } + + /** + * Retrieve jobs based on optional filters. + * + * @param filter - Partial filter to query jobs (e.g., by name, type, status). + * + * @return IJobEntry[] + */ + public async getJobs(filter: Partial = {}) { + return await this.db.collections.jobs_collection.find(filter).toArray(); + } + + /** + * Edit an existing job by updating its priority, next execution time, or period. + * + * @param requester - The ID of the user requesting the edit. + * @param jobId - The ID of the job to edit. + * @param priority - New priority value (optional). + * @param nextExecutionTime - New next execution time (optional). + * @param period - New period for recurring jobs (optional). + * + * @return IJobEntry + */ + public async editJob( + requester: string, + jobId: string, + priority?: number | null, + nextExecutionTime?: number | null, + period?: number | null + ): Promise { + const job = await this.db.collections.jobs_collection.findOne({ id: jobId }); + + if (!job) { + throw new CoreError( + enumCoreErrors.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY, + 'Job does not exist.' + ); + } + + const setObj: Partial = {}; + if (priority !== undefined && priority !== null) { + setObj.priority = priority; + } + if (nextExecutionTime !== undefined && nextExecutionTime !== null) { + setObj.nextExecutionTime = nextExecutionTime; + setObj.status = enumJobStatus.PENDING; + } + if (period !== undefined) { + setObj.period = period; + } + + const result = await this.db.collections.jobs_collection.findOneAndUpdate( + { id: jobId }, + { $set: setObj }, + { returnDocument: 'after' } ); + + if (!result) { + throw new CoreError( + enumCoreErrors.DATABASE_ERROR, + 'Failed to update the job.' + ); + } + + return result; } + + /** + * Get a job based on specific filters, like job ID, name, type, status, etc. + * + * @param filter - The filter for querying jobs. + * + * @return IJobEntry[] + */ + public async getJob(filter: Partial) { + return await this.db.collections.jobs_collection.find(filter).toArray(); + } + } diff --git a/packages/itmat-cores/src/coreFunc/userCore.ts b/packages/itmat-cores/src/coreFunc/userCore.ts index c2f5aba7a..e7b809ee1 100644 --- a/packages/itmat-cores/src/coreFunc/userCore.ts +++ b/packages/itmat-cores/src/coreFunc/userCore.ts @@ -12,6 +12,7 @@ import tmp from 'tmp'; import { UpdateFilter } from 'mongodb'; import * as pubkeycrypto from '../utils/pubkeycrypto'; import crypto from 'crypto'; +import fs from 'fs'; export class UserCore { db: DBType; @@ -20,6 +21,7 @@ export class UserCore { objStore: ObjectStore; fileCore: FileCore; configCore: ConfigCore; + systemSecret: { publickey: string, privatekey: string }; constructor(db: DBType, mailer: Mailer, config: IConfiguration, objStore: ObjectStore) { this.db = db; this.mailer = mailer; @@ -27,6 +29,27 @@ export class UserCore { this.objStore = objStore; this.fileCore = new FileCore(db, objStore); this.configCore = new ConfigCore(db); + + // System secret key pairs + // Load the system secret key pairs + this.systemSecret = { + publickey: this.loadKey(config.systemKey['pubkey']), + privatekey: this.loadKey(config.systemKey['privkey']) + }; + } + /** + * Load key from text content or file path. + * @param keyPathOrContent - Either the key content or a file path to the key. + * @returns The key as a string. + */ + private loadKey(keyPathOrContent: string): string { + if (keyPathOrContent.includes('-----BEGIN')) { + // Key is provided as direct content + return keyPathOrContent; + } else { + // Key is provided as a file path, read from file + return fs.readFileSync(keyPathOrContent, 'utf8'); + } } /** * Get a user. One of the parameters should not be null, we will find users by the following order: usreId, username, email. @@ -882,6 +905,26 @@ export class UserCore { return accessToken; } + //TODO: Adapt to the new token generation function, like using the public key from the user + public async issueSystemAccessToken(userId: string, life?: number) { + // payload of the JWT for storing user information + const payload = { + publicKey: this.systemSecret.publickey, + userId: userId, // encode the UserId into the token + Issuer: 'IDEA-FAST DMP SYSTEM', + timestamp: Date.now() + }; + // set the life time to 1 week if not specified + life = life || 7 * 24 * 60 * 60 * 1000; + + const accessToken = { + // set the token not to be expired by transfer the life time to 1 year + accessToken: tokengen(payload, this.systemSecret.privatekey, undefined,undefined, life) + }; + + return accessToken; + } + /** * Delete a pubkey. * diff --git a/packages/itmat-cores/src/database/database.ts b/packages/itmat-cores/src/database/database.ts index 6764e46f3..27f7f08ae 100644 --- a/packages/itmat-cores/src/database/database.ts +++ b/packages/itmat-cores/src/database/database.ts @@ -1,4 +1,4 @@ -import type { IField, IFile, IJobEntry, ILog, IOrganisation, IProject, IPubkey, IQueryEntry, IRole, IStudy, IUser, IStandardization, IConfig, IData, IDrive, ICache, IDomain, IOntologyTree, IBase, IWebAuthn } from '@itmat-broker/itmat-types'; +import type { IField, IFile, IJob, ILog, IOrganisation, IProject, IPubkey, IQueryEntry, IRole, IStudy, IUser, IStandardization, IConfig, IData, IDrive, ICache, IDomain, IOntologyTree, IBase, IWebAuthn, IInstance} from '@itmat-broker/itmat-types'; import { Database as DatabaseBase, IDatabaseBaseConfig } from '@itmat-broker/itmat-commons'; import type { Collection } from 'mongodb'; @@ -24,13 +24,14 @@ export interface IDatabaseConfig extends IDatabaseBaseConfig { cache_collection: string, domains_collection: string, doc_collection: string, - webauthn_collection: string + webauthn_collection: string, + instance_collection: string }; } export interface IDatabaseCollectionConfig { users_collection: Collection, - jobs_collection: Collection, + jobs_collection: Collection, studies_collection: Collection, projects_collection: Collection, queries_collection: Collection, @@ -51,5 +52,6 @@ export interface IDatabaseCollectionConfig { // TODO: Implemet doc feature docs_collection: Collection webauthn_collection: Collection + instance_collection: Collection } export type DBType = DatabaseBase; diff --git a/packages/itmat-cores/src/index.ts b/packages/itmat-cores/src/index.ts index 9df31b0df..56baa8318 100644 --- a/packages/itmat-cores/src/index.ts +++ b/packages/itmat-cores/src/index.ts @@ -24,3 +24,6 @@ export * from './utils'; export * from './webdav/dmpWebDAV'; // webauthn export * from './coreFunc/webauthnCore'; +// instance, lxd, Analystic Environment +export * from './coreFunc/instanceCore'; +export * from './lxd/lxdManager'; \ No newline at end of file diff --git a/packages/itmat-cores/src/lxd/lxd.util.ts b/packages/itmat-cores/src/lxd/lxd.util.ts new file mode 100644 index 000000000..a6ecdffc8 --- /dev/null +++ b/packages/itmat-cores/src/lxd/lxd.util.ts @@ -0,0 +1,35 @@ +import {LxdConfiguration} from '@itmat-broker/itmat-types'; + +// sanitized the update playload +export const sanitizeUpdatePayload = (payload: LxdConfiguration) => { + const sanitizedPayload: LxdConfiguration = { ...payload }; + + // Check if config property exists + if (sanitizedPayload.config) { + // Check and format CPU limit + if (typeof sanitizedPayload.config['limits.cpu'] !== 'string') { + sanitizedPayload.config['limits.cpu'] = '2'; // Default to '2' if not a string + } else { + sanitizedPayload.config['limits.cpu'] = sanitizeCpuLimit(sanitizedPayload.config['limits.cpu']); + } + + // Check and format memory limit + if (typeof sanitizedPayload.config['limits.memory'] !== 'string') { + sanitizedPayload.config['limits.memory'] = '16GB'; // Default to '16GB' if not a string + } else { + sanitizedPayload.config['limits.memory'] = sanitizeMemoryLimit(sanitizedPayload.config['limits.memory']); + } + } + + return sanitizedPayload; +}; + +// Sanitize CPU limit +const sanitizeCpuLimit = (cpuLimit: string): string => { + return cpuLimit ? cpuLimit.toString() : '2'; // Default to '2' if not provided +}; + +// Sanitize memory limit +const sanitizeMemoryLimit = (memoryLimit: string): string => { + return memoryLimit ? memoryLimit.toString() : '16GB'; // Default to '16GB' if not provided +}; \ No newline at end of file diff --git a/packages/itmat-cores/src/lxd/lxdManager.ts b/packages/itmat-cores/src/lxd/lxdManager.ts new file mode 100644 index 000000000..98f04d997 --- /dev/null +++ b/packages/itmat-cores/src/lxd/lxdManager.ts @@ -0,0 +1,363 @@ +import * as https from 'https'; +import * as fs from 'fs'; +import { WebSocket } from 'ws'; +import axios, { AxiosInstance, isAxiosError } from 'axios'; +import { LXDInstanceState, LxdConfiguration, Cpu, Memory, Storage, Gpu , LxdOperation, LxdGetInstanceConsoleResponse} from '@itmat-broker/itmat-types'; +import { Logger } from '@itmat-broker/itmat-commons'; +import { sanitizeUpdatePayload } from './lxd.util'; +import { IConfiguration } from '../utils/configManager'; + +export class LxdManager { + private lxdInstance: AxiosInstance; + private lxdAgent: https.Agent; + config: IConfiguration; + + constructor(config: IConfiguration) { + this.config = config; + let lxdSslCert: string; + let lxdSslKey: string; + + // Load the SSL certificate and key + // Determine if cert and key are file paths or direct content + if (config.lxdCertFile['cert'].includes('-----BEGIN')) { + lxdSslCert = config.lxdCertFile['cert']; + } else { + lxdSslCert = fs.readFileSync(config.lxdCertFile['cert'], 'utf8'); + } + + if (config.lxdCertFile['key'].includes('-----BEGIN')) { + lxdSslKey = config.lxdCertFile['key']; + } else { + lxdSslKey = fs.readFileSync(config.lxdCertFile['key'], 'utf8'); + } + + const lxdOptions: https.AgentOptions & Pick = { + cert: lxdSslCert, + key: lxdSslKey, + rejectUnauthorized: config.lxdRejectUnauthorized + }; + + this.lxdAgent = lxdOptions.agent = new https.Agent(lxdOptions); + this.lxdInstance = axios.create({ + baseURL: config.lxdEndpoint, + httpsAgent: this.lxdAgent + }); + } + + // get resources of lxd server. + async getResources() { + try { + const project = this.config.lxdProject || 'default'; + const response = await this.lxdInstance.get(`/1.0/resources?project=${project}`); + const data = response.data.metadata; + + return { + data: { + cpu: data.cpu as Cpu, + memory: data.memory as Memory, + storage: data.storage as Storage, + gpu: data.gpu as Gpu + } + }; + + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + Logger.error('getResources axios error' + error.message); + return { + error: true, + data: error.message + }; + } + Logger.error('getResources unknown error 1' + error); + return { + error: true, + data: String(error) + }; + } + } + + // getProfile + async getProfile(profileName: string, project: string) { + try { + const response = await this.lxdInstance.get(`/1.0/profiles/${encodeURIComponent(profileName)}?project=${project}`); + if (response.status === 200) { + return { + data: response.data.metadata // assuming this is the format in which LXD returns profile data + }; + } else { + Logger.error(`Failed to fetch profile data. ${response.data}`); + return { + error: true, + data: `Failed to fetch profile data. ${response.data}` + }; + } + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + Logger.error(`Failed to fetch profile data. ${error.response}`); + return { + error: true, + data: `Failed to fetch profile data. ${error.response}` + }; + } else { + Logger.error('Error fetching profile data from LXD:' + error); + return { + error: true, + data: String(error) + }; + } + } + } + + // This should almost never be run, only for admin user + async getInstances() { + try { + const project = this.config.lxdProject || 'default'; + const instanceUrls = await this.lxdInstance.get(`/1.0/instances?project=${project}`); + const instances = await Promise.allSettled(instanceUrls.data.metadata.map(async (instanceUrl: string) => await this.lxdInstance.get(instanceUrl))); + + const sanitizedInstances = instances.map((instance) => { + if (instance.status === 'fulfilled') { + const { metadata } = instance.value.data; + return { + name: metadata.name, + description: metadata.description, + status: metadata.status, + statusCode: metadata.status_code, + profiles: metadata.profiles, + type: metadata.type, + architecture: metadata.architecture, + creationDate: metadata.created_at, + lastUsedDate: metadata.last_used_at, + username: metadata.config['user.username'] || 'N/A', + cpuLimit: metadata.config['limits.cpu'] || 'N/A', + memoryLimit: metadata.config['limits.memory'] || 'N/A' + }; + } else { + Logger.error('Error fetching instance data: ' + instance.reason); + return null; + } + }).filter(instance => instance !== null); + + return { + data: sanitizedInstances + }; + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + Logger.error('getInstances axios error' + error.message); + return { + error: true, + data: error.message + }; + } + Logger.error('getInstances unknown error 1' + error); + return { + error: true, + data: String(error) + }; + } + } + + async getInstanceInfo(instanceName: string, project: string) { + instanceName = encodeURIComponent(instanceName); + return await this.lxdInstance.get(`/1.0/instances/${instanceName}?project=${project}`); + } + + async getInstanceState(instanceName: string, project: string) { + instanceName = encodeURIComponent(instanceName); + try { + const response = await this.lxdInstance.get(`/1.0/instances/${instanceName}/state?project=${project}`); + const instanceState: LXDInstanceState = response.data.metadata; + return { + data: instanceState + }; + } catch (error: unknown) { + Logger.error('Error fetching Instance state from LXD:' + error); + if (axios.isAxiosError(error)) { + Logger.error(`Error fetching Instance state from LXD: ${error.message}`); + if (error.response) { + Logger.error(`Status: ${error.response.status}`); + Logger.error(`Headers: ${JSON.stringify(error.response.headers)}`); + Logger.error(`Data: ${JSON.stringify(error.response.data)}`); + } + } else { + Logger.error(`Error fetching Instance state from LXD: ${String(error)}`); + } + return { + error: true, + data: String(error) + }; + } + } + + async getInstanceConsole(instanceName: string, options: { height: number; width: number; type: string; }): Promise { + try { + instanceName = encodeURIComponent(instanceName); + const project = this.config.lxdProject || 'default'; + const consoleInfo = await this.lxdInstance.post(`/1.0/instances/${instanceName}/console?project=${project}&wait=10`, options); + return { + operationId: consoleInfo.data.metadata.id, + operationSecrets: consoleInfo.data.metadata.metadata.fds + }; + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + Logger.error(`getInstanceConsole unknown error : ${error.message}`); + return { + error: true, + data: error.message + }; + } + Logger.error(`getInstanceConsole unknown error : ${error}`); + return { + error: true, + data: String(error) + }; + } + } + + async getInstanceConsoleLog(instanceName: string) { + instanceName = encodeURIComponent(instanceName); + const project = this.config.lxdProject || 'default'; + try { + const response = await this.lxdInstance.get(`/1.0/instances/${instanceName}/console?project=${project}`); + if (response.status === 200) { + return response.data; + } else { + const errorMessage = `Failed to fetch Logger log data. Status: ${response.status}, Data: ${JSON.stringify(response.data)}`; + Logger.error(errorMessage); + return { + error: true, + data: errorMessage + }; + } + } catch (error: unknown) { + let errorMessage = 'Error fetching instance Logger log from LXD:'; + if (axios.isAxiosError(error)) { + if (error.response) { + if (error.response.status === 404) { + errorMessage = `Logger log file not found. Status: ${error.response.status}, Data: ${JSON.stringify(error.response.data)}`; + Logger.error(errorMessage); + // Return an empty string to the frontend + return { + error: false, + data: '' + }; + } else { + errorMessage = `Failed to fetch Logger log data. Status: ${error.response.status}, Data: ${JSON.stringify(error.response.data)}`; + } + } else { + errorMessage = `Axios error without response: ${error.message}`; + } + } else { + errorMessage = `Unknown error: ${String(error)}`; + } + Logger.error(errorMessage); + return { + error: true, + data: errorMessage + }; + } + } + + async getOperations() { + try { + const project = this.config.lxdProject || 'default'; + // add project to the url + const operationUrls = await this.lxdInstance.get(`/1.0/operations?project=${project}`); + return { + data: operationUrls.data.metadata + }; + } catch (e) { + if (isAxiosError(e)) { + Logger.error('getOperations axios error' + e.message); + return { + error: true, + data: e.message + }; + } + Logger.error('getOperations unknown error' + e); + return { + error: true, + data: e + }; + } + } + + async getOperationStatus(operationUrl: string) { + try { + const opResponse = await this.lxdInstance.get(operationUrl); + return opResponse.data; + } catch (error) { + Logger.error('Error fetching operation status from LXD:' + error); + throw error; + } + } + + getOperationSocket(operationId: string, operationSecret: string) { + operationId = encodeURIComponent(operationId); + operationSecret = encodeURIComponent(operationSecret); + const containerConsoleSocket = new WebSocket(`wss://${this.config.lxdEndpoint.replace('https://', '')}/1.0/operations/${operationId}/websocket?secret=${operationSecret}`, { + agent: this.lxdAgent + }); + containerConsoleSocket.binaryType = 'arraybuffer'; + return containerConsoleSocket; + } + + async createInstance(payload: LxdConfiguration, project: string) { + // console.log('Creating instance with payload:', payload); + // console.log('Project:', project); + try { + const lxdResponse = await this.lxdInstance.post(`/1.0/instances?project=${encodeURIComponent(project)}`, payload, { + headers: { 'Content-Type': 'application/json' }, + httpsAgent: this.lxdAgent + }); + // console.log(lxdResponse.data); + return lxdResponse.data; + } catch (error) { + if (isAxiosError(error)) { + console.error('Error response:', error.response?.data); + } + + Logger.error(`Error creating instance on LXD: ${error}`); + throw new Error('Error creating instance on LXD'); + } + } + + async updateInstance(instanceName: string, payload: LxdConfiguration, project: string) { + try { + const sanitizedPayload = sanitizeUpdatePayload(payload); + // Perform the PATCH request to LXD to update the instance configuration + const response = await this.lxdInstance.patch(`/1.0/instances/${encodeURIComponent(instanceName)}?project=${project}`, sanitizedPayload, { + headers: { 'Content-Type': 'application/json' } + }); + return response.data; // Return the response or format as needed + } catch (error) { + Logger.error('Error updating instance on LXD:' + error); + throw error; + } + } + + async startStopInstance(instanceName: string, action: string, project: string) { + try { + const response = await this.lxdInstance.put(`/1.0/instances/${instanceName}/state?project=${project}`, { + action: action, + timeout: 30, // adjust as needed + force: false, + stateful: false + }); + return response.data; + } catch (error) { + Logger.error(`Error ${action} instance on LXD: ${error}`); + throw error; // Propagate the error + } + } + + async deleteInstance(instanceName: string, project: string) { + try { + const response = await this.lxdInstance.delete(`/1.0/instances/${instanceName}?project=${project}`); + return response.data; + } catch (error) { + Logger.error(`Error deleting instance on LXD: ${error}`); + throw error; // Propagate the error + } + } +} \ No newline at end of file diff --git a/packages/itmat-cores/src/utils/configManager.ts b/packages/itmat-cores/src/utils/configManager.ts index f1333e173..b636b0f47 100644 --- a/packages/itmat-cores/src/utils/configManager.ts +++ b/packages/itmat-cores/src/utils/configManager.ts @@ -18,6 +18,14 @@ export interface IConfiguration extends IServerConfig { aeEndpoint: string; useWebdav: boolean; webdavPort: number; + webdavServer: string; + lxdEndpoint: string; + lxdStoragePool: string; + lxdProject: string; + systemKey: Record; + lxdCertFile: Record; + lxdRejectUnauthorized: boolean; + jupyterPort: number; } export class ConfigurationManager { diff --git a/packages/itmat-interface/config/config.sample.json b/packages/itmat-interface/config/config.sample.json index f07ef5c04..61c0bca62 100644 --- a/packages/itmat-interface/config/config.sample.json +++ b/packages/itmat-interface/config/config.sample.json @@ -23,7 +23,8 @@ "drives_collection": "DRIVE_COLLECTION", "colddata_collection": "COLDDATA_COLLECTION", "domains_collection": "DOMAIN_COLLECTION", - "webauthn_collection": "WEBAUTHN_COLLECTION" + "webauthn_collection": "WEBAUTHN_COLLECTION", + "instance_collection": "INSTANCE_COLLECTION" } }, "server": { @@ -51,5 +52,17 @@ "adminEmail": "admin@example.com", "aeEndpoint": "http://localhost:9090", "useWebdav": true, - "webdavPort": 1900 + "webdavPort": 1900, + + "lxdEndpoint": "https://localhost:8443", + "systemKey":{ + "pubkey": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnKXlNUDuKSAAClMe2cXf\n7maUnwcS2qsPYuG/gjZn8/Y+ICguPia5c+xlcQgHFYP7bBLeohsPbnp5bG0YTgeg\nGlLPoy+z/rZKBdKTXnv3fAy6ko9SnjpxfC/+kIkNfMcQe7YiwyGsvhVRYUErn5zZ\nQdxjot+DHXrrOsX00fu/E7mU0VqeISouRKkeYSzx4kJVYxGE89hrya8RyOmnyAay\nP5rhPicde4Oiag4J4K+t6eyNS3cel4JiKct8p+fPs+ryw/yzhYOffpOhkAJZvVcR\n/FRB1dUio2pn6BrqYZGWEJllZbOEbhKep8VKCQgqhI74Ab9Bl/hZU6n6+FFJbJfe\nFUaFqdkIMrEy5lKA7K50prhvkjokvf3XeDl+2WnfndIGVjcDMag1KXoQqdqdEScq\nT9DCQuib+JIdq4P81krtqxMOKg4c5cPhzTydACluAr6FLNjStCIMcI0uDHlmfIt/\nfd3/VdYoCxl0paZlA6Ku406HvpBk9oGz7i7eLQ2G6Wby3OP2EPA3JNW6zqe+AnfJ\nszLc39MrBeR4BMmj4++9QK5EsF+hg9KVVLwbaVs0HxlRqvnCN99M0EVPZL1DhnmQ\nQCn5O5vVAvmeDnTb+keI79qA6JIABdoEZSjFB2pWrwC/EQQj2nzL56EPT1oCNvLF\nBp5cqMAVb4vGCe2LAZ8MNxECAwEAAQ==\n-----END PUBLIC KEY-----\n", + "privkey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIJtTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQAxpn0LAybkjSkRMr\nK6XcqgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAKKAQmCpD6ZU0rz\nsu4s6q4EgglQVD13OfDhXnqrJT2v3CHq5rr5bip3P+j6yacgeB2l+s/IlY+0MM2f\n8JtOojUV9RYclV1dAi8krIOv2KVEH81Olb/mse5/Zr7BKBe5g+9jHF6EtWuTWqzX\n2TTkatULeJDnqUd+tBPBmuY3CUEl/6nUepTNzrRoVG4Rp/z2IbtaMx3IIpEZ9Fgt\n3iMkY0O8rtattwJp2k+eIFFf2c63J+NlXzIbuvLqNuPMq/GaUUCLtVsljZdtS/nw\n+tPQJfxW0hfawq1erIMu32Cnm2Tp0ZR2tGFIPMXT+zB/m4TEjbNEwS1abGqZ0y/l\nzJUckj5i5MCSmiF6Mxv9AVAXixlMWbvzhC9isyJKJY+3fde99/h71ZPMaO5NBJW6\nAzguuMc8DbVuGbWKUIkRMyj5Tm63YRasQIzTdrrZ6MR5QA4090RC8n3AsCguVzYB\n7+7fKhDNUIUsvuIXQ95WYpndJF/UbUIGtI8u1DpRZd7U/0lFkl2oX5UPIXgEY1Kv\n8JuWKhH/sYpMhmSWJqBmKV0ZhzDdMRJJtRJpSnLwEO1xchvTtjw9ySjdWZvGLF5z\n7uJwUKCu7aeo18YH4YpW66ladoBWXWN3bfZ5+d3mTMgV6d4z8Opg/8NzC1fsdfzm\n9uqrRFm9+dedCmnQ7w4wfKnDrmeg4oOMgXnbzyxVabYfade2Qk73bxdzUh66pocW\ngis6Q6MSwhHGXIShUKhcFtZAuON9Yedfe6ycSNrYP8Z2aU3Zmg8QcNMJnk3RXtms\nFPTt5SCz/re0ZsckgYD1CfQHB2zrp1PgV0O0wMGAw5FBDxbfEW+rA7qp21UJJBG8\n14YtepW+7sOFYZX164JZQrpCzZ4Qkdx9MeRMc6z670rOh132DpZ/vZLp9JEZt46I\nEXse7fcp3mfl2s4lMQERqYnphQ45ZUheNZge7qrPXKhE26Eqk4O8yE3EPPGzlIrf\nWLzUxCF/k19d+r7NLa6i5uHZ1RnB/kepTB9tdaVMY9CX68kIMKUN8HDNLxKzB4bC\nv6/VLjWt14P0OUnJ/uuZ3TwwXeAmBVJJb99UwKoUp6SKYyH1tShzMKNNNXZwdhhr\nCGf1T+1f1lnqDQX+YcQbmyV9EhPNpOtsSNDawJgN8qSs+fOY4vvpgPsLMmzlFsL3\nG8beIQaJ1Kryip9lJ7bKMZQzy/JTDHnH4+RHn57GcZtZTumFcHkhtexJXhWUxgQB\ncny0myAkvqt9/iF6hRGZYmcP2BWHRg1bXotETcdBk4gvpWf6byt95GA7OK1n+K7J\nrnit9DYG1Mq8K0t0Dju2FpMzOQMy3r+EciiAXwpU9Alio7lWbnYpZwIM5DtCTc07\nwwyFiMdOtku5Hg54VXSMAYJpY1+37bNtSlKkT/G24j7rT9Vx1DMeZwLY97bBZ9OE\napbmRWLUHi+jKdjkxId4+Bx46WIqYyawdKP7N6W+TCd+W7ChBm0/3L6E+tQYyt+z\nL5xfyNVZ3Cr3zzqHgrN33LFWAtEhVUzIxUOzXlWA7QdTSQsOYaERH+An01kRdqTA\nwg+pBNtd/4DskZdKMikGNsvOGhfx1b51S2zD/Jv7j/SvaWIiXkDtB7f/TBkYBBMB\nc3otTTtd8F6GnmuBQnRRK84pk3jhJmW5vEle6ok1tr/mWGU658ToreCWXZZ+FnLe\nmzY9z7qPFZu0P5tM4dxDrASwkxpC7ed3UDbzdL23TTL6naSW7SpGqOhgZ+sUrUAg\nLl7s5BrILXgqx+ymLFtaFW5Pk1h2bTJPQgBNP7VaUx6a5GI048uGS1loGKMzDjGl\n6mNVIAAaI5W7cJel/jph8JJivGav1rZGr5TGmMJtTkE4PBRYdhOfpIKeHY/Ylu3R\n43cKvN3ci3jvzdTkW577Msd2TFk150Mn0hP47stpNWqb0WWieNGjeiN4Wj270iqT\n+ifAkq1hBkU4Ytt1MQOwbpKOmrl51SZO8zUhVIgGPk3UaUtrm/T9usVAi7oyLdmO\ntKLNFPJp9CIotwwQybbHlMWQ862SgT6LOJkLsrPwxfRz3XajnBKH2ABMXRCHE5ZO\nqvTuZoxa4azIp+EATqroPkjzJn+VQRC//7X4kaL44Ijga8JzFedgyvIVyGdMMmAu\nyxHjY1xEryhUEefWA+FnSB47mJSxRKS9h1upsaR2k8toyMRHDKTVgQViyGq5O6Sj\n7Tl0fYnUM6hqRI+M9A54CFwouoeIQybbAC6cFs4ka087HKa53fe8ZfaAUmWX0iuW\nnrhwIe2Z2o48pxNWENduWWHsnBnPZnJwZtOoLRU4QHwYfBGxzBFfXPbhVb4XY9/9\npQICA+nBCZSNylnIiw7CFfFvfvgMYls0xcj+0jakSsyoVj01ojw7crcOGTULQs8Q\nbvjnkL+5Vk3BLdiKS9EGzn/4STJYbQOJ2VzgvAwTxYccrlOjiLAfj9pOZW56MV0F\n4vyxrWXqKGvKP7xXFaw2C3Aud4fII5gVGn3SDVg8em/jlOch+78fGVnprRALbOEv\nEh5NoG6m06hzvf1/90kKZd7ADzlFG7rLXhU2yazt+Om6Jnb/+Yf+iElTaBRv3BcF\nBHgeCZqMhJYtI6bkUJ7OpX3TkOP+s8qRI6BPUaIQPTdo5VQZwHS2OiligWSxnY4r\nXeCKcz6mT65vLcc4zkNHiv1r6nQmZb1wW2JFqWPOXzt+zK4ABJMv39b9JHney9Lm\nBhajnBmlYLbQCrF8u/W/hrkcNwUeezF79pThX6M4OSLs0pfGIaJytDOjYI8YpAmf\n8RZJJbdjCg5IqQqRBAjnt4hHVgvVIaFyb55OVa0VixbHXTs+zuZds0lyTiPNam3Q\n1JEJRy9JM//nxCwO4Lvpgfz1xNija3zVHISvlTWiiBrq8XgeTI4ZYKfw9CKMIUle\n49w9HGCYwfJltrmfjkAODsTGTABeVuKYTx2YgCUdUemlfdTVOTgjouhOjWQZELJs\nmmiXksmId/NP8B+jBnDs/ycvSCcqp4c+TAgeWnKF7ot6y/JaO2jLNVV4bjajfYY8\nd2OnZyJRWa56f+szPM+1kAaiJm2q6VQz29W2KD//Ng7noFC11RfzvwVBzKHsNojW\nbIfQQwIYwqz1RTmLeY23aaKiHXfIH6Av3qfpUxRlzMcAt7QoweY52dFd+TFjG99+\n615Zg5U22Wra3541u9Cj2P7RRhLH+gBqtfRmsl/6KZVFZfr1pnrxgQA=\n-----END ENCRYPTED PRIVATE KEY-----\n" + }, + "lxdCertFile": { + "cert": "-----BEGIN CERTIFICATE-----\nMIIFETCCAvmgAwIBAgIUQQ5zBtfZBlNWzwy9HJB4VV2BgTswDQYJKoZIhvcNAQEL\nBQAwGDEWMBQGA1UEAwwNbXktbHhkLWNsaWVudDAeFw0yMzEyMjAxNDQyMzlaFw0y\nNDEyMTkxNDQyMzlaMBgxFjAUBgNVBAMMDW15LWx4ZC1jbGllbnQwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCbwsuJRGSa5UbtYdjqQD0fo9pBwHSgHB14\n0zezRxlzgI8272y0/1Zwie4O1DxRPu/85zP/q2IyB9pr8RP4744a+ICPD3pdM56Y\ntHP34Pd6OxWemN+YRfst9VRHQ54hsz0lWD8qeNuuRL5MPgaaomNNC4LBJKRTC6Iy\nSmj1Mc7hbiTW7IVqwmFCmNtlW1iOY6JpJfhL+Zi13g0SBJwkoO7RoPKv9B3jee4x\nU6P7mqtUSOYvmO3i5hfI+cTPpnEBrlCZ2/5DzL8l4e78r6qCCwzhnSYYVcAgLIYY\nlcL47qjx5f8czrT/gZrOC010K0snsRV1sQ4ovsfKhc9qhZNAILIxfPoObsmIpRdD\n3b6JsfJlHVrWgiLiqwFMC/Fa+caKfPt+qhBTelPvwggST7y0/0k5hUrgX69NZe09\n2GH0Dr7I6IOyls9NDxMz7ZgOoY3bHsb47U6yTV57qjWwkbv2EkY/Nseqf5G0ZLE8\nyT5VgLhii+/zZb3Ch+T6dFjwNHfEWPmUY+9L9AHOYvU713NoH9gScGtWQtVGs3l3\nQ8z2pbAnQiRkYfRi2/4gQ5jMKnATztBx09tXx0vzspGWHB7j+9nq0fFgqmtBDulh\nfamZf/09ge1vbjbR4GrmWysKjrxGK6jezMa8yyK2jFs8+a5OatCV7n+MTRnn1Bib\n9/R/9At0dQIDAQABo1MwUTAdBgNVHQ4EFgQUke2sAkbSJmeXS3t+xlW2/JGGqCIw\nHwYDVR0jBBgwFoAUke2sAkbSJmeXS3t+xlW2/JGGqCIwDwYDVR0TAQH/BAUwAwEB\n/zANBgkqhkiG9w0BAQsFAAOCAgEAgQ7O3gY62ZDeJtLEIlp+orAG1n6LHBh2Z2Lc\nrSW/YbTIXSVF9FbKGBU6MTrxCdBNOEebSXQToYTKgz7KfUa6LnWmuqHMU7UYmVc8\ncOVVc40H5Pyyz1AHdCN4HxpRZgOzy5/95bYdsFN6JRfLqnIG+EW6XjK13arMshRR\n7E9JmLTnikmfdARUdALSHqQd+jRS4MfpU+3kvnaeSz48UwdN8/GxqFtWoRdmqBcV\ngJ8tzLzKM6MaUHum5H14dYISN2BOnAINHPqt0y+Lh7HJtmiBtLpoSbofUtIsAgYn\nDW0uNtL9gQONa5kKo8KHSoX5nmLdSQycj1Bhm84Z7qw83XNTgAjjcAf0eTWU2gjH\nbjX5XqdiQ+d4RmPEJsPoAKNVcvbJBT6ywG5P91GW8kbMCuQ/ieJw/fc0UdQRye+D\nZwbNoKez6Pfu8H8EbTa9sL7n51lu4VIgvNujpl5oK/MP6pt4lQzoqEmWe8V+8TN4\nVv/Ez+UpJqH6KHINruYU3OHO4CCH1Cj+zSdY7mKjeflOM268JD0mcZwN1Tyxsf16\nah7Xfs3kyRapi1+dQnrXphM39HcXudUpDoa6+n+aeMd+ly/Q7ZdpP0nbUrc7M2DK\n9VgTkGup7F20Yn1d/JU/taS2yKG7rwrX3swEQ/KgqMoTFkIES1hfiVE5z2Dj94BE\njnKGDYg=\n-----END CERTIFICATE-----", + "key": "-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCbwsuJRGSa5Ubt\nYdjqQD0fo9pBwHSgHB140zezRxlzgI8272y0/1Zwie4O1DxRPu/85zP/q2IyB9pr\n8RP4744a+ICPD3pdM56YtHP34Pd6OxWemN+YRfst9VRHQ54hsz0lWD8qeNuuRL5M\nPgaaomNNC4LBJKRTC6IySmj1Mc7hbiTW7IVqwmFCmNtlW1iOY6JpJfhL+Zi13g0S\nBJwkoO7RoPKv9B3jee4xU6P7mqtUSOYvmO3i5hfI+cTPpnEBrlCZ2/5DzL8l4e78\nr6qCCwzhnSYYVcAgLIYYlcL47qjx5f8czrT/gZrOC010K0snsRV1sQ4ovsfKhc9q\nhZNAILIxfPoObsmIpRdD3b6JsfJlHVrWgiLiqwFMC/Fa+caKfPt+qhBTelPvwggS\nT7y0/0k5hUrgX69NZe092GH0Dr7I6IOyls9NDxMz7ZgOoY3bHsb47U6yTV57qjWw\nkbv2EkY/Nseqf5G0ZLE8yT5VgLhii+/zZb3Ch+T6dFjwNHfEWPmUY+9L9AHOYvU7\n13NoH9gScGtWQtVGs3l3Q8z2pbAnQiRkYfRi2/4gQ5jMKnATztBx09tXx0vzspGW\nHB7j+9nq0fFgqmtBDulhfamZf/09ge1vbjbR4GrmWysKjrxGK6jezMa8yyK2jFs8\n+a5OatCV7n+MTRnn1Bib9/R/9At0dQIDAQABAoICABQv0SHaK6Zo3qIMqLM7XhtH\n/hUVdJ1SPNOYSZjCNzSL+IOoe2XoeHMfZ9X41am7RsPNJyUUjiZpHtAXL30iIZWx\n7TTH+cDd3B5zDLCXMcZf+p5SYRrNabn8GKR1HqfEIMGX8p/LiBUe3aw97FcwLT8i\npcXoZ3pH1gk/MY54RRVcVAlIIBvE/hvxJEYPlPuR7Xjugy6S4Pgjw305UbnsODy8\n55cRmVS49xDiLoIrpSwdl+Tv+gIKdhIFpOc7QrFyvl7NGeKpmisLtrsoUn3bdsZA\nEAbes4a389fN2Qtayv8xeA3Aux5+KpUCS/3T+pU1/c+srGbFda6esade5nBM4dW/\nidLFEXDRjar3kYBq/4hfg/7MOIOZX9XFa3jg0yhzCHiTDFHmXKKGC+EABOFcOh1E\nb9UUunqb37tNiKthnLM+wLlXQNponmzyoEsooRCyzxjcjyCrdmRogwoswt+hOmdV\nl3dnvxYwo8SHUq9P4HCGk9QGuvpmszGtV3kCxfouT1kT70ZUPyLZclax/HDCDnq2\n4OMBln4pQk/EGsMLhzbQeQNbpOR2C5b4GEwPbjGExHJde3GBneJxQGSXJq+7Nrsx\nELtZgoKFa46dB+vm0NBz7v8JiRAxiRS/a0xvEQMXrYOf2UhhjxnBfVxGfNyxbEA6\nCIoK8pHWeua+OuzJHQFlAoIBAQDW0vKh3nfMxKV/LuJPJ4HgHB9Ld9xD5QQnpuUn\nVi91hBGgcRukWGUhsypBln41ChuI9O3ZrQApuNM7+2on5SLElwef+65G/W2IbHPb\nUKGpgcxyyPv7/qvQtWGV5U+OIdmxXlNQrruc6Neq8XhlEyHwPDpwQBKfbB8gzfOz\n3jQ6BOFXR+t3Nx+lwErXPRxWTN+7ccb6rOkCF4m2LJwdf7tjAvhHjtuUMASjaIt4\na/QXERuOSvUbp5LcZA9F/YgKEs+yV0AtwyjTnWfzl14rH8oh0AccDNDNX//z+wXv\nYwohdnQyX2mwfxJ3IMSBkAXlikc89PLgxapx8RjSsBcnOH4HAoIBAQC5nbiAjAHg\nJ5AYj8t/sDZnhTvsiLsi4Vo/2/Unrx599a9ChQSTv6gVMVweKtWI43TK7Ez8tsfQ\ne9psN6c9XCE6mc9S6KsQhEgbSict6hP/ayPeb+TWHpUju5qCw6PuXWy+rwZKbgza\nFtMTp4QbgJcCY4WF6YOxNRSTjlTQw0nOMc/VsqGdVDqkVeZ+A0bMlz2tXIXvJiIV\nUBgjgdt+JQnGZplS12ooL8V+rLoHDcUwKH3oeh9ON1HNe3vb7dAlyR+HsC1Mm4Pn\nfB4Xc86vMYX/5OVLwuSdN3VpsBPnDGmkH8+U5pwB+FhiIeS0947PDJwVsaiZXrwp\nRxzLvbPl5ZqjAoIBAQCd887Y+9VEJ1a0NAnMP3U8DhFokQHQngQ3D3ywNquAkZHQ\nUToM1b3OUIkCXp//aaYjRkvYYF6dTrtqAArmuJCe0ZmWpRxYMCCoTW3GVPv4wWpM\n/8BfYbp9I9BTwZ6EGBmTU5KY4VErJvzkQNXQI4gxtmcVf9bxhzNAEI5es0PdYRc6\n8LOOHWbUnZWputIqFi3vCdJPIHHWyu3Dl/tVqURjoZxiKQUEaWYPrF/YNC/uAfMr\n5athIQ5Xo+6i/K5ZEcnLDGIxA6zyI2t6bNKdjKs3v1hq5HVmfG6auvh7MmwRfKIl\nI4h3cIdoNhymUvoy80A77rLiWBRh4O7qgvUTLnNjAoIBAAiDgXjz8woTBnr57X2X\n2Yb6B3ub8elxqLARKLd/QsjIQhes/j7ApbcDIpSHpm+27x53pDhbMeMQKz6XduZL\nmYKUl3vYDDCfwKbvychDWlN22JhVTYu8r16KNlYVHynJwzkj0ggL8C74qQnXvyl7\nxnFnmzI/ObkhFCaIer9wlawNgNjubpdGy8HJ5t6Uy+SKc1vGSKZle16648CNLkIk\n9MPS5Ol10/qv5kEfLxEvwoGo+c11/IWb5/ai2VWHHOr+xKF2pT1ETNKLUN4Gg85p\nWRoZp6LH97B2YL5OQztvyFCs3NqZkUJN38/wegsK59P7YhVkprUSMVM7XcjClMPQ\nuj0CggEBAJVS6a3cxR5b05tVUrRw31MSkdj21JRLf3DUtkPY711+vJBZXCEjZU6c\nj2D4wMMFNqD4tc3D2C+ro6L/iyHtyUl2LC9h+Pfa/7UaFsqZI2XeyPUiyzkoWYZJ\n7KNuUefNxbhnJFM3kwW9Aga03SPhYiS7OiLzQzhSaiFd6WX4GsmNrKUUt12rp0NE\ndoSLqqmhUazwC97t8X/gUC7x54zM4WY4665ljQ8tP18/0pSPw0R+gutclygeK8Lz\nou9j96INWlOgMdFZXbvf5rXe79hFA4fOU64LdxSIXOD530wNOB0hZTuDTJWbho6P\na+jChGwRN78gwuxmniJraA5Hl6aaSyg=\n-----END PRIVATE KEY-----" + }, + "lxdRejectUnauthorized": false, + "jupyterPort": 8888 } \ No newline at end of file diff --git a/packages/itmat-interface/src/index.ts b/packages/itmat-interface/src/index.ts index 6b09651bd..c845d8686 100644 --- a/packages/itmat-interface/src/index.ts +++ b/packages/itmat-interface/src/index.ts @@ -42,9 +42,9 @@ function serverStart() { // notice users of expiration await emailNotification(); - const interfaceRouterProxy = itmatRouter.getProxy(); - if (interfaceRouterProxy?.upgrade) - interfaceServer.on('upgrade', interfaceRouterProxy?.upgrade); + // const interfaceRouterProxy = itmatRouter.getProxy(); + // if (interfaceRouterProxy?.upgrade) + // interfaceServer.on('upgrade', interfaceRouterProxy?.upgrade); }).catch((error) => { console.error('An error occurred while starting the ITMAT core.', error); diff --git a/packages/itmat-interface/src/lxd/index.ts b/packages/itmat-interface/src/lxd/index.ts new file mode 100644 index 000000000..efca9a242 --- /dev/null +++ b/packages/itmat-interface/src/lxd/index.ts @@ -0,0 +1,451 @@ + +// import * as http from 'http'; +import httpProxy from 'http-proxy'; +import { WebSocketServer, WebSocket, RawData} from 'ws'; +import { NextFunction, Request, Response} from 'express'; +import http from 'node:http'; +import * as net from 'net'; // For WebSocket handling (net.Socket) +import * as url from 'url'; // For handling target URLs +import qs from 'qs'; +import {LxdManager, InstanceCore} from '@itmat-broker/itmat-cores'; +import { Logger } from '@itmat-broker/itmat-commons'; + + +// const textDecoder = new TextDecoder('utf-8'); +export const registerContainSocketServer = (server: WebSocketServer, lxdManager: LxdManager) => { + + server.on('connection', (clientSocket, req) => { + clientSocket.pause(); + let containerSocket: WebSocket | undefined; + const query = qs.parse(req.url?.split('?')[1] || ''); + const operationId = query['o']?.toString() || ''; + const operationSecret = query['s']?.toString() || ''; + const clientMessageBuffers: Array<[Buffer, boolean]> = []; + const containerMessageBuffers: Array<[Buffer, boolean]> = []; + + const flushClientMessageBuffers = () => { + if (containerSocket && containerSocket.readyState === WebSocket.OPEN) { + + const curr = clientMessageBuffers[0]; + if (curr) { + containerSocket.send(curr[0], { binary: curr[1] }, (err) => { + if (err) { + Logger.error('Error sending message to container' + err); + } else { + clientMessageBuffers.shift(); + if (clientMessageBuffers.length > 0) + flushClientMessageBuffers(); + } + }); + } + } + }; + + const flushContainerMessageBuffers = () => { + if (clientSocket.readyState === WebSocket.OPEN) { + const curr = containerMessageBuffers[0]; + if (curr) { + clientSocket.send(curr[0], { binary: curr[1] }, (err) => { + if (err) { + Logger.error(`Error sending message to client ${JSON.stringify(err)}`); + } else { + containerMessageBuffers.shift(); + if (containerMessageBuffers.length > 0) + flushContainerMessageBuffers(); + } + }); + } + } + }; + // send test message to client + clientSocket.on('open', () => { + clientSocket.send('test message'); + }); + + clientSocket.on('message', (message, isBinary) => { + const tuple: [Buffer, boolean] = [Buffer.from(message as ArrayBuffer), isBinary]; + clientMessageBuffers.push(tuple); + flushClientMessageBuffers(); + }); + + clientSocket.on('close', (code, reason) => { + if (containerSocket?.readyState === WebSocket.OPEN) + containerSocket?.close(4110, `The client socket was closed with code ${code}: ${reason.toString()}`); + }); + + clientSocket.on('error', () => { + if (containerSocket?.readyState === WebSocket.OPEN) + containerSocket?.close(4115, 'The client socket errored'); + }); + + try { + containerSocket = lxdManager.getOperationSocket(operationId, operationSecret); + + containerSocket.pause(); + containerSocket.on('open', () => { + flushClientMessageBuffers(); + }); + + containerSocket.on('message', (message: ArrayBuffer | Uint8Array[], isBinary: boolean) => { + // let arrayBuffer; + + // if (isBinary) { + // // message will be one of ArrayBuffer | Buffer[] | Buffer + // if (Array.isArray(message)) { + // // If it's an array, we need to concatenate into a single buffer + // const combinedBuffer = Buffer.concat(message); + // arrayBuffer = combinedBuffer.buffer.slice( + // combinedBuffer.byteOffset, + // combinedBuffer.byteOffset + combinedBuffer.byteLength + // ); + // } else if (message instanceof Buffer) { + // // Node.js Buffer instance, we need to slice the ArrayBuffer it references + // arrayBuffer = message.buffer.slice( + // message.byteOffset, + // message.byteOffset + message.byteLength + // ); + // } else { + // // It's already an ArrayBuffer + // arrayBuffer = message; + // } + + // const messageContent = `Binary message: ${textDecoder.decode(arrayBuffer)}`; + + // const asciiMessage = Array.from(new Uint8Array(arrayBuffer)) + // .map(byte => String.fromCharCode(byte)) + // .join(''); + // const hexMessage = Array.from(new Uint8Array(arrayBuffer)) + // .map(byte => byte.toString(16).padStart(2, '0')) + // .join(' '); + + // console.log(`Message from container: ${messageContent}`); + // console.log(`Message from container: Binary message (ASCII): ${asciiMessage}`); + // console.log(`Message from container: Binary message (Hex): ${hexMessage}`); + // console.log(`Message from container: type ${isBinary}, ${message}`); + // } else { + // // If it's not binary, we expect a text message, which should be a string + // console.log(`Message from container: type ${isBinary}, ${message}`); + // const messageContent = `Text message: ${message.toString()}`; + // console.log(`Message from container: ${messageContent}`); + // } + const tuple: [Buffer, boolean] = [Buffer.from(message as ArrayBuffer), isBinary]; + containerMessageBuffers.push(tuple); + flushContainerMessageBuffers(); + }); + + containerSocket.on('close', (code, reason) => { + // console.log('container websocket close:',code, reason.toString()); + flushContainerMessageBuffers(); + if (clientSocket?.readyState === WebSocket.OPEN) + clientSocket?.close(4010, `The container socket was closed with code${code}: ${reason.toString()}`); + }); + + containerSocket.on('error', () => { + if (clientSocket?.readyState === WebSocket.OPEN) + clientSocket?.close(4015, 'The container socket errored'); + }); + + containerSocket.resume(); + clientSocket.resume(); + + } catch (e) { + Logger.error(`Failed to create container WebSocket: ${JSON.stringify(e)}`); + if (clientSocket?.readyState === WebSocket.OPEN) + clientSocket?.close(4015, 'The container socket failed to open'); + } + }); + server.on('error', (err) => { + Logger.error(`LXD socket broker errored: ${JSON.stringify(err)}`); + }); +}; + + + +export const registerJupyterSocketServer = (server: WebSocketServer, lxdManager: LxdManager, instanceCore: InstanceCore) => { + + server.on('connection', (clientSocket, req) => { + const handleConnection = async () => { + + clientSocket.pause(); + clientSocket.binaryType = 'arraybuffer'; + + // Extract subprotocol from the client's WebSocket request + const clientSubprotocol = req.headers['sec-websocket-protocol'] || ''; + + + let containerSocket: WebSocket | undefined; + + if ( !req.url ||!req.url?.startsWith('/jupyter')){ + Logger.log(`error request ${req.url}`); + clientSocket.close(); + } + // get the instance_id from the url + const instance_id = req.url?.split('/')[2] ?? ''; + const clientMessageBuffers: Array<[RawData, boolean]> = []; + const containerMessageBuffers: Array<[RawData, boolean]> = []; + + let ip = ''; // Initialize with a default value + let port = 0; // Initialize with a default value + try { + // Get the container IP according to the instance_id + const result = await getInstanceTarget(instance_id, instanceCore); + ({ ip, port } = result); // Wrap destructuring in parentheses + } catch (error) { + Logger.error(`Failed to retrieve container IP: ${JSON.stringify(error)}`); + clientSocket.close(); + return; + } + + const targetUrl = `ws://${ip}:${port}${req.url}`; + + const flushClientMessageBuffers = () => { + + if (containerSocket && containerSocket.readyState === WebSocket.OPEN) { + + if (clientMessageBuffers.length > 0) { + const buffer = clientMessageBuffers.shift(); + if (buffer !== undefined) { + const [message, isBinary] = buffer; + containerSocket.send(message, { binary: isBinary }, (err) => { + if (err) { + console.error('Error sending message to container:', err); + } + }); + if (clientMessageBuffers.length > 0) + flushClientMessageBuffers(); + } else { + console.error('Unexpected undefined buffer'); + } + } + } + }; + + const flushContainerMessageBuffers = () => { + if (clientSocket.readyState === WebSocket.OPEN) { + + if (containerMessageBuffers.length > 0) { + const buffer = containerMessageBuffers.shift(); + if (buffer !== undefined) { + const [message, isBinary] = buffer; + clientSocket.send(message, { binary: isBinary }, (err) => { + if (err) { + console.error('Error sending message to client:', err); + } + }); + if (containerMessageBuffers.length > 0) + flushContainerMessageBuffers(); + } + } + + } + }; + clientSocket.on('message', (message, isBinary) => { + // Buffer the message and send it to the containerSocket + clientMessageBuffers.push([message, isBinary]); + flushClientMessageBuffers(); + }); + clientSocket.on('open', () => { + console.log('client socket open'); + clientSocket.ping(); + } ); + + clientSocket.on('error', (error) => { + Logger.error(`clientSocket error: ${JSON.stringify(error, null, 2)}`); + if (containerSocket && containerSocket.readyState === WebSocket.OPEN) { + containerSocket.close(4000, 'Client WebSocket encountered an error.'); + } + }); + + clientSocket.on('close', (code, reason) => { + console.log('clientSocket closed. Code:', code, 'Reason:', reason.toString()); + if (containerSocket && containerSocket.readyState === WebSocket.OPEN) { + containerSocket.close(4001, `Client WebSocket closed. Code: ${code}`); + } + // if is connecting, then set a interval to close the container socket + else if (containerSocket && containerSocket.readyState === WebSocket.CONNECTING) { + const interval = setInterval(() => { + if (containerSocket?.readyState === WebSocket.OPEN) { + containerSocket?.close(4001, `Client WebSocket closed. Code: ${code}`); + clearInterval(interval); + } + }, 1000); + } + }); + + + try { + + // Create WebSocket connection to the backend (container) + const headers = { + 'Sec-WebSocket-Protocol': clientSubprotocol, // Pass client's subprotocol if provided + 'Sec-WebSocket-Key': req.headers['sec-websocket-key'], + 'Sec-WebSocket-Version': req.headers['sec-websocket-version'], + 'Sec-WebSocket-Extensions': req.headers['sec-websocket-extensions'], + 'pragma': req.headers['pragma'], + 'cache-control': req.headers['cache-control'], + 'upgrade': req.headers['upgrade'], + 'connection': req.headers['connection'], + 'Accept-Encoding': req.headers['accept-encoding'], + 'Accept-Language': req.headers['accept-language'], + 'User-Agent': req.headers['user-agent'] + }; + + // Create WebSocket connection to the backend (container) + containerSocket = new WebSocket(targetUrl, + clientSubprotocol ? [clientSubprotocol] : [], + { headers }); + containerSocket.binaryType = 'arraybuffer'; + + containerSocket.pause(); + + + // Set maximum listeners to prevent memory leaks + containerSocket.setMaxListeners(20); + clientSocket.setMaxListeners(20); + + containerSocket.on('message', (message, isBinary) => { + containerMessageBuffers.push([message, isBinary]); + flushContainerMessageBuffers(); + }); + + containerSocket.on('open', () => { + flushClientMessageBuffers(); + }); + + containerSocket.on('error', (error) => { + Logger.error(`Container WebSocket error: ${JSON.stringify(error, null, 2)}`); + if (clientSocket && clientSocket.readyState === WebSocket.OPEN) { + clientSocket.close(4002, 'Container WebSocket encountered an error.'); + } + }); + + containerSocket.on('close', (code, reason) => { + flushContainerMessageBuffers(); + if (clientSocket?.readyState === WebSocket.OPEN) + clientSocket?.close(4010, `The container socket was closed with code${code}: ${reason.toString()}`); + }); + + containerSocket.resume(); + clientSocket.resume(); + + + } catch (error) { + console.error('Failed to create container WebSocket:', error); + if (clientSocket?.readyState === WebSocket.OPEN) + clientSocket?.close(4015, 'The container socket failed to open'); + if (containerSocket?.readyState === WebSocket.OPEN) + containerSocket?.close(4015, 'The container socket failed to open'); + } + }; + // Call the async function + handleConnection().catch((error) => { + Logger.error(`Error handling jupyter connection: ${JSON.stringify(error)}`); + clientSocket.close(); + }); + }); + server.on('error', (err) => { + Logger.error(`LXD Jupyter socket broker errored: ${JSON.stringify(err)}`); + }); +}; + + + +interface ProxyCache { + [instance_id: string]: { + // proxy is the instance of http-proxy + proxy: httpProxy; + ip: string; + port: number; + }; +} + +const proxyCache: ProxyCache = {}; + +// Retrieve dynamic instance IP and port +const getInstanceTarget = async ( instance_id: string, instanceCore: InstanceCore, user_id?: string | undefined) => { + // get the target IP from the instance_id, better to get the ip from database instead of the container directly to avoid the delay + const containerIP = await instanceCore.getContainerIP(instance_id, user_id); + if (!containerIP || !containerIP.ip) { + throw new Error('Failed to retrieve container IP'); + } + const { ip, port } = containerIP; + return { ip, port }; +}; + +// Middleware to handle HTTP and WebSocket proxying for Jupyter instances +export const jupyterProxyMiddleware = async (req: Request & { user?: { id: string } }, res: Response, next: NextFunction, instanceCore: InstanceCore) => { + + const instance_id = req.params['instance_id']; + + if (!instance_id) { + if (!res.headersSent) { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Instance not found'); + } + return; + } + + // Retrieve the instance-specific target if not already cached + if (!proxyCache[instance_id]) { + const { ip, port } = await getInstanceTarget( instance_id, instanceCore, req.user?.id); + const target = `http://${ip}:${port}`; + + // Create an instance of http-proxy + const proxy = httpProxy.createProxyServer({ + target, + xfwd: true, // Add X-Forwarded-For header to the request + autoRewrite: true, + changeOrigin: true + }); + + // Attach event listeners for proxy events + proxy.on('proxyReq', (proxyReq: http.ClientRequest, req: http.IncomingMessage & { body?: unknown }) => { + + const originalUrl = req.url || ''; + const instancePrefix = `/jupyter/${instance_id}`; + + if (!originalUrl.startsWith(instancePrefix)) { + const newUrl = `${instancePrefix}${originalUrl}`; + proxyReq.path = newUrl; // Ensure the proxy forwards the correct URL + } + + if (req.body && Object.keys(req.body).length) { + const contentType = proxyReq.getHeader('Content-Type'); + + const writeBody = (bodyData: string) => { + proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); + proxyReq.write(bodyData); + proxyReq.end(); + }; + + if (contentType === 'application/json') { + writeBody(JSON.stringify(req.body)); + } else if (contentType === 'application/x-www-form-urlencoded') { + writeBody(qs.stringify(req.body)); + } + } + + }); + + proxy.on('error', (err: Error, req: http.IncomingMessage, res: http.ServerResponse | net.Socket, target?: string | url.UrlObject) => { + Logger.error(`Proxy error for target ${JSON.stringify(target)}: error ${err.message}`); + + if (res instanceof http.ServerResponse && !res.headersSent) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Proxy Error'); + } else if (res instanceof net.Socket) { + res.end('Proxy Error'); + } + }); + // Cache the proxy handler for the instance + proxyCache[instance_id] = { proxy, ip, port }; + } + + // Handle regular HTTP requests + try { + proxyCache[instance_id].proxy.web(req, res); + } catch (error) { + Logger.error(`error ${error}`); + } + +}; diff --git a/packages/itmat-interface/src/server/commonMiddleware.ts b/packages/itmat-interface/src/server/commonMiddleware.ts index 64e2176ef..1a2f3682c 100644 --- a/packages/itmat-interface/src/server/commonMiddleware.ts +++ b/packages/itmat-interface/src/server/commonMiddleware.ts @@ -21,6 +21,10 @@ export const tokenAuthentication = async (token: string) => { } return true; }); + // TODO: check if the token is system token for lxd-ae user + // if (decodedPayload['iss'] === 'lxd-ae') { + // then user retrieval by userID + // store the associated user with the JWT to context try { const associatedUser = await userRetrieval(db, pubkey); diff --git a/packages/itmat-interface/src/server/helper.ts b/packages/itmat-interface/src/server/helper.ts index ba173d712..0eb5ec3e8 100644 --- a/packages/itmat-interface/src/server/helper.ts +++ b/packages/itmat-interface/src/server/helper.ts @@ -1,6 +1,6 @@ -import { ConfigRouter, DataRouter, DomainRouter, DriveRouter, FileResolvers, GraphQLResolvers, JobResolvers, LogResolvers, LogRouter, OrganisationResolvers, OrganisationRouter, PermissionResolvers, PubkeyResolvers, RoleRouter, StandardizationResolvers, StudyResolvers, StudyRouter, TRPCAggRouter, UserResolvers, UserRouter, tRPCBaseProcedureMilldeware, WebAuthnRouter} from '@itmat-broker/itmat-apis'; +import { ConfigRouter, DataRouter, DomainRouter, DriveRouter, FileResolvers, GraphQLResolvers, JobResolvers, LogResolvers, LogRouter, OrganisationResolvers, OrganisationRouter, PermissionResolvers, PubkeyResolvers, RoleRouter, StandardizationResolvers, StudyResolvers, StudyRouter, TRPCAggRouter, UserResolvers, UserRouter, tRPCBaseProcedureMilldeware, WebAuthnRouter, InstanceRouter, LXDRouter} from '@itmat-broker/itmat-apis'; import { db } from '../database/database'; -import { ConfigCore, DataCore, DataTransformationCore, DomainCore, DriveCore, FileCore, LogCore, OrganisationCore, PermissionCore, StandarizationCore, StudyCore, UserCore, UtilsCore, WebauthnCore } from '@itmat-broker/itmat-cores'; +import { ConfigCore, DataCore, DataTransformationCore, DomainCore, DriveCore, FileCore, LogCore, OrganisationCore, PermissionCore, StandarizationCore, StudyCore, UserCore, UtilsCore, WebauthnCore, JobCore, InstanceCore, LxdManager} from '@itmat-broker/itmat-cores'; import { objStore } from '../objStore/objStore'; import { mailer } from '../emailer/emailer'; import configManager from '../utils/configManager'; @@ -22,6 +22,9 @@ export class APICalls { configCore: ConfigCore; domainCore: DomainCore; webauthnCore: WebauthnCore; + jobCore: JobCore; + instanceCore: InstanceCore; + lxdManager: LxdManager; constructor() { this.permissionCore = new PermissionCore(db); this.fileCore = new FileCore(db, objStore); @@ -37,6 +40,9 @@ export class APICalls { this.configCore = new ConfigCore(db); this.domainCore = new DomainCore(db, this.fileCore); this.webauthnCore = new WebauthnCore(db, mailer, configManager, objStore); + this.jobCore = new JobCore(db); + this.instanceCore = new InstanceCore(db, mailer, configManager, this.jobCore, this.userCore); + this.lxdManager = new LxdManager(configManager); } _listOfGraphqlResolvers() { @@ -79,7 +85,9 @@ export class APICalls { new LogRouter(baseProcedure, router, this.logCore), new DomainRouter(baseProcedure, router, this.domainCore), new OrganisationRouter(baseProcedure, router, this.organisationCore), - new WebAuthnRouter(baseProcedure, router, this.webauthnCore) + new WebAuthnRouter(baseProcedure, router, this.webauthnCore), + new InstanceRouter(baseProcedure, router, this.instanceCore), + new LXDRouter(baseProcedure, router, this.lxdManager) ))._routers(); } } \ No newline at end of file diff --git a/packages/itmat-interface/src/server/router.ts b/packages/itmat-interface/src/server/router.ts index 351a2a893..7410a11e7 100644 --- a/packages/itmat-interface/src/server/router.ts +++ b/packages/itmat-interface/src/server/router.ts @@ -17,7 +17,7 @@ import passport from 'passport'; import { db } from '../database/database'; import { fileDownloadControllerInstance } from '../rest/fileDownload'; import { BigIntResolver as scalarResolvers } from 'graphql-scalars'; -import { createProxyMiddleware, RequestHandler } from 'http-proxy-middleware'; +import { createProxyMiddleware} from 'http-proxy-middleware'; import qs from 'qs'; import { FileUploadSchema, IUser, IUserConfig, enumConfigType, enumUserTypes } from '@itmat-broker/itmat-types'; import { logPluginInstance } from '../log/logPlugin'; @@ -30,12 +30,15 @@ import { Readable } from 'stream'; import { z } from 'zod'; import { ApolloServerContext, DMPContext, createtRPCContext, typeDefs } from '@itmat-broker/itmat-apis'; import { APICalls } from './helper'; +import { registerContainSocketServer, registerJupyterSocketServer, jupyterProxyMiddleware } from '../lxd'; +import { Socket } from 'node:net'; +import { Logger } from '@itmat-broker/itmat-commons'; export class Router { private readonly app: Express; private readonly server: http.Server; private readonly config: IConfiguration; - public readonly proxies: Array> = []; + // public readonly proxies: Array> = []; constructor(config: IConfiguration) { @@ -257,7 +260,7 @@ export class Router { } }); - this.proxies.push(ae_proxy); + // this.proxies.push(ae_proxy); /* AE routers */ // pun for AE portal @@ -284,17 +287,21 @@ export class Router { /* register the graphql subscription functionalities */ // Creating the WebSocket subscription server - const wsServer = new WebSocketServer({ - // This is the `httpServer` returned by createServer(app); - server: this.server, - // Pass a different path here if your ApolloServer serves at - // a different path. - path: '/graphql' + // const wsServer = new WebSocketServer({ + // // This is the `httpServer` returned by createServer(app); + // server: this.server, + // // Pass a different path here if your ApolloServer serves at + // // a different path. + // path: '/graphql' + // }); + const graphqlWsServer = new WebSocketServer({ + noServer: true // We'll handle upgrades manually }); + // Passing in an instance of a GraphQLSchema and // telling the WebSocketServer to start listening - const serverCleanup = useServer({ schema: schema, execute: execute, subscribe: subscribe }, wsServer); + const serverCleanup = useServer({ schema: schema, execute: execute, subscribe: subscribe }, graphqlWsServer); /* Bounce all unauthenticated non-graphql HTTP requests */ // this.app.use((req: Request, res: Response, next: NextFunction) => { @@ -363,15 +370,83 @@ export class Router { this.app.get('/file/:fileId', fileDownloadControllerInstance.fileDownloadController); + + // set up the proxy and register directly, combine the grapgql's websocket and lxd's websocket + + // Setup HTTP route handlers for Jupyter proxy requests + const proxyRoutes = ['/jupyter/:instance_id', '/jupyter/:instance_id/*']; + proxyRoutes.forEach((route) => { + this.app.use(route, (req, res, next) => { + jupyterProxyMiddleware(req, res, next, apiCalls.instanceCore).catch(next); + }); + }); + + // Create WebSocket server for RTC connections + const containerWsServer = new WebSocketServer({ + noServer: true + }); + + registerContainSocketServer(containerWsServer, apiCalls.lxdManager); + + // Create WebSocket server for connections + const targetWsServer = new WebSocketServer({ + noServer: true + }); + + registerJupyterSocketServer(targetWsServer, apiCalls.lxdManager, apiCalls.instanceCore); + + + // Upgrade handler for WebSocket requests + this.server.on('upgrade', (req, socket: Socket, head: Buffer) => { + + if (req.url?.startsWith('/graphql')) { + // Handle GraphQL WebSocket connection for subscriptions + graphqlWsServer.handleUpgrade(req, socket, head, (ws) => { + graphqlWsServer.emit('connection', ws, req); // Forward the upgrade to the GraphQL WebSocket server + }); + } else if(req.url?.startsWith('/jupyter')) { + const instance_id = req.url.split('/')[2]; // Extract instance ID from the URL + + if (!instance_id) { + Logger.error('No instance ID found in URL.'); + socket.end(); + return; + } + try { + targetWsServer.handleUpgrade(req, socket, head, (ws) => { + + targetWsServer.emit('connection', ws, req); // Forward the upgrade to the container WebSocket server + } + ); + } catch (error) { + Logger.log(`error ${JSON.stringify(error)}`); + socket.end(); // End socket if there's an error creating the proxy + return; } + } + // Handle RTC WebSocket connections + else if (req.url?.startsWith('/rtc')) { + containerWsServer.handleUpgrade(req, socket, head, (ws) => { + containerWsServer.emit('connection', ws, req); // Forward the upgrade to the container WebSocket server + }); + } else if (req.url?.startsWith('/pun') || req.url?.startsWith('/node') || req.url?.startsWith('/rnode') || req.url?.startsWith('/public')) { + ae_proxy.upgrade?.(req, socket, head); // Forward the upgrade to the AE proxy + } + // Invalid WebSocket request + else { + Logger.error(`Invalid WebSocket upgrade request: ${req.url}`); + socket.destroy(); // Close socket if the request is not valid + } + }); + } public getApp(): Express { return this.app; } - public getProxy(): RequestHandler { - return this.proxies[0]; - } + // public getProxy(): RequestHandler { + // return this.proxies[0]; + // } public getServer(): http.Server { return this.server; diff --git a/packages/itmat-interface/test/trpcTests/instance.test.ts b/packages/itmat-interface/test/trpcTests/instance.test.ts new file mode 100644 index 000000000..a35f5cfdd --- /dev/null +++ b/packages/itmat-interface/test/trpcTests/instance.test.ts @@ -0,0 +1,289 @@ +/** + * @with Minio + */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { db } from '../../src/database/database'; +import { objStore } from '../../src/objStore/objStore'; +import request from 'supertest'; +import { connectAdmin, connectUser } from './_loginHelper'; +import { Router } from '../../src/server/router'; +import { setupDatabase } from '@itmat-broker/itmat-setup'; +import config from '../../config/config.sample.json'; +import { v4 as uuid } from 'uuid'; +import { Express } from 'express'; +import { IUser, enumUserTypes, LXDInstanceTypeEnum, enumAppType, enumOpeType, enumInstanceStatus } from '@itmat-broker/itmat-types'; +import { Db, MongoClient } from 'mongodb'; +import { JobCore, UserCore } from '@itmat-broker/itmat-cores'; + + + + +jest.mock('nodemailer', () => { + const { TEST_SMTP_CRED, TEST_SMTP_USERNAME } = process.env; + if (!TEST_SMTP_CRED || !TEST_SMTP_USERNAME || !config?.nodemailer?.auth?.pass || !config?.nodemailer?.auth?.user) + return { + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: jest.fn() + })) + }; + return jest.requireActual('nodemailer'); +}); + + +if (global.hasMinio) { // eslint-disable-line no-undef + let app: Express; + let mongodb: MongoMemoryServer; + let admin: request.SuperTest; + let user: request.SuperTest; + let mongoConnection: MongoClient; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let mongoClient: Db; + let adminProfile: IUser; + let userProfile: IUser; + + const setupDatabaseAndApp = async () => { + const dbName = uuid(); + mongodb = await MongoMemoryServer.create({ instance: { dbName } }); + const connectionString = mongodb.getUri(); + await setupDatabase(connectionString, dbName); + + config.objectStore.port = global.minioContainerPort; + config.database.mongo_url = connectionString; + config.database.database = dbName; + + await db.connect(config.database, MongoClient); + + await objStore.connect(config.objectStore); + + // Initialize the router and application + const router = new Router(config); + await router.init(); + + mongoConnection = await MongoClient.connect(connectionString); + mongoClient = mongoConnection.db(dbName); + + app = router.getApp(); + admin = request.agent(app); + user = request.agent(app); + await connectAdmin(admin); + await connectUser(user); + + // Connect admin and user (mock login) + const users = await db.collections.users_collection.find({}).toArray(); + adminProfile = users.find(el => el.type === enumUserTypes.ADMIN); + userProfile = users.find(el => el.type === enumUserTypes.STANDARD); + }; + + + let userCoreMock: jest.SpyInstance; + let jobCoreMock: jest.SpyInstance; + + beforeAll(async () => { + await setupDatabaseAndApp(); + + // Mock specific methods inside UserCore and JobCore + userCoreMock = jest.spyOn(UserCore.prototype, 'issueSystemAccessToken').mockResolvedValue({ accessToken: 'mocked-token' }); + jobCoreMock = jest.spyOn(JobCore.prototype, 'createJob').mockResolvedValue(true); + }); + + afterAll(async () => { + await db.closeConnection(); + await mongoConnection.close(); + await mongodb.stop(); + userCoreMock.mockRestore(); + jobCoreMock.mockRestore(); + }); + afterEach(async () => { + await db.collections.instance_collection.deleteMany({}); // Clean up all instances after each test + }); + + describe('tRPC Instance APIs', () => { + + describe('createInstance', () => { + test('Should create an instance successfully', async () => { + const response = await user.post('/trpc/instance.createInstance').send({ + name: 'test-instance', + type: LXDInstanceTypeEnum.CONTAINER, + appType: enumAppType.JUPYTER, + cpuLimit: 2, + memoryLimit: '4GB', + diskLimit: '10GB', + lifeSpan: 3600 + }); + + + expect(response.status).toBe(200); + expect(response.body.result.data.name).toBe('test-instance'); + + const instance = await db.collections.instance_collection.findOne({ name: 'test-instance' }); + expect(instance).toBeDefined(); + expect(instance?.name).toBe('test-instance'); + }); + + test('Should throw error when user is not logged in', async () => { + const response = await request(app).post('/trpc/instance.createInstance').send({ + name: 'test-instance', + type: LXDInstanceTypeEnum.CONTAINER, + appType: enumAppType.JUPYTER, + lifeSpan: 3600 + }); + + expect(response.status).toBe(400); // User is not authenticated + expect(response.body.error).toBeDefined(); + }); + }); + + describe('getQuotaAndFlavors', () => { + test('Should return quota and flavors for logged-in user', async () => { + + const response = await user.get('/trpc/instance.getQuotaAndFlavors').send(); + expect(response.status).toBe(200); + expect(response.body.result?.data?.userQuota).toBeDefined(); + expect(response.body.result?.data?.userFlavors).toBeDefined(); + }); + + test('Should throw error when user is not logged in', async () => { + const response = await request(app).get('/trpc/instance.getQuotaAndFlavors').send(); + expect(response.status).toBe(400); + expect(response.body.error).toBeDefined(); + expect(response.body.error.message).toBe('User must be authenticated.'); + }); + }); + + describe('startStopInstance', () => { + test('Should start an instance successfully', async () => { + const instanceId = uuid(); + await db.collections.instance_collection.insertOne({ + id: instanceId, + name: 'test-instance', + type: LXDInstanceTypeEnum.CONTAINER, + appType: enumAppType.JUPYTER, + userId: userProfile.id, + status: enumInstanceStatus.PENDING + }); + + const response = await user.post('/trpc/instance.startStopInstance').send({ + instanceId, + action: enumOpeType.START + }); + + expect(response.status).toBe(200); + + const instance = await db.collections.instance_collection.findOne({ id: instanceId }); + expect(instance?.status).toBe(enumInstanceStatus.STARTING); + }); + + test('Should return error when unauthorized user tries to start an instance', async () => { + // Step 1: Create an instance assigned to another user + const instanceId = uuid(); // Generate a unique instance ID + await db.collections.instance_collection.insertOne({ + id: instanceId, + name: 'test-instance', + type: LXDInstanceTypeEnum.CONTAINER, + appType: enumAppType.JUPYTER, + userId: 'some-other-user-id', // Assign this instance to another user (not the logged-in user) + status: enumInstanceStatus.PENDING // Set initial status to PENDING + }); + + // Step 2: Make the request without a logged-in user (using `request(app)` directly) + const response = await request(app).post('/trpc/instance.startStopInstance').send({ + instanceId: instanceId, + action: enumOpeType.START + }); + + // Step 3: Ensure the user gets a 400 Forbidden error since they don't own the instance + expect(response.status).toBe(400); // Expecting a Forbidden status code + expect(response.body.error).toBeDefined(); // Ensure the response contains an error + }); + }); + + describe('restartInstance', () => { + test('Should restart an instance successfully', async () => { + const instanceId = uuid(); + await db.collections.instance_collection.insertOne({ + id: instanceId, + name: 'test-instance', + type: LXDInstanceTypeEnum.CONTAINER, + appType: enumAppType.JUPYTER, + userId: userProfile.id, + status: enumInstanceStatus.STOPPED + }); + + const response = await user.post('/trpc/instance.restartInstance').send({ + instanceId, + lifeSpan: 7200 + }); + + expect(response.status).toBe(200); + + const instance = await db.collections.instance_collection.findOne({ id: instanceId }); + expect(instance?.status).toBe(enumInstanceStatus.STARTING); + expect(instance?.lifeSpan).toBe(7200); + }); + }); + + describe('getInstances', () => { + test('Logged-in user should be able to retrieve their own instances', async () => { + // Insert an instance for the logged-in user + await db.collections.instance_collection.insertOne({ + id: uuid(), + name: 'user-instance', + type: LXDInstanceTypeEnum.CONTAINER, + appType: enumAppType.JUPYTER, + userId: userProfile.id, // Belongs to the logged-in user + status: enumInstanceStatus.RUNNING, + config: { + 'limits.cpu': '2', + 'limits.memory': '4GB' + } + }); + + // The logged-in user should retrieve their own instance + const response = await user.get('/trpc/instance.getInstances').send(); + expect(response.status).toBe(200); + expect(response.body.result.data).toHaveLength(1); // Only the user's instance + expect(response.body.result.data[0].name).toBe('user-instance'); + }); + + test('Non-logged-in user should not be able to retrieve any instances', async () => { + // Try to retrieve instances without being logged in + const response = await request(app).get('/trpc/instance.getInstances').send(); + + // Ensure the user gets a 400 Unauthorized error + expect(response.status).toBe(400); // Unauthorized + expect(response.body.error).toBeDefined(); + expect(response.body.error.message).toBe('Insufficient permissions.'); + }); + }); + + describe('deleteInstance', () => { + test('Admin should delete an instance successfully', async () => { + const instanceId = uuid(); + await db.collections.instance_collection.insertOne({ + id: instanceId, + name: 'instance-to-delete', + type: LXDInstanceTypeEnum.CONTAINER, + appType: enumAppType.JUPYTER, + userId: adminProfile.id, + status: enumInstanceStatus.RUNNING + }); + + const response = await admin.post('/trpc/instance.deleteInstance').send({ + instanceId + }); + + expect(response.status).toBe(200); + + const instance = await db.collections.instance_collection.findOne({ id: instanceId }); + expect(instance?.status).toBe(enumInstanceStatus.DELETED); + }); + }); + + }); +} else { + test(`${__filename.split(/[\\/]/).pop()} skipped because it requires Minio on Docker`, () => { + expect(true).toBe(true); + }); +} \ No newline at end of file diff --git a/packages/itmat-job-executor/config/config.sample.json b/packages/itmat-job-executor/config/config.sample.json index fba58f2ef..e54f8ab17 100644 --- a/packages/itmat-job-executor/config/config.sample.json +++ b/packages/itmat-job-executor/config/config.sample.json @@ -1,4 +1,5 @@ -{ +{ + "appName": "ITMAT Job Executor", "database": { "mongo_url": "mongodb://localhost:27017", "database": "itmat", @@ -23,12 +24,17 @@ "cache_collection": "CACHE_COLLECTION", "drives_collection": "DRIVE_COLLECTION", "colddata_collection": "COLDDATA_COLLECTION", - "domains_collection": "DOMAIN_COLLECTION" + "domains_collection": "DOMAIN_COLLECTION", + "webauthn_collection": "WEBAUTHN_COLLECTION", + "instance_collection": "INSTANCE_COLLECTION" } }, "server": { "port": 3003 }, + "bcrypt": { + "saltround": 2 + }, "objectStore": { "host": "localhost", "port": 9000, @@ -37,5 +43,32 @@ "bucketRegion": "region", "useSSL": false }, - "pollingInterval": 10000 + "sessionsSecret": "Change_Me", + "aesSecret": "change_this", + "nodemailer": { + "auth": { + "user": "change_this", + "pass": "change_this" + } + }, + "adminEmail": "admin@example.com", + "aeEndpoint": "http://localhost:9090", + "useWebdav": false, + "webdavPort": 1900, + "webdavServer": "http://localhost:4200/webdav", + "pollingInterval": 10000, + + "lxdEndpoint": "https://localhost:8443", + "lxdStoragePool": "newpool", + "lxdProject": "default", + "systemKey":{ + "pubkey": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnKXlNUDuKSAAClMe2cXf\n7maUnwcS2qsPYuG/gjZn8/Y+ICguPia5c+xlcQgHFYP7bBLeohsPbnp5bG0YTgeg\nGlLPoy+z/rZKBdKTXnv3fAy6ko9SnjpxfC/+kIkNfMcQe7YiwyGsvhVRYUErn5zZ\nQdxjot+DHXrrOsX00fu/E7mU0VqeISouRKkeYSzx4kJVYxGE89hrya8RyOmnyAay\nP5rhPicde4Oiag4J4K+t6eyNS3cel4JiKct8p+fPs+ryw/yzhYOffpOhkAJZvVcR\n/FRB1dUio2pn6BrqYZGWEJllZbOEbhKep8VKCQgqhI74Ab9Bl/hZU6n6+FFJbJfe\nFUaFqdkIMrEy5lKA7K50prhvkjokvf3XeDl+2WnfndIGVjcDMag1KXoQqdqdEScq\nT9DCQuib+JIdq4P81krtqxMOKg4c5cPhzTydACluAr6FLNjStCIMcI0uDHlmfIt/\nfd3/VdYoCxl0paZlA6Ku406HvpBk9oGz7i7eLQ2G6Wby3OP2EPA3JNW6zqe+AnfJ\nszLc39MrBeR4BMmj4++9QK5EsF+hg9KVVLwbaVs0HxlRqvnCN99M0EVPZL1DhnmQ\nQCn5O5vVAvmeDnTb+keI79qA6JIABdoEZSjFB2pWrwC/EQQj2nzL56EPT1oCNvLF\nBp5cqMAVb4vGCe2LAZ8MNxECAwEAAQ==\n-----END PUBLIC KEY-----\n", + "privkey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIJtTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQAxpn0LAybkjSkRMr\nK6XcqgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAKKAQmCpD6ZU0rz\nsu4s6q4EgglQVD13OfDhXnqrJT2v3CHq5rr5bip3P+j6yacgeB2l+s/IlY+0MM2f\n8JtOojUV9RYclV1dAi8krIOv2KVEH81Olb/mse5/Zr7BKBe5g+9jHF6EtWuTWqzX\n2TTkatULeJDnqUd+tBPBmuY3CUEl/6nUepTNzrRoVG4Rp/z2IbtaMx3IIpEZ9Fgt\n3iMkY0O8rtattwJp2k+eIFFf2c63J+NlXzIbuvLqNuPMq/GaUUCLtVsljZdtS/nw\n+tPQJfxW0hfawq1erIMu32Cnm2Tp0ZR2tGFIPMXT+zB/m4TEjbNEwS1abGqZ0y/l\nzJUckj5i5MCSmiF6Mxv9AVAXixlMWbvzhC9isyJKJY+3fde99/h71ZPMaO5NBJW6\nAzguuMc8DbVuGbWKUIkRMyj5Tm63YRasQIzTdrrZ6MR5QA4090RC8n3AsCguVzYB\n7+7fKhDNUIUsvuIXQ95WYpndJF/UbUIGtI8u1DpRZd7U/0lFkl2oX5UPIXgEY1Kv\n8JuWKhH/sYpMhmSWJqBmKV0ZhzDdMRJJtRJpSnLwEO1xchvTtjw9ySjdWZvGLF5z\n7uJwUKCu7aeo18YH4YpW66ladoBWXWN3bfZ5+d3mTMgV6d4z8Opg/8NzC1fsdfzm\n9uqrRFm9+dedCmnQ7w4wfKnDrmeg4oOMgXnbzyxVabYfade2Qk73bxdzUh66pocW\ngis6Q6MSwhHGXIShUKhcFtZAuON9Yedfe6ycSNrYP8Z2aU3Zmg8QcNMJnk3RXtms\nFPTt5SCz/re0ZsckgYD1CfQHB2zrp1PgV0O0wMGAw5FBDxbfEW+rA7qp21UJJBG8\n14YtepW+7sOFYZX164JZQrpCzZ4Qkdx9MeRMc6z670rOh132DpZ/vZLp9JEZt46I\nEXse7fcp3mfl2s4lMQERqYnphQ45ZUheNZge7qrPXKhE26Eqk4O8yE3EPPGzlIrf\nWLzUxCF/k19d+r7NLa6i5uHZ1RnB/kepTB9tdaVMY9CX68kIMKUN8HDNLxKzB4bC\nv6/VLjWt14P0OUnJ/uuZ3TwwXeAmBVJJb99UwKoUp6SKYyH1tShzMKNNNXZwdhhr\nCGf1T+1f1lnqDQX+YcQbmyV9EhPNpOtsSNDawJgN8qSs+fOY4vvpgPsLMmzlFsL3\nG8beIQaJ1Kryip9lJ7bKMZQzy/JTDHnH4+RHn57GcZtZTumFcHkhtexJXhWUxgQB\ncny0myAkvqt9/iF6hRGZYmcP2BWHRg1bXotETcdBk4gvpWf6byt95GA7OK1n+K7J\nrnit9DYG1Mq8K0t0Dju2FpMzOQMy3r+EciiAXwpU9Alio7lWbnYpZwIM5DtCTc07\nwwyFiMdOtku5Hg54VXSMAYJpY1+37bNtSlKkT/G24j7rT9Vx1DMeZwLY97bBZ9OE\napbmRWLUHi+jKdjkxId4+Bx46WIqYyawdKP7N6W+TCd+W7ChBm0/3L6E+tQYyt+z\nL5xfyNVZ3Cr3zzqHgrN33LFWAtEhVUzIxUOzXlWA7QdTSQsOYaERH+An01kRdqTA\nwg+pBNtd/4DskZdKMikGNsvOGhfx1b51S2zD/Jv7j/SvaWIiXkDtB7f/TBkYBBMB\nc3otTTtd8F6GnmuBQnRRK84pk3jhJmW5vEle6ok1tr/mWGU658ToreCWXZZ+FnLe\nmzY9z7qPFZu0P5tM4dxDrASwkxpC7ed3UDbzdL23TTL6naSW7SpGqOhgZ+sUrUAg\nLl7s5BrILXgqx+ymLFtaFW5Pk1h2bTJPQgBNP7VaUx6a5GI048uGS1loGKMzDjGl\n6mNVIAAaI5W7cJel/jph8JJivGav1rZGr5TGmMJtTkE4PBRYdhOfpIKeHY/Ylu3R\n43cKvN3ci3jvzdTkW577Msd2TFk150Mn0hP47stpNWqb0WWieNGjeiN4Wj270iqT\n+ifAkq1hBkU4Ytt1MQOwbpKOmrl51SZO8zUhVIgGPk3UaUtrm/T9usVAi7oyLdmO\ntKLNFPJp9CIotwwQybbHlMWQ862SgT6LOJkLsrPwxfRz3XajnBKH2ABMXRCHE5ZO\nqvTuZoxa4azIp+EATqroPkjzJn+VQRC//7X4kaL44Ijga8JzFedgyvIVyGdMMmAu\nyxHjY1xEryhUEefWA+FnSB47mJSxRKS9h1upsaR2k8toyMRHDKTVgQViyGq5O6Sj\n7Tl0fYnUM6hqRI+M9A54CFwouoeIQybbAC6cFs4ka087HKa53fe8ZfaAUmWX0iuW\nnrhwIe2Z2o48pxNWENduWWHsnBnPZnJwZtOoLRU4QHwYfBGxzBFfXPbhVb4XY9/9\npQICA+nBCZSNylnIiw7CFfFvfvgMYls0xcj+0jakSsyoVj01ojw7crcOGTULQs8Q\nbvjnkL+5Vk3BLdiKS9EGzn/4STJYbQOJ2VzgvAwTxYccrlOjiLAfj9pOZW56MV0F\n4vyxrWXqKGvKP7xXFaw2C3Aud4fII5gVGn3SDVg8em/jlOch+78fGVnprRALbOEv\nEh5NoG6m06hzvf1/90kKZd7ADzlFG7rLXhU2yazt+Om6Jnb/+Yf+iElTaBRv3BcF\nBHgeCZqMhJYtI6bkUJ7OpX3TkOP+s8qRI6BPUaIQPTdo5VQZwHS2OiligWSxnY4r\nXeCKcz6mT65vLcc4zkNHiv1r6nQmZb1wW2JFqWPOXzt+zK4ABJMv39b9JHney9Lm\nBhajnBmlYLbQCrF8u/W/hrkcNwUeezF79pThX6M4OSLs0pfGIaJytDOjYI8YpAmf\n8RZJJbdjCg5IqQqRBAjnt4hHVgvVIaFyb55OVa0VixbHXTs+zuZds0lyTiPNam3Q\n1JEJRy9JM//nxCwO4Lvpgfz1xNija3zVHISvlTWiiBrq8XgeTI4ZYKfw9CKMIUle\n49w9HGCYwfJltrmfjkAODsTGTABeVuKYTx2YgCUdUemlfdTVOTgjouhOjWQZELJs\nmmiXksmId/NP8B+jBnDs/ycvSCcqp4c+TAgeWnKF7ot6y/JaO2jLNVV4bjajfYY8\nd2OnZyJRWa56f+szPM+1kAaiJm2q6VQz29W2KD//Ng7noFC11RfzvwVBzKHsNojW\nbIfQQwIYwqz1RTmLeY23aaKiHXfIH6Av3qfpUxRlzMcAt7QoweY52dFd+TFjG99+\n615Zg5U22Wra3541u9Cj2P7RRhLH+gBqtfRmsl/6KZVFZfr1pnrxgQA=\n-----END ENCRYPTED PRIVATE KEY-----\n" + }, + "lxdCertFile": { + "cert": "-----BEGIN CERTIFICATE-----\nMIIFETCCAvmgAwIBAgIUQQ5zBtfZBlNWzwy9HJB4VV2BgTswDQYJKoZIhvcNAQEL\nBQAwGDEWMBQGA1UEAwwNbXktbHhkLWNsaWVudDAeFw0yMzEyMjAxNDQyMzlaFw0y\nNDEyMTkxNDQyMzlaMBgxFjAUBgNVBAMMDW15LWx4ZC1jbGllbnQwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCbwsuJRGSa5UbtYdjqQD0fo9pBwHSgHB14\n0zezRxlzgI8272y0/1Zwie4O1DxRPu/85zP/q2IyB9pr8RP4744a+ICPD3pdM56Y\ntHP34Pd6OxWemN+YRfst9VRHQ54hsz0lWD8qeNuuRL5MPgaaomNNC4LBJKRTC6Iy\nSmj1Mc7hbiTW7IVqwmFCmNtlW1iOY6JpJfhL+Zi13g0SBJwkoO7RoPKv9B3jee4x\nU6P7mqtUSOYvmO3i5hfI+cTPpnEBrlCZ2/5DzL8l4e78r6qCCwzhnSYYVcAgLIYY\nlcL47qjx5f8czrT/gZrOC010K0snsRV1sQ4ovsfKhc9qhZNAILIxfPoObsmIpRdD\n3b6JsfJlHVrWgiLiqwFMC/Fa+caKfPt+qhBTelPvwggST7y0/0k5hUrgX69NZe09\n2GH0Dr7I6IOyls9NDxMz7ZgOoY3bHsb47U6yTV57qjWwkbv2EkY/Nseqf5G0ZLE8\nyT5VgLhii+/zZb3Ch+T6dFjwNHfEWPmUY+9L9AHOYvU713NoH9gScGtWQtVGs3l3\nQ8z2pbAnQiRkYfRi2/4gQ5jMKnATztBx09tXx0vzspGWHB7j+9nq0fFgqmtBDulh\nfamZf/09ge1vbjbR4GrmWysKjrxGK6jezMa8yyK2jFs8+a5OatCV7n+MTRnn1Bib\n9/R/9At0dQIDAQABo1MwUTAdBgNVHQ4EFgQUke2sAkbSJmeXS3t+xlW2/JGGqCIw\nHwYDVR0jBBgwFoAUke2sAkbSJmeXS3t+xlW2/JGGqCIwDwYDVR0TAQH/BAUwAwEB\n/zANBgkqhkiG9w0BAQsFAAOCAgEAgQ7O3gY62ZDeJtLEIlp+orAG1n6LHBh2Z2Lc\nrSW/YbTIXSVF9FbKGBU6MTrxCdBNOEebSXQToYTKgz7KfUa6LnWmuqHMU7UYmVc8\ncOVVc40H5Pyyz1AHdCN4HxpRZgOzy5/95bYdsFN6JRfLqnIG+EW6XjK13arMshRR\n7E9JmLTnikmfdARUdALSHqQd+jRS4MfpU+3kvnaeSz48UwdN8/GxqFtWoRdmqBcV\ngJ8tzLzKM6MaUHum5H14dYISN2BOnAINHPqt0y+Lh7HJtmiBtLpoSbofUtIsAgYn\nDW0uNtL9gQONa5kKo8KHSoX5nmLdSQycj1Bhm84Z7qw83XNTgAjjcAf0eTWU2gjH\nbjX5XqdiQ+d4RmPEJsPoAKNVcvbJBT6ywG5P91GW8kbMCuQ/ieJw/fc0UdQRye+D\nZwbNoKez6Pfu8H8EbTa9sL7n51lu4VIgvNujpl5oK/MP6pt4lQzoqEmWe8V+8TN4\nVv/Ez+UpJqH6KHINruYU3OHO4CCH1Cj+zSdY7mKjeflOM268JD0mcZwN1Tyxsf16\nah7Xfs3kyRapi1+dQnrXphM39HcXudUpDoa6+n+aeMd+ly/Q7ZdpP0nbUrc7M2DK\n9VgTkGup7F20Yn1d/JU/taS2yKG7rwrX3swEQ/KgqMoTFkIES1hfiVE5z2Dj94BE\njnKGDYg=\n-----END CERTIFICATE-----", + "key": "-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCbwsuJRGSa5Ubt\nYdjqQD0fo9pBwHSgHB140zezRxlzgI8272y0/1Zwie4O1DxRPu/85zP/q2IyB9pr\n8RP4744a+ICPD3pdM56YtHP34Pd6OxWemN+YRfst9VRHQ54hsz0lWD8qeNuuRL5M\nPgaaomNNC4LBJKRTC6IySmj1Mc7hbiTW7IVqwmFCmNtlW1iOY6JpJfhL+Zi13g0S\nBJwkoO7RoPKv9B3jee4xU6P7mqtUSOYvmO3i5hfI+cTPpnEBrlCZ2/5DzL8l4e78\nr6qCCwzhnSYYVcAgLIYYlcL47qjx5f8czrT/gZrOC010K0snsRV1sQ4ovsfKhc9q\nhZNAILIxfPoObsmIpRdD3b6JsfJlHVrWgiLiqwFMC/Fa+caKfPt+qhBTelPvwggS\nT7y0/0k5hUrgX69NZe092GH0Dr7I6IOyls9NDxMz7ZgOoY3bHsb47U6yTV57qjWw\nkbv2EkY/Nseqf5G0ZLE8yT5VgLhii+/zZb3Ch+T6dFjwNHfEWPmUY+9L9AHOYvU7\n13NoH9gScGtWQtVGs3l3Q8z2pbAnQiRkYfRi2/4gQ5jMKnATztBx09tXx0vzspGW\nHB7j+9nq0fFgqmtBDulhfamZf/09ge1vbjbR4GrmWysKjrxGK6jezMa8yyK2jFs8\n+a5OatCV7n+MTRnn1Bib9/R/9At0dQIDAQABAoICABQv0SHaK6Zo3qIMqLM7XhtH\n/hUVdJ1SPNOYSZjCNzSL+IOoe2XoeHMfZ9X41am7RsPNJyUUjiZpHtAXL30iIZWx\n7TTH+cDd3B5zDLCXMcZf+p5SYRrNabn8GKR1HqfEIMGX8p/LiBUe3aw97FcwLT8i\npcXoZ3pH1gk/MY54RRVcVAlIIBvE/hvxJEYPlPuR7Xjugy6S4Pgjw305UbnsODy8\n55cRmVS49xDiLoIrpSwdl+Tv+gIKdhIFpOc7QrFyvl7NGeKpmisLtrsoUn3bdsZA\nEAbes4a389fN2Qtayv8xeA3Aux5+KpUCS/3T+pU1/c+srGbFda6esade5nBM4dW/\nidLFEXDRjar3kYBq/4hfg/7MOIOZX9XFa3jg0yhzCHiTDFHmXKKGC+EABOFcOh1E\nb9UUunqb37tNiKthnLM+wLlXQNponmzyoEsooRCyzxjcjyCrdmRogwoswt+hOmdV\nl3dnvxYwo8SHUq9P4HCGk9QGuvpmszGtV3kCxfouT1kT70ZUPyLZclax/HDCDnq2\n4OMBln4pQk/EGsMLhzbQeQNbpOR2C5b4GEwPbjGExHJde3GBneJxQGSXJq+7Nrsx\nELtZgoKFa46dB+vm0NBz7v8JiRAxiRS/a0xvEQMXrYOf2UhhjxnBfVxGfNyxbEA6\nCIoK8pHWeua+OuzJHQFlAoIBAQDW0vKh3nfMxKV/LuJPJ4HgHB9Ld9xD5QQnpuUn\nVi91hBGgcRukWGUhsypBln41ChuI9O3ZrQApuNM7+2on5SLElwef+65G/W2IbHPb\nUKGpgcxyyPv7/qvQtWGV5U+OIdmxXlNQrruc6Neq8XhlEyHwPDpwQBKfbB8gzfOz\n3jQ6BOFXR+t3Nx+lwErXPRxWTN+7ccb6rOkCF4m2LJwdf7tjAvhHjtuUMASjaIt4\na/QXERuOSvUbp5LcZA9F/YgKEs+yV0AtwyjTnWfzl14rH8oh0AccDNDNX//z+wXv\nYwohdnQyX2mwfxJ3IMSBkAXlikc89PLgxapx8RjSsBcnOH4HAoIBAQC5nbiAjAHg\nJ5AYj8t/sDZnhTvsiLsi4Vo/2/Unrx599a9ChQSTv6gVMVweKtWI43TK7Ez8tsfQ\ne9psN6c9XCE6mc9S6KsQhEgbSict6hP/ayPeb+TWHpUju5qCw6PuXWy+rwZKbgza\nFtMTp4QbgJcCY4WF6YOxNRSTjlTQw0nOMc/VsqGdVDqkVeZ+A0bMlz2tXIXvJiIV\nUBgjgdt+JQnGZplS12ooL8V+rLoHDcUwKH3oeh9ON1HNe3vb7dAlyR+HsC1Mm4Pn\nfB4Xc86vMYX/5OVLwuSdN3VpsBPnDGmkH8+U5pwB+FhiIeS0947PDJwVsaiZXrwp\nRxzLvbPl5ZqjAoIBAQCd887Y+9VEJ1a0NAnMP3U8DhFokQHQngQ3D3ywNquAkZHQ\nUToM1b3OUIkCXp//aaYjRkvYYF6dTrtqAArmuJCe0ZmWpRxYMCCoTW3GVPv4wWpM\n/8BfYbp9I9BTwZ6EGBmTU5KY4VErJvzkQNXQI4gxtmcVf9bxhzNAEI5es0PdYRc6\n8LOOHWbUnZWputIqFi3vCdJPIHHWyu3Dl/tVqURjoZxiKQUEaWYPrF/YNC/uAfMr\n5athIQ5Xo+6i/K5ZEcnLDGIxA6zyI2t6bNKdjKs3v1hq5HVmfG6auvh7MmwRfKIl\nI4h3cIdoNhymUvoy80A77rLiWBRh4O7qgvUTLnNjAoIBAAiDgXjz8woTBnr57X2X\n2Yb6B3ub8elxqLARKLd/QsjIQhes/j7ApbcDIpSHpm+27x53pDhbMeMQKz6XduZL\nmYKUl3vYDDCfwKbvychDWlN22JhVTYu8r16KNlYVHynJwzkj0ggL8C74qQnXvyl7\nxnFnmzI/ObkhFCaIer9wlawNgNjubpdGy8HJ5t6Uy+SKc1vGSKZle16648CNLkIk\n9MPS5Ol10/qv5kEfLxEvwoGo+c11/IWb5/ai2VWHHOr+xKF2pT1ETNKLUN4Gg85p\nWRoZp6LH97B2YL5OQztvyFCs3NqZkUJN38/wegsK59P7YhVkprUSMVM7XcjClMPQ\nuj0CggEBAJVS6a3cxR5b05tVUrRw31MSkdj21JRLf3DUtkPY711+vJBZXCEjZU6c\nj2D4wMMFNqD4tc3D2C+ro6L/iyHtyUl2LC9h+Pfa/7UaFsqZI2XeyPUiyzkoWYZJ\n7KNuUefNxbhnJFM3kwW9Aga03SPhYiS7OiLzQzhSaiFd6WX4GsmNrKUUt12rp0NE\ndoSLqqmhUazwC97t8X/gUC7x54zM4WY4665ljQ8tP18/0pSPw0R+gutclygeK8Lz\nou9j96INWlOgMdFZXbvf5rXe79hFA4fOU64LdxSIXOD530wNOB0hZTuDTJWbho6P\na+jChGwRN78gwuxmniJraA5Hl6aaSyg=\n-----END PRIVATE KEY-----" + }, + "lxdRejectUnauthorized": false, + "jupyterPort": 8888 } \ No newline at end of file diff --git a/packages/itmat-job-executor/express-user.d.ts b/packages/itmat-job-executor/express-user.d.ts new file mode 100644 index 000000000..2fff509fe --- /dev/null +++ b/packages/itmat-job-executor/express-user.d.ts @@ -0,0 +1,27 @@ +import type { IUserWithoutToken } from '@itmat-broker/itmat-types'; + +declare global { + + namespace Express { + + // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type + interface User extends IUserWithoutToken { } + + interface MulterFile { + fieldname: string; + originalname: string; + encoding: string; + mimetype: string; + buffer: Buffer; + size: number; + } + + interface Request { + user?: User; + files?: MulterFile[]; + headers: IncomingHttpHeaders; + login(user: User, done: (err: unknown) => void): void; + logout(done: (err: unknown) => void): void; + } + } +} \ No newline at end of file diff --git a/packages/itmat-job-executor/src/database/database.ts b/packages/itmat-job-executor/src/database/database.ts index a2b9938ab..74eabf00e 100644 --- a/packages/itmat-job-executor/src/database/database.ts +++ b/packages/itmat-job-executor/src/database/database.ts @@ -1,25 +1,64 @@ -import type { IFile, IJobEntry, IProject, IQueryEntry, IField, IData } from '@itmat-broker/itmat-types'; +// import { Database as DatabaseBase, IDatabaseBaseConfig } from '@itmat-broker/itmat-commons'; +// import { IDatabaseCollectionConfig } from '@itmat-broker/itmat-cores'; + + +import type { IField, IFile, IJob, ILog, IOrganisation, IProject, IPubkey, IQueryEntry, IRole, IStudy, IUser, IStandardization, IConfig, IData, IDrive, ICache, IDomain, IOntologyTree, IBase, IWebAuthn, IInstance} from '@itmat-broker/itmat-types'; import { Database as DatabaseBase, IDatabaseBaseConfig } from '@itmat-broker/itmat-commons'; import type { Collection } from 'mongodb'; export interface IDatabaseConfig extends IDatabaseBaseConfig { collections: { + users_collection: string, jobs_collection: string, + studies_collection: string, + projects_collection: string, + queries_collection: string, field_dictionary_collection: string, + roles_collection: string, files_collection: string, + organisations_collection: string, + log_collection: string, + pubkeys_collection: string, data_collection: string, - queries_collection: string, - projects_collection: string + standardizations_collection: string, + configs_collection: string, + ontologies_collection: string, + drives_collection: string, + colddata_collection: string, + cache_collection: string, + domains_collection: string, + doc_collection: string, + webauthn_collection: string, + instance_collection: string }; } export interface IDatabaseCollectionConfig { - jobs_collection: Collection, + users_collection: Collection, + jobs_collection: Collection, + studies_collection: Collection, + projects_collection: Collection, + queries_collection: Collection, field_dictionary_collection: Collection, + roles_collection: Collection, files_collection: Collection, + organisations_collection: Collection, + log_collection: Collection, + pubkeys_collection: Collection, data_collection: Collection, - queries_collection: Collection, - projects_collection: Collection + standardizations_collection: Collection, + configs_collection: Collection, + ontologies_collection: Collection, + drives_collection: Collection, + colddata_collection: Collection, + cache_collection: Collection, + domains_collection: Collection, + // TODO: Implemet doc feature + docs_collection: Collection, + webauthn_collection: Collection, + instance_collection: Collection } +// export type DBType = DatabaseBase; + export const db = new DatabaseBase(); diff --git a/packages/itmat-job-executor/src/emailer/emailer.ts b/packages/itmat-job-executor/src/emailer/emailer.ts new file mode 100644 index 000000000..7c0782d8a --- /dev/null +++ b/packages/itmat-job-executor/src/emailer/emailer.ts @@ -0,0 +1,4 @@ +import appConfig from '../utils/configManager'; +import { Mailer } from '@itmat-broker/itmat-commons'; + +export const mailer = new Mailer(appConfig.nodemailer); diff --git a/packages/itmat-job-executor/src/jobDispatch/dispatcher.ts b/packages/itmat-job-executor/src/jobDispatch/dispatcher.ts index f939e36da..eb5ab2dbe 100644 --- a/packages/itmat-job-executor/src/jobDispatch/dispatcher.ts +++ b/packages/itmat-job-executor/src/jobDispatch/dispatcher.ts @@ -1,29 +1,53 @@ -import { IJobEntry } from '@itmat-broker/itmat-types'; +import { IJob, enumJobType, IJobActionReturn} from '@itmat-broker/itmat-types'; import { JobHandler } from '../jobHandlers/jobHandlerInterface'; +import { Logger } from '@itmat-broker/itmat-commons'; + export class JobDispatcher { private _handlerCollection: { - [jobType: string]: () => Promise + [jobType in enumJobType]?: () => Promise + }; + private _handlerInstances: { + [jobType in enumJobType]?: JobHandler }; constructor() { this.dispatch = this.dispatch.bind(this); this._handlerCollection = {}; + this._handlerInstances = {}; } - public registerJobType(jobType: string, getHandlerInstanceFunction: () => Promise): void { + public registerJobType(jobType: enumJobType, getHandlerInstanceFunction: () => Promise): void { this._handlerCollection[jobType] = getHandlerInstanceFunction; } - public removeHandler(jobType: string): void { + public removeHandler(jobType: enumJobType): void { delete this._handlerCollection[jobType]; + delete this._handlerInstances[jobType]; } - public async dispatch(job: IJobEntry): Promise { - if (!this._handlerCollection[job.jobType]) { + public async dispatch(job: IJob): Promise { + if (!this._handlerCollection[job.type]) { //TODO set job to UNPROCESSED - return; + Logger.error(`No JobHandler for job ${job.type}`); + throw Error(`No JobHandler for job ${job.type}`); + } + + let handler = this._handlerInstances[job.type]; + if (!handler) { + const handlerFactory = this._handlerCollection[job.type]; + if (handlerFactory) { + handler = await handlerFactory(); + this._handlerInstances[job.type] = handler; + } else { + throw new Error(`No JobHandler for job ${job.type}`); + } + } + + try { + return await handler.execute(job); + } catch (error) { + return { successful: false, error }; } - await (await this._handlerCollection[job.jobType]()).execute(job); } -} +} \ No newline at end of file diff --git a/packages/itmat-job-executor/src/jobExecutorRunner.ts b/packages/itmat-job-executor/src/jobExecutorRunner.ts index e69d40c4b..d0ad179f2 100644 --- a/packages/itmat-job-executor/src/jobExecutorRunner.ts +++ b/packages/itmat-job-executor/src/jobExecutorRunner.ts @@ -1,13 +1,16 @@ // External node module imports import { v4 as uuid } from 'uuid'; -import { db } from './database/database'; import { objStore } from './objStore/objStore'; import { Router } from './server/router'; import { Runner } from './server/server'; import { JobPoller } from '@itmat-broker/itmat-commons'; import { JobDispatcher } from './jobDispatch/dispatcher'; import { MongoClient } from 'mongodb'; -import { QueryHandler } from './query/queryHandler'; +import { APIHandler } from './jobHandlers/apiJobHandler'; +import { defaultSettings, enumJobType} from '@itmat-broker/itmat-types'; +import { LXDJobHandler} from './jobHandlers/lxdJobHandler'; +import { LXDMonitorHandler} from './jobHandlers/lxdMonitorHandler'; +import {db} from './database/database'; class ITMATJobExecutorRunner extends Runner { @@ -29,19 +32,23 @@ class ITMATJobExecutorRunner extends Runner { db.connect(this.config.database, MongoClient) .then(async () => objStore.connect(this.config.objectStore)) .then(() => { - _this.router = new Router(); - const jobDispatcher = new JobDispatcher(); /* TO_DO: can we figure out the files at runtime and import at runtime */ - jobDispatcher.registerJobType('QUERY_EXECUTION', QueryHandler.prototype.getInstance.bind(QueryHandler)); + // jobDispatcher.registerJobType('QUERY_EXECUTION', QueryHandler.prototype.getInstance.bind(QueryHandler)); + jobDispatcher.registerJobType(enumJobType.DMPAPI, APIHandler.getInstance.bind(APIHandler)); + // register the lxd jobhandler, bind the instance collection to LXDJobHandler + jobDispatcher.registerJobType(enumJobType.LXD, LXDJobHandler.getInstance.bind(LXDJobHandler)); + // Register the LXD monitor handler + jobDispatcher.registerJobType(enumJobType.LXD_MONITOR, LXDMonitorHandler.getInstance.bind(LXDMonitorHandler)); const poller = new JobPoller({ identity: uuid(), jobCollection: db.collections.jobs_collection, pollingInterval: this.config.pollingInterval, - action: jobDispatcher.dispatch + action: jobDispatcher.dispatch.bind(jobDispatcher), + jobSchedulerConfig: defaultSettings.systemConfig.jobSchedulerConfig }); poller.setInterval(); diff --git a/packages/itmat-job-executor/src/jobHandlers/apiJobHandler.ts b/packages/itmat-job-executor/src/jobHandlers/apiJobHandler.ts new file mode 100644 index 000000000..1f3802e0e --- /dev/null +++ b/packages/itmat-job-executor/src/jobHandlers/apiJobHandler.ts @@ -0,0 +1,55 @@ +import { UserCore, JobCore, InstanceCore, LxdManager } from '@itmat-broker/itmat-cores'; +import { mailer } from '../emailer/emailer'; +import configManager from '../utils/configManager'; +import { objStore } from '../objStore/objStore'; +import { JobHandler } from './jobHandlerInterface'; +import { IJob, IJobActionReturn } from '@itmat-broker/itmat-types'; +import {db} from '../database/database'; + +export class APIHandler extends JobHandler { + private static _instance: APIHandler; + public jobCore: JobCore; + public instanceCore: InstanceCore; + public lxdManager: LxdManager; + + constructor() { + super(); + this.jobCore = new JobCore(db); + this.instanceCore = new InstanceCore(db, mailer, configManager, this.jobCore, new UserCore(db, mailer, configManager, objStore)); + this.lxdManager = new LxdManager(configManager); + } + + public static override async getInstance(): Promise { + if (!APIHandler._instance) { + APIHandler._instance = new APIHandler(); + } + return APIHandler._instance; + } + + public async execute(document: IJob):Promise{ + try { + if (!document.executor) { + return { successful: false, error: 'No path found.' }; + } + const [className, methodName] = document.executor.path.split('.'); + const instance = this[className as keyof this] as unknown; + + if (!instance || typeof (instance as Record)[methodName] !== 'function') { + throw new Error(`Method ${methodName} not found on class ${className}`); + } + + const method = (instance as Record unknown>)[methodName]; + let parameters: unknown[] = []; + if (Array.isArray(document.parameters)) { + parameters = document.parameters; + } else if (document.parameters && typeof document.parameters === 'object') { + parameters = [document.parameters]; + } + + const result = await method(...parameters); + return { successful: true, result }; + } catch (e) { + return { successful: false, error: e instanceof Error ? e.message : JSON.stringify(e) }; + } + } +} \ No newline at end of file diff --git a/packages/itmat-job-executor/src/jobHandlers/jobHandlerInterface.ts b/packages/itmat-job-executor/src/jobHandlers/jobHandlerInterface.ts index 6e3eec11a..5517e5083 100644 --- a/packages/itmat-job-executor/src/jobHandlers/jobHandlerInterface.ts +++ b/packages/itmat-job-executor/src/jobHandlers/jobHandlerInterface.ts @@ -1,17 +1,19 @@ -import { IJobEntry } from '@itmat-broker/itmat-types'; +import { IJob, IJobActionReturn } from '@itmat-broker/itmat-types'; export abstract class JobHandler { /* subclass can decide either singleton (if there is expensive metadata collection) or just make this return new instance everytime. Gets called the first time when a job of the job type appears, so is lazily instantiated */ - public abstract getInstance(): Promise; + public static async getInstance(): Promise { + throw new Error('Subclass must implement static getInstance method.'); + } /* called by job dispatcher */ - public abstract execute(document: IJobEntry): Promise; + public abstract execute(document: IJob): Promise; } export type CodeRecords = Record> \ No newline at end of file +}>>; \ No newline at end of file diff --git a/packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts b/packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts new file mode 100644 index 000000000..321159d35 --- /dev/null +++ b/packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts @@ -0,0 +1,169 @@ +import { + IJob, + IInstance, + enumOpeType, + enumInstanceStatus, + LxdConfiguration +} from '@itmat-broker/itmat-types'; +import { APIHandler } from './apiJobHandler'; +import { Logger } from '@itmat-broker/itmat-commons'; +import type * as mongodb from 'mongodb'; +import { pollOperation } from './lxdPollOperation'; +import { db } from '../database/database'; + +export class LXDJobHandler extends APIHandler { + private static instance: LXDJobHandler; + private readonly instanceCollection: mongodb.Collection; + + private constructor() { + super(); + this.instanceCollection = db.collections.instance_collection; + } + + public static override async getInstance(): Promise { + if (!LXDJobHandler.instance) { + LXDJobHandler.instance = new LXDJobHandler(); + } + return LXDJobHandler.instance; + } + + public override async execute(document: IJob) { + const { operation, instanceId } = document.metadata as { operation: string; instanceId: string }; + + if (!operation || !instanceId) { + throw new Error('Missing required metadata: operation or instanceId.'); + } + + switch (operation) { + case enumOpeType.CREATE: + return this.create(document); + case enumOpeType.UPDATE: + return this.update(document); + case enumOpeType.START: + return this.startStopInstance(instanceId, operation); + case enumOpeType.STOP: + return this.startStopInstance(instanceId, operation); + case enumOpeType.DELETE: + return this.deleteInstance(instanceId); + default: + throw new Error('Unsupported operation.'); + } + } + + private async create(document: IJob){ + const metadata = document.metadata as { payload: LxdConfiguration, instanceId: string }; + const payload = metadata.payload ?? {}; + const instanceId = metadata.instanceId ?? ''; + + const instanceData = await this.instanceCollection.findOne({ id: instanceId }); + if (!instanceData) { + throw new Error('Instance not found.'); + } + const project = instanceData.project || 'default'; + + try { + const data = await this.lxdManager.createInstance(payload, project); + + if (data?.operation) { + await pollOperation(this.lxdManager, data.operation, project); + } + + await this.updateInstanceMetadata(instanceId, data, enumInstanceStatus.STOPPED); + return { successful: true, result: data }; + } catch (error) { + Logger.error(`Error creating instance: ${instanceId}, ${error}`); + await this.updateInstanceMetadata(instanceId, {}, enumInstanceStatus.FAILED); + return { successful: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + private async update(document: IJob) { + // const { instanceId, updates } = document.metadata ?? {}; + const { instanceId, updates } = document.metadata as { instanceId: string; updates: LxdConfiguration }; + const instance = await this.instanceCollection.findOne({ id: instanceId }); + + if (!instance) { + Logger.error(`LXD update: Instance not found.: ${instanceId} ${JSON.stringify(updates)}`); + return { successful: false, error: 'LXD update: Instance not found' }; + } + const project = instance.project || 'default'; + try { + const data = await this.lxdManager.updateInstance(instance.name, updates, project); + return { successful: true, result: data }; + } catch (error) { + Logger.error(`Error updating instance configuration: ${instanceId} - ${JSON.stringify(error)}`); + return { successful: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + private async startStopInstance(instanceId: string, action: enumOpeType.START | enumOpeType.STOP) { + const instanceData = await this.instanceCollection.findOne({ id: instanceId }); + if (!instanceData) { + return { successful: false, error: 'Instance not found.' }; + } + const project = instanceData.project || 'default'; + try { + const data = await this.lxdManager.startStopInstance(instanceData.name, action, project); + + if (data?.operation) { + await pollOperation(this.lxdManager, data.operation, project); + } + + const newStatus = action === enumOpeType.START ? enumInstanceStatus.RUNNING : enumInstanceStatus.STOPPED; + // use the this.instanceCore to update the instance status + await this.updateInstanceMetadata(instanceId, null, newStatus); + + return { successful: true, result: data }; + } catch (error) { + Logger.error(`Error in startStopInstance: ${error}`); + return { successful: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + private async deleteInstance(instanceId: string) { + const instanceData = await this.instanceCollection.findOne({ id: instanceId }); + if (!instanceData) { + return { successful: false, error: 'Instance not found.' }; + } + const project = instanceData.project || 'default'; + try { + const data = await this.lxdManager.deleteInstance(instanceData.name, project); + + if (data?.operation) { + await pollOperation(this.lxdManager, data.operation, project); + } + + await this.instanceCollection.deleteOne({ id: instanceId }); + return { successful: true, result: data }; + } catch (error) { + Logger.error(`[JOB] Failed to delete instance: ${error}`); + return { successful: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + private async updateInstanceMetadata( + instanceId: string, + metadata: Record | null, + status: enumInstanceStatus + ) { + const updateObject: mongodb.UpdateFilter = { + $set: { + status: status + } + }; + if (metadata !== null) { + updateObject.$set = { ...updateObject.$set, metadata }; + } + + try { + const updateResult = await this.instanceCollection.findOneAndUpdate({ id: instanceId }, updateObject); + if (!updateResult) { + Logger.error(`Failed to update instance ${instanceId}:`); + throw new Error(`Failed to update instance metadata: ${instanceId}`); + } + } catch (error) { + Logger.error(`Failed to update instance ${instanceId} error: ${error}`); + throw new Error(`Failed to update instance metadata: ${instanceId} ${error instanceof Error ? error.message : String(error)}`); + } + } +} \ No newline at end of file diff --git a/packages/itmat-job-executor/src/jobHandlers/lxdMonitorHandler.ts b/packages/itmat-job-executor/src/jobHandlers/lxdMonitorHandler.ts new file mode 100644 index 000000000..4ceeb4984 --- /dev/null +++ b/packages/itmat-job-executor/src/jobHandlers/lxdMonitorHandler.ts @@ -0,0 +1,97 @@ +import { + IJob, + IInstance, + enumInstanceStatus, + enumMonitorType, + LXDInstanceState, + IJobActionReturn +} from '@itmat-broker/itmat-types'; +import { APIHandler } from './apiJobHandler'; +import { Logger } from '@itmat-broker/itmat-commons'; +import type * as mongodb from 'mongodb'; +import { db } from '../database/database'; + +export class LXDMonitorHandler extends APIHandler { + private static instance: LXDMonitorHandler; + private readonly instanceCollection: mongodb.Collection; + + constructor() { + super(); + this.instanceCollection = db.collections.instance_collection; + } + + public static override async getInstance(): Promise { + if (!LXDMonitorHandler.instance) { + LXDMonitorHandler.instance = new LXDMonitorHandler(); + } + return LXDMonitorHandler.instance; + } + + public override async execute(document: IJob): Promise{ + const { operation, userId } = document.metadata as { operation: string; userId: string } ?? {}; + + if (!operation || !userId) { + return { successful: false, error: 'Missing required metadata: operation or userId.' }; + } + + try { + switch (operation) { + case enumMonitorType.STATE: + return await this.updateInstanceState(userId); + default: + return { successful: false, error: 'Unsupported operation.' }; + } + } catch (error) { + return { successful: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + private async updateInstanceState(userId: string) { + const instances = await this.instanceCollection.find({ userId }).toArray(); + + for (const instance of instances) { + try { + const project = instance.project || 'default'; + const response = await this.lxdManager.getInstanceState(instance.name, project); + + if (response.data) { + const instanceState = response.data as LXDInstanceState; + + await this.instanceCollection.updateOne( + { id: instance.id }, + { + $set: { + lxdState: instanceState, + status: this.determineInstanceStatus(instanceState) + } + } + ); + return { successful: true }; + } else { + Logger.error(`Failed to retrieve state for instance: ${instance.name}`); + return { successful: false, error: `Failed to retrieve state for instance: ${instance.name}` }; + } + } catch (error) { + Logger.error(`Error updating state for instance ${instance.name}: ${error}`); + return { successful: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + // Add a default return statement + return { successful: false, error: 'Unknown error occurred.' }; + } + + private determineInstanceStatus(state: LXDInstanceState): enumInstanceStatus { + switch (state.status) { + case 'Running': + return enumInstanceStatus.RUNNING; + case 'Stopped': + return enumInstanceStatus.STOPPED; + case 'Error': + return enumInstanceStatus.FAILED; + default: + Logger.error(`Unknown instance state: ${state.status}`); + return enumInstanceStatus.FAILED; + } + } +} \ No newline at end of file diff --git a/packages/itmat-job-executor/src/jobHandlers/lxdPollOperation.ts b/packages/itmat-job-executor/src/jobHandlers/lxdPollOperation.ts new file mode 100644 index 000000000..bac094b33 --- /dev/null +++ b/packages/itmat-job-executor/src/jobHandlers/lxdPollOperation.ts @@ -0,0 +1,62 @@ +import { Logger } from '@itmat-broker/itmat-commons'; +import { LxdManager } from '@itmat-broker/itmat-cores'; // Adjust the import path as necessary + +/** + * Polls an LXD operation until it is completed + * @param lxdManager The LxdManager instance + * @param operationUrl The URL of the operation to poll + * @param maxTry The maximum number of tries before giving up + * @returns A promise that resolves when the operation is completed + */ +export const pollOperation = async ( + lxdManager: LxdManager, + operationUrl: string, + project: string, + maxTry = 100 +): Promise => { + let tryCount = 0; + return new Promise((resolve, reject) => { + const operationIdMatch = operationUrl.match(/\/1\.0\/operations\/([^/]+)/); + if (!operationIdMatch) { + reject(new Error('Invalid operation URL')); + return; + } + const operationId = operationIdMatch[1]; + + const interval = setInterval(() => { + tryCount++; + if (tryCount > maxTry) { + clearInterval(interval); + reject(new Error(`Operation polling timed out: ${operationUrl} -> ${operationId}`)); + return; + } + void (async () => { + try { + const opData = await lxdManager.getOperationStatus(`/1.0/operations/${operationId}?project=${project}`); + const operationStatus = opData.metadata.status; + + if (operationStatus === 'Success') { + clearInterval(interval); + resolve(); // Operation succeeded + } else if (operationStatus === 'Failure') { + if (opData.metadata.err.includes('Instance is busy running')) { + return; + } else { + clearInterval(interval); + reject(new Error(`Operation failed for ${opData.metadata.err}`)); + } + } else if (operationStatus === 'Running') { + return; + } else { + clearInterval(interval); + reject(new Error(`Unknown operation status: ${operationStatus}`)); + } + } catch (error) { + Logger.error(`Error polling operation: ${error}`); + clearInterval(interval); + reject(new Error(`Fatal error polling operation: ${error instanceof Error ? error.message : String(error)}`)); + } + })(); + }, 4000); + }); +}; \ No newline at end of file diff --git a/packages/itmat-job-executor/src/query/pipeLineGenerator.ts b/packages/itmat-job-executor/src/query/pipeLineGenerator.ts deleted file mode 100644 index 2e24b7704..000000000 --- a/packages/itmat-job-executor/src/query/pipeLineGenerator.ts +++ /dev/null @@ -1,268 +0,0 @@ -// /** -// * @fn QueryHelper -// * @desc Helper to manipulate and query the MongoDB database storing UK biobank data formatted in the -// * white paper format -// * @param config -// * @constructor -// */ - -import { ICohortSelection, IEquationDescription, IQueryString, IStudyDataVersion } from '@itmat-broker/itmat-types'; -import { Filter } from 'mongodb'; - -type MongoAggregationOperand = MongoAggregationExpression | string | number; - -type MongoAggregationExpression = - | { $multiply: [MongoAggregationOperand, MongoAggregationOperand] } - | { $add: [MongoAggregationOperand, MongoAggregationOperand] } - | { $subtract: [MongoAggregationOperand, MongoAggregationOperand] } - | { $divide: [MongoAggregationOperand, MongoAggregationOperand] } - | { $pow: [string, number] } // MongoDB expects field name and a number for $pow - | number - | string; - -interface AddFields { - [key: string]: MongoAggregationExpression; -} - - -class PipelineGenerator { - constructor(private readonly config = {}) { } - - /* - * @fn buildPipeline - * @desc Methods that builds thee pipeline for the mongo query - * @param query - * structure of the request - * { - "new_fields": [ - { - "name": String // Name of the new field - "value": String // equation defining the derived feature - "op": String // Derived only - } - ], - "cohort": [ - [{ - "field": String, // Field identifier or field in the form X.X for a count - "value": String Or Float, // Value requested can either categorical - "op": String // Logical operation - }, - { - "field": String, // Field identifier or field in the form X.X for a count - "value": String Or Float, // Value requested can either categorical - "op": String // Logical operation - } - ], - [ - { - "field": String, // Field identifier or field in the form X.X for a count - "value": String Or Float, // Value requested can either categorical - "op": String // Logical operation - } - ]], - "data_requested": [ "field1", "field2", "field3"] // Fields requested - } - } - */ - public buildPipeline(query: IQueryString, studyId: string, availableDataVersions: Array) { - // check query, then decide whether to parse the query - if (query['data_requested'] === undefined || query['cohort'] === undefined || query['new_fields'] === undefined) { - return null; - } - if (Array.isArray(query['data_requested']) === false || Array.isArray(query['cohort']) === false || Array.isArray(query['new_fields']) === false) { - return null; - } - - const fields: { - _id: 0; - m_subjectId: 1; - m_visitId: 1; - [key: string]: 1 | 0 - } = { _id: 0, m_subjectId: 1, m_visitId: 1 }; - // We send back the requested fields - query.data_requested.forEach((field) => { - fields[field] = 1; - }); - const addFields: AddFields = {}; - // We send back the newly created derived fields by default - if (query.new_fields.length > 0) { - query.new_fields.forEach((field) => { - if (field.op === 'derived') { - fields[field.name] = 1; - const newField = this._createNewField(field.value); - if (Object.keys(newField).length !== 0) { - addFields[field.name] = newField as MongoAggregationExpression; - } - } else { - return 'Error'; - } - }); - } - let match = {}; - if (query.cohort.length > 1) { - const subqueries: Filter[] = []; - query.cohort.forEach((subcohort) => { - // addFields. - subqueries.push(this._translateCohort(subcohort)); - }); - match = { $or: subqueries }; - } else { - match = this._translateCohort(query.cohort[0]); - } - let dataVersionsFilter; - if (availableDataVersions == null) { - dataVersionsFilter = null; - } else { - dataVersionsFilter = { $in: availableDataVersions }; - } - if (this._isEmptyObject(addFields)) { - return [ - { $match: { m_studyId: studyId } }, - { $match: match }, - { $match: { m_versionId: dataVersionsFilter } }, - { $project: fields } - ]; - } else { - return [ - { $match: { m_studyId: studyId } }, - { $addFields: addFields }, - { $match: match }, - { $match: { m_versionId: dataVersionsFilter } }, - { $project: fields } - ]; - } - } - - - /** - * @fn _createNewdField - * @desc Creates the new fields required to compare when using an expresion like BMI or an average - * expression = { - * "left": json for nested or string for field id or Float, - * "right": json for nested or string for field id or Float, - * "op": String // Logical operation - * @param expression - * @return json formated in the mongo format in the pipeline stage addfield - * @private - */ - private _createNewField(expression: IEquationDescription) { - let newField = {}; - - if (typeof expression.left === 'object' && typeof expression.right === 'object') { - if (expression.op === '*') { - newField = { - $multiply: [this._createNewField(expression.left), this._createNewField(expression.right)] - }; - } else if (expression.op === '/') { - newField = { - $divide: [this._createNewField(expression.left), this._createNewField(expression.right)] - }; - } else if (expression.op === '-') { - newField = { - $subtract: [this._createNewField(expression.left), this._createNewField(expression.right)] - }; - } else if (expression.op === '+') { - newField = { - $add: [this._createNewField(expression.left), this._createNewField(expression.right)] - }; - } - } else if (typeof expression.left === 'object' && typeof expression.right === 'string') { - if (expression.op === '^') { - newField = { - $pow: ['$' + expression.left, parseInt(expression.right, 10)] - }; - } - } else if (typeof expression.left === 'string') { - if (expression.op === 'val') { - newField = parseFloat(expression.left); - } else if (expression.op === 'field') { - newField = '$' + expression.left; - } - } - - return newField; - } - - /** - * @fn _isEmptyObject - * @desc tests if an object is empty - * @param obj - * @returns {boolean} - * @private - */ - private _isEmptyObject(obj: unknown) { - return obj ? !Object.keys(obj).length : true; - } - - /** - * @fn _translateCohort - * @desc Tranforms a query into a mongo query. - * @param cohorts - * @private - */ - private _translateCohort(cohorts: ICohortSelection[]) { - const match: Record = {}; - cohorts.forEach(function (select) { - - switch (select.op) { - case '=': - // select.value must be an array - match[select.field] = { $in: [select.value] }; - break; - case '!=': - // select.value must be an array - match[select.field] = { $ne: [select.value] }; - break; - case '<': - // select.value must be a float - match[select.field] = { $lt: parseFloat(select.value) }; - break; - case '>': - // select.value must be a float - match[select.field] = { $gt: parseFloat(select.value) }; - break; - case 'derived': { - // equation must only have + - * / - const derivedOperation = select.value.split(' '); - if (derivedOperation[0] === '=') { - match[select.field] = { $eq: parseFloat(select.value) }; - } - if (derivedOperation[0] === '>') { - match[select.field] = { $gt: parseFloat(select.value) }; - } - if (derivedOperation[0] === '<') { - match[select.field] = { $lt: parseFloat(select.value) }; - } - break; - } - case 'exists': - // We check if the field exists. This is to be used for checking if a patient - // has an image - match[select.field] = { $exists: true }; - break; - case 'count': { - // counts can only be positive. NB: > and < are inclusive e.g. < is <= - const countOperation = select.value.split(' '); - const countfield = select.field + '.count'; - if (countOperation[0] === '=') { - match[countfield] = { $eq: parseInt(countOperation[1], 10) }; - } - if (countOperation[0] === '>') { - match[countfield] = { $gt: parseInt(countOperation[1], 10) }; - } - if (countOperation[0] === '<') { - match[countfield] = { $lt: parseInt(countOperation[1], 10) }; - } - break; - } - default: - break; - } - } - ); - return match; - } -} - - -export const pipelineGenerator = Object.freeze(new PipelineGenerator()); diff --git a/packages/itmat-job-executor/src/query/queryController.ts b/packages/itmat-job-executor/src/query/queryController.ts deleted file mode 100644 index 0fcf29628..000000000 --- a/packages/itmat-job-executor/src/query/queryController.ts +++ /dev/null @@ -1,88 +0,0 @@ -// TEST QUERY example -// -// const query_test = { -// id : '474b9676-8b2c-4983-9dbe-a6ba84370288', -// queryString : '{"31.0.0":"Male"}', -// study : null, -// requester : 'admin', -// status : 'PROCESSING', -// error : null, -// cancelled : false, -// lastClaimed : 1542288352356.0, -// queryResult : {}, -// data_requested: [ '31.0.0', '102.0.1', '102.0.2'], -// cohort: [ -// [{ field: '102.0', -// value: '> 1', -// op: 'count'}, -// { field: '31.0.0', -// value: 'Male', -// op: '='}], -// [{ field: '31.0.0', -// value: 'Female', -// op: '='}, -// { field: '102.0.1', -// value: '', -// op: 'exists'}] -// ], -// new_fields: [ -// { -// name: 'BMI', -// value: { -// left: { -// left: '12143.0.0', -// right: '', -// op: 'field' -// }, -// right: { -// left: '12144.0.0', -// right: '2', -// op: '^' -// }, -// op: '/' -// }, -// op: 'derived' -// } -// ] -// }; - -/// ** -// * @fn getStatus -// * @desc HTTP method GET handler on this service status -// * @param req Incoming message -// * @param res Server Response -// */ -// QueryController.prototype.processQuery = function(req, res) { -// let _this = this; -// let queryId = req.body.query_id; -// try { -// _this._queryCollection.findOne({id : queryId}).then(function(query){ -// -// let pipeline = _this._queryHelper.buildPipeline(query); -// -// if(pipeline === 'Error'){ -// res.status(500); -// res.json('Error while building the pipeline: ' + query); -// } -// -// _this._dataCollection.aggregate(pipeline).toArray().then(function (results) { -// res.status(200); -// res.json(results); -// },function(error){ -// res.status(500).json(error + ' ' + error.toString()); -// -// }); -// },function(error){ -// res.status(401); -// res.json('Error while processing the query ' + queryId + error.toString()); -// }); -// } -// catch (error) { -// res.status(510); -// res.json('Error occurred', error); -// } -// }; -// -// -// module.exports = QueryController; -// diff --git a/packages/itmat-job-executor/src/query/queryHandler.ts b/packages/itmat-job-executor/src/query/queryHandler.ts deleted file mode 100644 index 34ac3e1fc..000000000 --- a/packages/itmat-job-executor/src/query/queryHandler.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { IJobEntry } from '@itmat-broker/itmat-types'; -import { Logger } from '@itmat-broker/itmat-commons'; -import { db } from '../database/database'; -import { pipelineGenerator } from './pipeLineGenerator'; -import { JobHandler } from '../jobHandlers/jobHandlerInterface'; - -export class QueryHandler extends JobHandler { - private _instance?: QueryHandler; - // private ukbCurator: UKBCurator; - - public async getInstance() { - if (!this._instance) { - this._instance = new QueryHandler(); - } - return this._instance; - } - - public async execute(job: IJobEntry<{ queryId: string[], projectId: string, studyId: string }>) { - // get available data versions - const queryId = job.data?.queryId[0]; - const thisProject = await db.collections.projects_collection.findOne({ id: job.data?.projectId }); - if (!thisProject) { - await db.collections.queries_collection.findOneAndUpdate({ queryId }, { - $set: { - error: 'Project does not exist.', - status: 'FINISHED WITH ERROR' - } - }); - return; - } - const availableDataVersions = [thisProject.dataVersion]; - const query = await db.collections.queries_collection.findOne({ id: queryId }); - if (!query) { - await db.collections.queries_collection.findOneAndUpdate({ queryId }, { - $set: { - error: 'Query does not exist.', - status: 'FINISHED WITH ERROR' - } - }); - return; - } - const pipeline = pipelineGenerator.buildPipeline(query.queryString, job.studyId, availableDataVersions); - if (!pipeline) { - /* update query status */ - await db.collections.queries_collection.findOneAndUpdate({ queryId }, { - $set: { - error: 'Pipeline was not generated properly', - status: 'FINISHED WITH ERROR' - } - }); - return; - } - try { - const result = await db.collections.data_collection.aggregate(pipeline).toArray(); - /* if the query is on a project, then we need to map the results m_eid */ - if (job.projectId) { - const project = await db.collections.projects_collection.findOne({ id: job.data?.projectId }); - if (project === null || project === undefined) { - await db.collections.queries_collection.findOneAndUpdate({ queryId }, { - $set: { - error: 'Project does not exist or has been deleted.', - status: 'FINISHED WITH ERROR' - } - }); - return; - } - const mapping = project.patientMapping; - result.forEach((el) => { - if (el.m_eid === undefined) { return; } - el.m_eid = mapping[el.m_eid]; - }); - } - await db.collections.queries_collection.findOneAndUpdate({ id: queryId }, { - $set: { - queryResult: JSON.stringify(result), - status: 'FINISHED' - } - }); - await db.collections.jobs_collection.updateOne({ id: job.id }, { $set: { status: 'FINISHED' } }); - return; - } catch (e: unknown) { - - /* log */ - Logger.error(e); - - /* update query status */ - await db.collections.queries_collection.findOneAndUpdate({ queryId }, { - $set: { - error: e?.toString?.(), - status: 'FINISHED WITH ERROR' - } - }); - return; - } - } - - -} diff --git a/packages/itmat-job-executor/src/server/server.ts b/packages/itmat-job-executor/src/server/server.ts index a7f61751d..491fc4f21 100644 --- a/packages/itmat-job-executor/src/server/server.ts +++ b/packages/itmat-job-executor/src/server/server.ts @@ -1,16 +1,16 @@ -import { IServerBaseConfig, ServerBase } from '@itmat-broker/itmat-commons'; -import { IConfiguration } from '../utils/configManager'; +import { ServerBase, Logger, CustomError} from '@itmat-broker/itmat-commons'; - -export interface IServerConfig extends IServerBaseConfig { - pollingInterval: number; -} +import {IServerConfig, IConfiguration} from '../utils/configManager'; export class Runner extends ServerBase { - constructor(protected config: IConfiguration) { + constructor(protected override config: IConfiguration) { super(config); } protected async additionalChecksAndActions(): Promise { - return; + if (isNaN(parseInt(`${this.config.bcrypt.saltround}`, 10))) { + Logger.log(new CustomError('Salt round must be a number')); + process.exit(1); + } } } + diff --git a/packages/itmat-job-executor/src/utils/configManager.ts b/packages/itmat-job-executor/src/utils/configManager.ts index e23400489..43959d810 100644 --- a/packages/itmat-job-executor/src/utils/configManager.ts +++ b/packages/itmat-job-executor/src/utils/configManager.ts @@ -3,14 +3,40 @@ import fs from 'fs-extra'; import path from 'path'; import { IObjectStoreConfig, IDatabaseBaseConfig, Logger } from '@itmat-broker/itmat-commons'; import configDefaults from '../../config/config.sample.json'; -import { IServerConfig } from '../server/server.js'; +import chalk from 'chalk'; +import SMTPTransport from 'nodemailer/lib/smtp-transport'; + +import {IServerBaseConfig} from '@itmat-broker/itmat-commons'; + +export interface IServerConfig extends IServerBaseConfig { + bcrypt: { + saltround: number, + }; + pollingInterval: number; +} + export interface IConfiguration extends IServerConfig { + appName: string; database: IDatabaseBaseConfig; objectStore: IObjectStoreConfig; + nodemailer: SMTPTransport.Options & { auth: { user: string, pass: string } }; + aesSecret: string; + sessionsSecret: string; + adminEmail: string; + aeEndpoint: string; + useWebdav: boolean; + webdavPort: number; + lxdEndpoint: string; + lxdStoragePool: string; + lxdProject: string; + webdavServer: string; + systemKey: Record; + lxdCertFile: Record; + lxdRejectUnauthorized: boolean; + jupyterPort: number; } - -class ConfigurationManager { +export class ConfigurationManager { public static expand(configurationFiles: string[]): IConfiguration { @@ -25,7 +51,7 @@ class ConfigurationManager { Logger.log(`Applied configuration from ${path.resolve(configurationFile)}.`); } } catch (e) { - Logger.error('Could not parse configuration file. ' + e); + Logger.error(chalk.red(`Cannot parse configuration file.${JSON.stringify(e)}`)); } }); @@ -34,4 +60,4 @@ class ConfigurationManager { } -export default ConfigurationManager.expand((process.env.NODE_ENV === 'development' ? [path.join(__dirname.replace('dist', ''), 'config/config.json')] : []).concat(['config/config.json'])); +export default ConfigurationManager.expand((process.env['NODE_ENV'] === 'development' ? [path.join(__dirname.replace('dist', ''), 'config/config.json')] : []).concat(['config/config.json'])); diff --git a/packages/itmat-job-executor/tsconfig.json b/packages/itmat-job-executor/tsconfig.json index 7f9a7d526..17edd9f00 100644 --- a/packages/itmat-job-executor/tsconfig.json +++ b/packages/itmat-job-executor/tsconfig.json @@ -1,5 +1,15 @@ { "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noImplicitAny": false, + "noFallthroughCasesInSwitch": true + }, "files": [], "include": [], "references": [ diff --git a/packages/itmat-setup/src/databaseSetup/collectionsAndIndexes.ts b/packages/itmat-setup/src/databaseSetup/collectionsAndIndexes.ts index 02f2a1b6d..9d287245e 100644 --- a/packages/itmat-setup/src/databaseSetup/collectionsAndIndexes.ts +++ b/packages/itmat-setup/src/databaseSetup/collectionsAndIndexes.ts @@ -141,6 +141,13 @@ const collections = { { key: { id: 1 }, unique: true }, { key: { userId: 1 }, unique: false } ] + }, + instance_collection: { + name: 'INSTANCE_COLLECTION', + indexes: [ + { key: { id: 1 }, unique: true }, + { key: { userId: 1 }, unique: false } + ] } }; diff --git a/packages/itmat-setup/src/databaseSetup/seed/config.ts b/packages/itmat-setup/src/databaseSetup/seed/config.ts index 46932ede7..c0acacba6 100644 --- a/packages/itmat-setup/src/databaseSetup/seed/config.ts +++ b/packages/itmat-setup/src/databaseSetup/seed/config.ts @@ -1,3 +1,9 @@ +enum enumInstanceType { + SMALL = 'small', + MEDIUM ='medium', + LARGE = 'large' +} + export const seedConfigs = [{ id: 'root_admin_user_config_protected', type: 'USERCONFIG', @@ -21,7 +27,15 @@ export const seedConfigs = [{ defaultLXDMaximumContainerCPUCores: 2, defaultLXDMaximumContainerDiskSize: 50 * 1024 * 1024 * 1024, defaultLXDMaximumContainerMemory: 8 * 1024 * 1024 * 1024, - defaultLXDMaximumContainerLife: 8 * 60 * 60 + defaultLXDMaximumContainerLife: 8 * 60 * 60, + // LXD instances + defaultLXDflavor: [enumInstanceType.SMALL], + defaultLXDMaximumInstances: 3, // number + defaultLXDMaximumInstanceCPUCores: 3 * 4, // number + defaultLXDMaximumInstanceDiskSize: 3 * 40 * 1024 * 1024 * 1024, + defaultLXDMaximumInstanceMemory: 3 * 16 * 1024 * 1024 * 1024, + // set to 360 hours + defaultLXDMaximumInstanceLife: 360 * 60 * 60 } }, { id: 'root_standard_user_config_protected', @@ -46,7 +60,16 @@ export const seedConfigs = [{ defaultLXDMaximumContainerCPUCores: 2, defaultLXDMaximumContainerDiskSize: 50 * 1024 * 1024 * 1024, defaultLXDMaximumContainerMemory: 8 * 1024 * 1024 * 1024, - defaultLXDMaximumContainerLife: 8 * 60 * 60 + defaultLXDMaximumContainerLife: 8 * 60 * 60, + + // LXD instances + defaultLXDflavor: [enumInstanceType.SMALL], + defaultLXDMaximumInstances: 3, // number + defaultLXDMaximumInstanceCPUCores: 3 * 4, // number + defaultLXDMaximumInstanceDiskSize: 3 * 40 * 1024 * 1024 * 1024, + defaultLXDMaximumInstanceMemory: 3 * 16 * 1024 * 1024 * 1024, + // set to 360 hours + defaultLXDMaximumInstanceLife: 360 * 60 * 60 } }, { id: 'root_system_config_protected', @@ -70,7 +93,12 @@ export const seedConfigs = [{ logoSize: ['24px', '24px'], archiveAddress: '', defaultEventTimeConsumptionBar: [50, 100], - defaultUserExpireDays: 90 + defaultUserExpireDays: 90, + defaultLXDFlavor: { + [enumInstanceType.SMALL]: { cpuLimit: 4, memoryLimit: 16 * 1024 * 1024 * 1024, diskLimit: 20 * 1024 * 1024 * 1024 }, + [enumInstanceType.MEDIUM]: { cpuLimit: 8, memoryLimit: 32 * 1024 * 1024 * 1024, diskLimit: 40 * 1024 * 1024 * 1024 }, + [enumInstanceType.LARGE]: { cpuLimit: 16, memoryLimit: 64 * 1024 * 1024 * 1024, diskLimit: 60 * 1024 * 1024 * 1024 } + } }, life: { createdTime: Date.now(), diff --git a/packages/itmat-types/src/types/config.ts b/packages/itmat-types/src/types/config.ts index 02d3bca03..e0916bb0c 100644 --- a/packages/itmat-types/src/types/config.ts +++ b/packages/itmat-types/src/types/config.ts @@ -1,6 +1,8 @@ import { IBase, ILifeCircle } from './base'; import { v4 as uuid } from 'uuid'; import { enumReservedUsers } from './user'; +import { IJobSchedulerConfig, enumJobSchedulerStrategy } from './job'; +import { enumInstanceType } from './instance'; export interface IConfig extends IBase { type: enumConfigType; @@ -27,6 +29,14 @@ export interface ISystemConfig extends IBase { archiveAddress: string; defaultEventTimeConsumptionBar: number[]; defaultUserExpireDays: number; + jobSchedulerConfig: IJobSchedulerConfig; + defaultLXDFlavor: { + [key in enumInstanceType]: { + cpuLimit: number; + memoryLimit: number; + diskLimit: number; + } + }; } export enum enumStudyBlockColumnValueType { @@ -57,12 +67,13 @@ export interface IUserConfig extends IBase { defaultFileBucketId: string; defaultMaximumQPS: number; - // LXD containers - defaultLXDMaximumContainers: number; - defaultLXDMaximumContainerCPUCores: number; - defaultLXDMaximumContainerDiskSize: number; - defaultLXDMaximumContainerMemory: number; - defaultLXDMaximumContainerLife: number; + // LXD instances + defaultLXDMaximumInstances: number; + defaultLXDMaximumInstanceCPUCores: number; + defaultLXDMaximumInstanceDiskSize: number; + defaultLXDMaximumInstanceMemory: number; + defaultLXDMaximumInstanceLife: number; + defaultLXDflavor: enumInstanceType[]; // e.g., ['small', 'medium'] } export interface IOrganisationConfig extends IBase { @@ -118,7 +129,20 @@ export class DefaultSettings implements IDefaultSettings { logoSize: ['24px', '24px'], archiveAddress: '', defaultEventTimeConsumptionBar: [50, 100], - defaultUserExpireDays: 90 + defaultUserExpireDays: 90, + jobSchedulerConfig: { + strategy: enumJobSchedulerStrategy.FIFO, + usePriority: true, + // for errored jobs + reExecuteFailedJobs: true, + failedJobDelayTime: 30 * 60 * 1000, // unit timestamps + maxAttempts: 10 // the number of attempts should be stored in history + }, + defaultLXDFlavor: { + [enumInstanceType.SMALL]: { cpuLimit: 4, memoryLimit: 16 * 1024 * 1024 * 1024, diskLimit: 20 * 1024 * 1024 * 1024 }, + [enumInstanceType.MEDIUM]: { cpuLimit: 8, memoryLimit: 32 * 1024 * 1024 * 1024, diskLimit: 40 * 1024 * 1024 * 1024 }, + [enumInstanceType.LARGE]: { cpuLimit: 16, memoryLimit: 64 * 1024 * 1024 * 1024, diskLimit: 60 * 1024 * 1024 * 1024 } + } }; public readonly studyConfig: IStudyConfig = { @@ -152,11 +176,14 @@ export class DefaultSettings implements IDefaultSettings { defaultMaximumRepoSize: 10 * 1024 * 1024 * 1024, // 10GB defaultFileBucketId: 'user', defaultMaximumQPS: 500, - defaultLXDMaximumContainers: 2, - defaultLXDMaximumContainerCPUCores: 2, - defaultLXDMaximumContainerDiskSize: 50 * 1024 * 1024 * 1024, - defaultLXDMaximumContainerMemory: 8 * 1024 * 1024 * 1024, - defaultLXDMaximumContainerLife: 8 * 60 * 60 + // LXD instances + defaultLXDflavor: [enumInstanceType.SMALL], + defaultLXDMaximumInstances: 3, // number + defaultLXDMaximumInstanceCPUCores: 3 * 4, // number + defaultLXDMaximumInstanceDiskSize: 3 * 40 * 1024 * 1024 * 1024, + defaultLXDMaximumInstanceMemory: 3 * 16 * 1024 * 1024 * 1024, + // set to 360 hours + defaultLXDMaximumInstanceLife: 360 * 60 * 60 // TODO, set to suitable hours }; public readonly docConfig: IDocConfig = { diff --git a/packages/itmat-types/src/types/index.ts b/packages/itmat-types/src/types/index.ts index 4d639924a..322047ef3 100644 --- a/packages/itmat-types/src/types/index.ts +++ b/packages/itmat-types/src/types/index.ts @@ -20,6 +20,8 @@ import * as Permission from './permission'; import * as Cache from './cache'; import * as Domain from './domain'; import * as WebAuthn from './webauthn'; +import * as Instance from './instance'; +import * as Lxd from './lxd'; export * from './field'; export * from './file'; @@ -43,5 +45,7 @@ export * from './permission'; export * from './cache'; export * from './domain'; export * from './webauthn'; +export * from './instance'; +export * from './lxd'; -export const Types = { File, Job, Log, User, Organisation, Pubkey, Study, Query, Field, Data, Standardization, Common, Base, CoreErrors, Config, ZodSchema, Utils, Drive, Permission, Cache, Domain, WebAuthn }; +export const Types = { File, Job, Log, User, Organisation, Pubkey, Study, Query, Field, Data, Standardization, Common, Base, CoreErrors, Config, ZodSchema, Utils, Drive, Permission, Cache, Domain, WebAuthn, Instance, Lxd}; diff --git a/packages/itmat-types/src/types/instance.ts b/packages/itmat-types/src/types/instance.ts new file mode 100644 index 000000000..4e24c9bc8 --- /dev/null +++ b/packages/itmat-types/src/types/instance.ts @@ -0,0 +1,57 @@ +import { IBase } from './base'; +import { IUser } from './user'; +import { LXDInstanceState, LXDInstanceTypeEnum} from './lxd'; + +export interface IInstance extends IBase{ + id: string; + name: string; + userId: IUser['id']; + username: IUser['username']; + status: enumInstanceStatus; + type: LXDInstanceTypeEnum; // virtual-machine' | 'container'; instance type VM or container + appType: enumAppType; // the application type, jupyter or matlab + createAt: number; // instance creation time + lifeSpan: number; // instance's life span, exp. 10 hours + instanceToken: string; // instance cert token + project?: string |'default'; // the lxd project of the this instance + webDavToken?: string; // webDav cert token + config: Record; + lxdState?: LXDInstanceState; // Optional field for LXD instance state details +} + +export enum enumInstanceStatus { + PENDING = 'PENDING', // first creation will be pending + FAILED = 'FAILED', + STARTING = 'STARTING', + STOPPING = 'STOPPING', + RUNNING = 'RUNNING', + STOPPED = 'STOPPED', + DELETED = 'DELETED', +} + +export enum enumAppType { + JUPYTER = 'Jupyter', + MATLAB = 'Matlab' +} + + +// operation for instance +export enum enumOpeType { + CREATE = 'create', + UPDATE = 'update', + STOP = 'stop', + START = 'start', + DELETE = 'delete' +} + +//monitor operation for instances +export enum enumMonitorType { + STATE = 'state', +} + +export enum enumInstanceType { + SMALL = 'small', + MEDIUM ='medium', + LARGE = 'large' +} + diff --git a/packages/itmat-types/src/types/job.ts b/packages/itmat-types/src/types/job.ts index d053784d4..f6c187177 100644 --- a/packages/itmat-types/src/types/job.ts +++ b/packages/itmat-types/src/types/job.ts @@ -1,21 +1,87 @@ -import type { ObjectId } from 'mongodb'; +import { IBase } from './base'; +import type * as mongodb from 'mongodb'; -export interface IJobEntry { - _id?: ObjectId; - jobType: string; - id: string; - projectId?: string; +export interface IJobData { + queryId: string[]; + projectId: string; studyId: string; - requester: string; - requestTime: number; - receivedFiles: string[]; - status: 'QUEUED' | 'PROCESSING' | 'FINISHED' | 'ERROR'; - error: null | Error | Error[] | Record | Record[]; - cancelled: boolean; - cancelledTime?: number; - claimedBy?: string; - lastClaimed?: number; - data?: dataobj; -} - -export type IJobEntryForQueryCuration = IJobEntry<{ queryId: string[], studyId: string }>; +} + +export interface IJob extends IBase { + name: string; + nextExecutionTime: number; // when creating jobs, set it to now for immediate executed jobs or a further time; do not be confused if this time is older than the current time. + period: number | null; // null for oneoff jobs + type: enumJobType; + executor: IExecutor | null; + data: IJobData | JSON | null; + parameters: JSON | null; + priority: number; + history: IJobHistory[]; // by default we will only keep the latest history + counter: number; + status: enumJobStatus; +} + +export interface IJobHistory { + time: number; + status: enumJobHistoryStatus; + errors: string[]; +} + +export enum enumJobType { + DMPAPI = 'DMPAPI', + AE = 'AE', + SYSTEMPROCESS = 'SYSTEMPROCESS', + LXD = 'LXD', + LXD_MONITOR = 'LXD_MONITOR' +} + +export enum enumJobStatus { + PENDING = 'PENDING', // oneoff jobs will always be PENDING + CANCELLED = 'CANCELLED', + FINISHED = 'FINISHED', // period should be null, + INUSE = 'INUSE', // for lxd containers + ERROR = 'ERROR' +} + +export enum enumJobHistoryStatus { + SUCCESS = 'SUCCESS', + FAILED = 'FAILED' +} + +export interface IExecutor { + id: string; + path: string; // for DMPAPI, use the trpc router path + type: string | null; +} + + +// Job Executor +export interface IJobPollerConfig { + identity: string; // a string identifying the server; this is just to keep track in mongo + jobType?: string; // if undefined, matches all jobs + jobCollection: mongodb.Collection; // collection to poll + pollingInterval: number; // in ms + action: (document: IJob) => Promise;// gets called every time there is new document, + jobSchedulerConfig: IJobSchedulerConfig +} + +// define the action return type +export interface IJobActionReturn { + successful: boolean; + result?: unknown; + error?: unknown; +} + +export interface IJobSchedulerConfig { + strategy: enumJobSchedulerStrategy, + usePriority: boolean, + // for errored jobs + reExecuteFailedJobs: boolean, + failedJobDelayTime: number, // unit timestamps + maxAttempts: number, // the number of attempts should be stored in history + jobCollection?: mongodb.Collection; // collection to poll +} + +export enum enumJobSchedulerStrategy { + FIFO = 'FIFO' +} \ No newline at end of file diff --git a/packages/itmat-types/src/types/lxd.ts b/packages/itmat-types/src/types/lxd.ts new file mode 100644 index 000000000..6fe8c1814 --- /dev/null +++ b/packages/itmat-types/src/types/lxd.ts @@ -0,0 +1,416 @@ +/** + * LXD related types + */ + +interface LxdInstanceUsageProp { + usage: number; +} + +interface LxdInstanceMemory { + swap_usage: number; + swap_usage_peak: number; + usage: number; + usage_peak: number; +} + +interface LxdInstanceNetworkAddress { + address: string; + family: string; + netmask: string; + scope: string; +} + +interface LxdInstanceNetworkCounters { + bytes_received: number; + bytes_sent: number; + errors_received: number; + errors_sent: number; + packets_dropped_inbound: number; + packets_dropped_outbound: number; + packets_received: number; + packets_sent: number; +} + +interface LxdInstanceNetwork { + addresses: LxdInstanceNetworkAddress[]; + counters: LxdInstanceNetworkCounters; + host_name: string; + hwaddr: string; + mtu: number; + state: 'up' | 'down'; + type: string; +} + +export interface LXDInstanceState { + cpu: LxdInstanceUsageProp; + disk: { + root: LxdInstanceUsageProp; + } & Record; + memory: LxdInstanceMemory; + network?: Record; + pid: number; + processes: number; + status: string; +} + +export enum LXDInstanceTypeEnum { + CONTAINER = 'container', + VIRTUAL_MACHINE = 'virtual-machine', +} + +export interface LXDInstanceType { + name: string; + description: string; + status: string; + statusCode: number; + profiles: string[]; + type: LXDInstanceTypeEnum; + architecture: string; + creationDate: string; + lastUsedDate: string; + username: string; + cpuLimit: string; + memoryLimit: string; + key: string; +} + +export interface LxdConfiguration { + name: string; + architecture: string; + config?: { + 'limits.cpu': string, + 'limits.memory': string, + 'user.username': string, // store username to instance config + 'user.user-data': string // set the cloud-init user-data + }; + source: { + type: string; + alias: string; + }; + profiles?: string[]; + type?: string; + project?: string; +} + +// resources.d.ts +export interface LxdResources { + cpu: Cpu; + gpu: Gpu; + memory: Memory; + network: Network; + pci: Pci; + storage: Storage; + system: System; + usb: Usb; + } + +export interface Cpu { + architecture: string; + sockets?: CpuSockets[] | null; + total: number; + } + +export interface CpuSockets { + cache?: Cache[] | null; + cores?: Cores[] | null; + frequency: number; + frequency_minimum: number; + frequency_turbo: number; + name: string; + socket: number; + vendor: string; + } + +export interface Cache { + level: number; + size: number; + type: string; + } + +export interface Cores { + core: number; + die: number; + frequency: number; + threads?: Threads[] | null; + } + +export interface Threads { + id: number; + isolated: boolean; + numa_node: number; + online: boolean; + thread: number; + } + +export interface Gpu { + cards?: GpuCards[] | null; + total: number; + } + +export interface GpuCards { + driver: string; + driver_version: string; + drm: Drm; + mdev?: null; + numa_node: number; + nvidia: Nvidia; + pci_address: string; + product: string; + product_id: string; + sriov: Sriov; + usb_address: string; + vendor: string; + vendor_id: string; + } + +export interface Drm { + card_device: string; + card_name: string; + control_device: string; + control_name: string; + id: number; + render_device: string; + render_name: string; + } + +export interface Nvidia { + architecture: string; + brand: string; + card_device: string; + card_name: string; + cuda_version: string; + model: string; + nvrm_version: string; + uuid: string; + } + +export interface Sriov { + current_vfs: number; + maximum_vfs: number; + vfs?: null; + } + +export interface Memory { + hugepages_size: number; + hugepages_total: number; + hugepages_used: number; + nodes?: null; + total: number; + used: number; + } + +export interface Network { + cards?: NetworkCards[] | null; + total: number; + } + +export interface NetworkCards { + driver: string; + driver_version: string; + firmware_version: string; + numa_node: number; + pci_address: string; + ports?: Ports[] | null; + product: string; + product_id: string; + sriov: Sriov; + usb_address: string; + vendor: string; + vendor_id: string; + } + +export interface Ports { + address: string; + auto_negotiation: boolean; + id: string; + infiniband: Infiniband; + link_detected: boolean; + link_duplex: string; + link_speed: number; + port: number; + port_type: string; + protocol: string; + supported_modes?: string[] | null; + supported_ports?: string[] | null; + transceiver_type: string; + } + +export interface Infiniband { + issm_device: string; + issm_name: string; + mad_device: string; + mad_name: string; + verb_device: string; + verb_name: string; + } + +export interface Pci { + devices?: PciDevices[] | null; + total: number; + } + +export interface PciDevices { + driver: string; + driver_version: string; + iommu_group: number; + numa_node: number; + pci_address: string; + product: string; + product_id: string; + vendor: string; + vendor_id: string; + vpd: Vpd; + } + +export interface Vpd { + entries: string; + product_name: string; + } + +export interface Storage { + disks?: Disks[] | null; + total: number; + } + +export interface Disks { + block_size: number; + device: string; + device_id: string; + device_path: string; + firmware_version: string; + id: string; + model: string; + numa_node: number; + partitions?: Partitions[] | null; + pci_address: string; + read_only: boolean; + removable: boolean; + rpm: number; + serial: string; + size: number; + type: string; + usb_address: string; + wwn: string; + } + +export interface Partitions { + device: string; + id: string; + partition: number; + read_only: boolean; + size: number; + } + +export interface System { + chassis: Chassis; + family: string; + firmware: Firmware; + motherboard: Motherboard; + product: string; + serial: string; + sku: string; + type: string; + uuid: string; + vendor: string; + version: string; + } + +export interface Chassis { + serial: string; + type: string; + vendor: string; + version: string; + } + +export interface Firmware { + date: string; + vendor: string; + version: string; + } + +export interface Motherboard { + product: string; + serial: string; + vendor: string; + version: string; + } + +export interface Usb { + devices?: UsbDevices[] | null; + total: number; + } + +export interface UsbDevices { + bus_address: number; + device_address: number; + interfaces?: Interfaces[] | null; + product: string; + product_id: string; + speed: number; + vendor: string; + vendor_id: string; + } + +export interface Interfaces { + class: string; + class_id: number; + driver: string; + driver_version: string; + number: number; + subclass: string; + subclass_id: number; + } + + +export interface LxdOperationMetadata { + command: string[]; + environment: { + HOME: string; + LANG: string; + PATH: string; + TERM: string; + USER: string; + }; + fds: { + [key: string]: string; + }; + interactive: boolean; +} + +export interface LxdOperation { + metadata: { + class: string; + created_at: string; + description: string; + err: string; + id: string; + location: string; + may_cancel: boolean; + metadata: LxdOperationMetadata; + resources: { + containers: string[]; + instances: string[]; + }; + status: string; + status_code: number; + updated_at: string; + }; + operation: string; + status: string; + status_code: number; + type: string; +} + +export interface LxdErrorResponse { + error: string; + error_code: number; + type: string; +} + +export type LxdGetInstanceConsoleResponse = { + operationId: string; + operationSecrets: { [key: string]: string }; +} | { + error: boolean; + data: string; +}; \ No newline at end of file diff --git a/packages/itmat-ui-react/proxy.conf.js b/packages/itmat-ui-react/proxy.conf.js index 51c55608f..6cd22138b 100644 --- a/packages/itmat-ui-react/proxy.conf.js +++ b/packages/itmat-ui-react/proxy.conf.js @@ -27,7 +27,7 @@ module.exports = [ changeOrigin: true }, { - context: ['/pun', '/node', '/rnode', '/public'], + context: ['/pun', '/node', '/rnode', '/public', '/rtc', '/jupyter'], target: API_SERVER, secure: false, changeOrigin: true, diff --git a/packages/itmat-ui-react/src/Fence.tsx b/packages/itmat-ui-react/src/Fence.tsx index 9a79ed8e9..e563e1795 100644 --- a/packages/itmat-ui-react/src/Fence.tsx +++ b/packages/itmat-ui-react/src/Fence.tsx @@ -41,7 +41,7 @@ export const Fence: FunctionComponent = () => { } else { setIsUserLogin(false); } - }, [whoAmI.data]); + }, [setIsUserLogin, whoAmI.data]); useEffect(() => { if (isAnyLoading) { diff --git a/packages/itmat-ui-react/src/components/drive/file.tsx b/packages/itmat-ui-react/src/components/drive/file.tsx index 9a3bdabbb..9c9ffe5bc 100644 --- a/packages/itmat-ui-react/src/components/drive/file.tsx +++ b/packages/itmat-ui-react/src/components/drive/file.tsx @@ -261,9 +261,22 @@ export const MyFile: FunctionComponent = () => { } trigger={['click']}> - e.stopPropagation()}> + {/* e.stopPropagation()}> - + */} + diff --git a/packages/itmat-ui-react/src/components/instance/index.tsx b/packages/itmat-ui-react/src/components/instance/index.tsx new file mode 100644 index 000000000..a636c5915 --- /dev/null +++ b/packages/itmat-ui-react/src/components/instance/index.tsx @@ -0,0 +1,8 @@ +import { FunctionComponent } from 'react'; +import { InstanceSection } from './instance'; // Assuming InstanceSection is the component we're about to create + +export const InstancePage: FunctionComponent = () => { + return ( + + ); +}; diff --git a/packages/itmat-ui-react/src/components/instance/instance.module.css b/packages/itmat-ui-react/src/components/instance/instance.module.css new file mode 100644 index 000000000..e7d52bc5d --- /dev/null +++ b/packages/itmat-ui-react/src/components/instance/instance.module.css @@ -0,0 +1,25 @@ +.page_container { + /* padding: 2rem; */ + padding: 20px; /* Adjust as needed */ + overflow: auto; /* Ensures that scrolling is possible */ + height: calc(100vh - 100px); +} + +.createButton { + margin-bottom: 1rem; +} + +.table { + margin-top: 1rem; +} + +/* Style for cards, adjust as needed */ +.cardContainer { + max-height: 300px; /* Example height */ + overflow: auto; /* Allows scrolling within the card */ + margin-bottom: 20px; /* Adds space between cards */ +} + +.marginBottom { + margin-bottom: 20px; /* Adjust the value as needed */ +} diff --git a/packages/itmat-ui-react/src/components/instance/instance.tsx b/packages/itmat-ui-react/src/components/instance/instance.tsx new file mode 100644 index 000000000..0b1d7d94e --- /dev/null +++ b/packages/itmat-ui-react/src/components/instance/instance.tsx @@ -0,0 +1,434 @@ +import React, { FunctionComponent, useState, useRef, useEffect} from 'react'; +import { Button, message, Modal, Form, Select, Card, Tag, Progress, Space, Row, Col } from 'antd'; +import { trpc } from '../../utils/trpc'; +import css from './instance.module.css'; +import { enumAppType,enumInstanceType, enumInstanceStatus, IInstance, LXDInstanceTypeEnum, enumOpeType, IUserConfig} from '@itmat-broker/itmat-types'; +import { LXDConsole , LXDConsoleRef} from '../lxd/lxd.instance.console'; +import { LXDTextConsole } from '../lxd/lxd.instance.text.console'; + + +const { Option } = Select; + +type CreateInstanceFormValues = { + name: string; + type: LXDInstanceTypeEnum; + appType: enumAppType; + instanceType: enumInstanceType; + lifeSpan: number; + project: string; + cpuLimit: number; + memoryLimit: string; + diskLimit: string; +}; + +type FlavorDetails = { + cpuLimit: number; + memoryLimit: string; + diskLimit: string; +}; + + +export const InstanceSection: FunctionComponent = () => { + + const [consoleModalOpen, setConsoleModalOpen] = useState(false); + const [selectedInstance, setSelectedInstance] = useState(null); + + const [selectedInstanceTypeDetails, setSelectedInstanceTypeDetails] = useState(''); + const [isConnectingToJupyter, setIsConnectingToJupyter] = useState(false); + + const handleFullScreenRef = useRef(null); + + // quota (user) and flavor (system) + // const { data: quotaAndFlavors } = trpc.instance.getQuotaAndFlavors.useQuery(); + // quota (user) and flavor (system) + const { data: quotaAndFlavors } = trpc.instance.getQuotaAndFlavors.useQuery<{ + userQuota: IUserConfig; + userFlavors: { [key: string]: FlavorDetails }; + }>(); + + + const handleConsoleConnect = (instance) => { + setSelectedInstance(instance); + setConsoleModalOpen(true); + }; + + const getInstances = trpc.instance.getInstances.useQuery(undefined, { + refetchInterval: 60 * 1000 + }); + const createInstance = trpc.instance.createInstance.useMutation({ + onSuccess: async () => { + void message.success('Instance created successfully.'); + await getInstances.refetch(); + }, + onError: (error) => { + void message.error(`Failed to create instance: ${error.message}`); + } + }); + const deleteInstance = trpc.instance.deleteInstance.useMutation({ + onSuccess: async () => { + void message.success('Instance deleted successfully.'); + await getInstances.refetch(); + }, + onError: (error) => { + void message.error(`Failed to delete instance: ${error.message}`); + } + }); + const startStopInstance = trpc.instance.startStopInstance.useMutation({ + onSuccess: async () => { + void message.success('Instance state changed successfully.'); + await getInstances.refetch(); + }, + onError: (error) => { + void message.error(`Failed to change instance state: ${error.message}`); + } + }); + const restartInstance = trpc.instance.restartInstance.useMutation({ + onSuccess: async () => { + void message.success('Instance restarted successfully.'); + await getInstances.refetch(); // refetch the instances to update the list + }, + onError: (error) => { + void message.error(`Failed to restart instance: ${error.message}`); + } + }); + + const [isModalOpen, setIsModalOpen] = useState(false); + + const [createForm] = Form.useForm(); + + // Define the initial form values including the default instanceType + const initialFormValues: Partial = { + instanceType: enumInstanceType.SMALL, + lifeSpan: quotaAndFlavors?.userQuota?.defaultLXDMaximumInstanceLife ?? 360 * 60 * 60 // Default to 360 hours if no user quota + }; + + useEffect(() => { + if (quotaAndFlavors?.userFlavors && quotaAndFlavors.userFlavors[enumInstanceType.SMALL]) { + const { cpuLimit, memoryLimit, diskLimit } = quotaAndFlavors.userFlavors[enumInstanceType.SMALL]; + setSelectedInstanceTypeDetails(`${cpuLimit} CPU, ${memoryLimit} memory, ${diskLimit} disk`); + } + }, [quotaAndFlavors]); + + + const handleCreateInstance = (values: CreateInstanceFormValues) => { + + const generatedName = `${values.appType}-${Date.now()}`; + const determinedType = values.appType === enumAppType.MATLAB ? LXDInstanceTypeEnum.VIRTUAL_MACHINE: LXDInstanceTypeEnum.CONTAINER; + // get the cpu, memorylimit, disk limit from the backend server + // Get the flavor details from the selected type + const flavorDetails = quotaAndFlavors?.userFlavors[values.instanceType]; + if (!flavorDetails) { + void message.error('Invalid instance type selected.'); + return; + } + // Use user-defined maximum instance life or default value + const effectiveLifeSpan = values.lifeSpan ?? (quotaAndFlavors?.userQuota?.defaultLXDMaximumInstanceLife || 360 * 60 * 60); + + createInstance.mutate({ + name: generatedName, + type: determinedType, + appType: values.appType, + lifeSpan: effectiveLifeSpan, + cpuLimit: flavorDetails.cpuLimit, + memoryLimit: flavorDetails.memoryLimit, + diskLimit: flavorDetails.diskLimit + }); + setIsModalOpen(false); + createForm.resetFields(); + }; + + const handleRestartInstance = async (values: { instance_id: string, lifeSpan: number }) => { + restartInstance.mutate({ + instanceId:values.instance_id, + lifeSpan: values.lifeSpan + }); + + }; + + const connectToJupyterHandler = async (instance_id: string) => { + setIsConnectingToJupyter(true); // Indicate that connection attempt is in progress + + try { + // // Construct the Jupyter proxy URL directly + const baseUrl = new URL(window.location.href); + const jupyterProxyUrl = `${baseUrl.origin}/jupyter/${instance_id}`; + + // Open the Jupyter service in a new tab + window.open(jupyterProxyUrl, '_blank'); + // Construct the Jupyter proxy URL for WebSocket connection + + + } catch (error: unknown) { + if (error instanceof Error) { + void message.error(error.message || 'Failed to connect to Jupyter. Please try again.'); + } else { + void message.error('Failed to connect to Jupyter. Please try again.'); + } + } finally { + setIsConnectingToJupyter(false); // Reset the connection attempt status + } + }; + + const handleDeleteInstance = (instance) => { + Modal.confirm({ + title: 'Are you sure to delete this instance?', + content: `This will delete the instance "${instance.name}". This action cannot be undone.`, + okText: 'Yes, delete it', + okType: 'danger', + cancelText: 'No, cancel', + onOk: () => { + deleteInstance.mutate({ instanceId: instance.id }); + } + }); + }; + + if (getInstances.isLoading) { + return
Loading instances...
; + } + + if (getInstances.isError) { + return
Error loading instances: {getInstances.error.message}
; + } + + const getStatusTagColor = (status: enumInstanceStatus) => { + switch (status) { + case enumInstanceStatus.PENDING: + return '#ffecb3'; + case enumInstanceStatus.RUNNING: + return '#87d068'; + case enumInstanceStatus.STOPPING: + return '#ffd8bf'; + case enumInstanceStatus.STOPPED: + return '#d9d9d9'; + case enumInstanceStatus.DELETED: + return '#f50'; + default: + return 'default'; + } + }; + const enterFullScreen = () => { + if (handleFullScreenRef.current) { + handleFullScreenRef.current.handleFullScreen(); + } + }; + + // Reset the connect signal when the modal is closed + const handleCloseModal = () => { + setConsoleModalOpen(false); + }; + + // sorting function + const sortedInstances = [...getInstances.data].sort((a, b) => { + if (a.status === enumInstanceStatus.RUNNING && b.status !== enumInstanceStatus.RUNNING) { + return -1; // a comes first + } + if (b.status === enumInstanceStatus.RUNNING && a.status !== enumInstanceStatus.RUNNING) { + return 1; // b comes first + } + if (a.status === enumInstanceStatus.FAILED && b.status !== enumInstanceStatus.FAILED) { + return 1; // b comes before a if a is FAILED and b is not + } + if (b.status === enumInstanceStatus.FAILED && a.status !== enumInstanceStatus.FAILED) { + return -1; // a comes before b if b is FAILED and a is not + } + // Within the same status, sort by creation time, most recent first + return new Date(b.createAt).getTime() - new Date(a.createAt).getTime(); + }); + + + return ( +
+
+
+ {} + + + + +
+
+ + {sortedInstances.map((instance) => ( + {instance.name}} + extra={{instance.status}} + className={css.cardContainer} + > + + +

Application Type: {instance.appType}

+

Created At: {new Date(instance.createAt).toLocaleString()}

+

Life Span: {(Number(instance.lifeSpan) / 3600).toFixed(2)} (hours)

+

+ CPU: {instance.config && typeof instance.config['limits.cpu'] === 'string' ? instance.config['limits.cpu'] : 'N/A'} Cores, + Memory: {instance.config && typeof instance.config['limits.memory'] === 'string' ? instance.config['limits.memory'] : 'N/A'} +

+ + + {instance.status === enumInstanceStatus.RUNNING && ( + + ( + + CPU: {('cpuUsage' in instance.metadata) ? instance.metadata.cpuUsage as number : 0}% + + )} + strokeWidth={15} // Adjust thickness + style={{ width: '150px' }} // Adjust width + /> + ( + + Memory: {('memoryUsage' in instance.metadata) ? Math.round(instance.metadata.memoryUsage as number) : 0}% + + )} + strokeWidth={15} // Adjust thickness + style={{ width: '150px' }} // Adjust width + /> + + )} + +
+ {/* Conditionally render Launch/Stop button based on status */} + + {instance.status === enumInstanceStatus.STOPPED && instance.lifeSpan > 0 && ( + + )} + {instance.status === enumInstanceStatus.RUNNING && ( + // change the danger to warning color + + // + )} + {/* Only show Delete button for STOPPED status */} + {(instance.status === enumInstanceStatus.STOPPED || instance.status === enumInstanceStatus.FAILED) && ( + + )} + {/*Restart button to reset the instance with new lifespan */} + {instance.status === enumInstanceStatus.STOPPED && instance.lifeSpan <= 0 && ( + + )} + {/** console connection button, only show for RUNNING status */} + {instance.appType !== enumAppType.JUPYTER && instance.status === enumInstanceStatus.RUNNING && ( + // set the button color to green + + )} + {instance.appType === enumAppType.JUPYTER && instance.status === enumInstanceStatus.RUNNING && ( + )} + +
+ ))} + setIsModalOpen(false)} onOk={() => createForm.submit()}> +
+ + + + + + + {selectedInstanceTypeDetails && ( +
{selectedInstanceTypeDetails}
+ )} +
+
+ + Cancel + , + + ]} + > + {selectedInstance && selectedInstance.name && (selectedInstance.type === 'container' ? ( + + ) : ( + + ))} + › + +
+ ); +}; \ No newline at end of file diff --git a/packages/itmat-ui-react/src/components/lxd/index.tsx b/packages/itmat-ui-react/src/components/lxd/index.tsx new file mode 100644 index 000000000..4708ae788 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/index.tsx @@ -0,0 +1,19 @@ +import React, { FunctionComponent } from 'react'; +import css from './lxd.module.css'; +import LXDInstanceList from './lxd.instance.list'; +// import LXDCommandExecutor from './lxd.instance.terminal'; + +export const LXDPage: FunctionComponent = () => { + return ( +
+
+
+ LXD Management +
+
+ +
+
+
+ ); +}; diff --git a/packages/itmat-ui-react/src/components/lxd/instanceOptions.tsx b/packages/itmat-ui-react/src/components/lxd/instanceOptions.tsx new file mode 100644 index 000000000..785c49d4e --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/instanceOptions.tsx @@ -0,0 +1,62 @@ +export const instanceCreationTypes = [ + { + label: 'Container', + value: 'container' + }, + { + label: 'VM', + value: 'virtual-machine' + } +]; + +export const optionTrueFalse = [ + { + label: 'Select option', + value: '', + disabled: true + }, + { + label: 'true', + value: 'true' + }, + { + label: 'false', + value: 'false' + } +]; + +export const optionAllowDeny = [ + { + label: 'Select option', + value: '', + disabled: true + }, + { + label: 'Allow', + value: 'true' + }, + { + label: 'Deny', + value: 'false' + } +]; + +export const optionYesNo = [ + { + label: 'Select option', + value: '', + disabled: true + }, + { + label: 'Yes', + value: 'true' + }, + { + label: 'No', + value: 'false' + } +]; + +export const diskPriorities = [...Array(11).keys()].map((i) => { + return { label: i.toString(), value: i }; +}); diff --git a/packages/itmat-ui-react/src/components/lxd/instanceTable.tsx b/packages/itmat-ui-react/src/components/lxd/instanceTable.tsx new file mode 100644 index 000000000..fd67300c0 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/instanceTable.tsx @@ -0,0 +1,26 @@ +export const STATUS = 'Status'; +export const NAME = 'Name'; +export const TYPE = 'Type'; +export const DESCRIPTION = 'Description'; +export const IPV4 = 'IPv4'; +export const IPV6 = 'IPv6'; +export const SNAPSHOTS = 'Snapshots'; +export const ACTIONS = 'Actions'; + +export const COLUMN_WIDTHS: Record = { + [NAME]: 170, + [TYPE]: 130, + [DESCRIPTION]: 150, + [SNAPSHOTS]: 110, + [STATUS]: 160, + [ACTIONS]: 210 +}; + +export const SIZE_HIDEABLE_COLUMNS = [ + SNAPSHOTS, + DESCRIPTION, + TYPE, + STATUS +]; + +export const CREATION_SPAN_COLUMNS = [TYPE, DESCRIPTION, IPV4, IPV6, SNAPSHOTS]; diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.Instance.statusIcon.tsx b/packages/itmat-ui-react/src/components/lxd/lxd.Instance.statusIcon.tsx new file mode 100644 index 000000000..130774cea --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/lxd.Instance.statusIcon.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Tooltip } from 'antd'; +import { + CheckCircleOutlined, + SyncOutlined, // This icon can spin + PauseCircleOutlined, + ExclamationCircleOutlined, + StopOutlined, + QuestionCircleOutlined +} from '@ant-design/icons'; +import css from './lxd.module.css'; + +const iconStyle = { fontSize: '16px' }; + +const statusToIcon = { + Running: , + Stopped: , + Starting: , + Stopping: , + Error: , + default: +}; + +const InstanceStatusIcon = ({ status }) => { + const IconComponent = statusToIcon[status] || statusToIcon.default; + return ( + + + {IconComponent} + + + ); +}; + +export default InstanceStatusIcon; diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx b/packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx new file mode 100644 index 000000000..cf78c797f --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx @@ -0,0 +1,178 @@ +import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef} from 'react'; +import { SpiceMainConn, handle_resize } from './spice/src/main'; +import {message} from 'antd'; + +import css from './lxd.module.css'; +import { trpc } from '../../utils/trpc'; + +declare global { + interface Window { + spice_connection?: SpiceMainConn; + } +} + +interface LXDConsoleProps { + instanceName: string; + } + +export interface LXDConsoleRef { + handleFullScreen: () => void +} + +const updateVgaConsoleSize = () => { + const spiceScreen = document.getElementById('spice-screen'); + if (spiceScreen) { + + // Set minimum dimensions to ensure the console is usable + const minWidth = 1024; + const minHeight = 768; + + // Calculate available dimensions + const availableWidth = Math.max(window.innerWidth, minWidth); + const availableHeight = Math.max(window.innerHeight - spiceScreen.offsetTop, minHeight); + + spiceScreen.style.width = `${availableWidth}px`; + spiceScreen.style.height = `${availableHeight}px`; + } +}; + + +export const LXDConsole = forwardRef(({ instanceName }, ref) => { + + const spiceRef = useRef(null); + const [isVgaLoading, setIsVgaLoading] = useState(false); + + const getInstanceConsole = trpc.lxd.getInstanceConsole.useMutation(); + + const handleResize = () => { + updateVgaConsoleSize(); + handle_resize(); + + }; + + const openVgaConsole = async () => { + setIsVgaLoading(true); + // Ensure the size is updated based on the current window size90hjhh] + handleResize(); + + const spiceScreen = document.getElementById('spice-screen'); + const width = spiceScreen ? spiceScreen.clientWidth : 1024; // Provide fallback dimensions + const height = spiceScreen ? spiceScreen.clientHeight : 768; + + // use trpc to get the console console + + const res = await getInstanceConsole.mutateAsync({ + container: instanceName, + options: { + height: height, + width: width, + type: 'vga' + } + }); + setIsVgaLoading(false); + + if (!res) { + return; + } + // Check if the response is an error + if ('error' in res) { + void message.error(`Error in console connection: ${res.data}`); + return; + } + + + const result = res; + + const baseUrl = new URL(window.location.href); + baseUrl.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + baseUrl.pathname = '/rtc'; + + const dataUrl = `${baseUrl.href}?t=d&o=${result.operationId}&s=${result.operationSecrets['0']}`; + const controlUrl = `${baseUrl.href}?t=c&o=${result.operationId}&s=${result.operationSecrets.control}`; + + const control = new WebSocket(controlUrl); + + try { + window.spice_connection = new SpiceMainConn({ + uri: dataUrl, + screen_id: 'spice-screen', + onerror: (e) => { + void message.error(`Error in console connection ${JSON.stringify(e)}`); + }, + onsuccess: () => { + setIsVgaLoading(false); + handleResize(); + }, + onagent: handleResize + }); + + } catch (e) { + void message.error(`Console create error ${e}`); + } + + return control; + }; + + const handleFullScreen = () => { + const container = spiceRef.current; + if (!container) { + return; + } + container + .requestFullscreen() + .then(handleResize) + .catch((e) => { + void message.error(`Failed to enter full-screen mode: ${JSON.stringify(e)}`); + }); + handleResize(); + + + }; + + useImperativeHandle(ref, () => ({ + handleFullScreen + })); + + useEffect(() => { + + const controlWebsocketPromise = openVgaConsole(); + + return () => { + controlWebsocketPromise.then((controlWebsocket) => { + if (controlWebsocket && controlWebsocket.readyState === WebSocket.OPEN) { + controlWebsocket.close(); + } + }).catch(e => { + void message.error(`Error closing WebSocket: ${JSON.stringify(e)}`); + }); + + // Ensure that the SPICE connection is stopped properly + if (window.spice_connection) { + try { + window.spice_connection.stop(); + } catch (e) { + void message.error(`Error stopping Console connection: ${JSON.stringify(e)}`); + } + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [instanceName]); + + useEffect(() => { + handleResize(); + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [instanceName]); + + return isVgaLoading ? ( +
Loading VGA console...
+ ) : ( +
+
+
+ ); +}); \ No newline at end of file diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx b/packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx new file mode 100644 index 000000000..44c0bbcc8 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx @@ -0,0 +1,373 @@ +import React, { useEffect, useRef, useState} from 'react'; +import { Table, Button, message, Spin, Modal, Space, Form, InputNumber, Input } from 'antd'; +import { DeleteOutlined } from '@ant-design/icons'; +// Additional imports +import { PoweroffOutlined, PlayCircleOutlined } from '@ant-design/icons'; +import { LXDInstanceType, enumOpeType} from '@itmat-broker/itmat-types'; + +// import { instanceCreationTypes } from './instanceOptions'; +import InstanceStatusIcon from '././lxd.Instance.statusIcon'; +import { LXDConsole, LXDConsoleRef} from './lxd.instance.console'; +import {LXDTextConsole, LXDTextConsoleRef} from './lxd.instance.text.console'; +import { trpc } from '../../utils/trpc'; +import css from './lxd.module.css'; +import { formatCPUInfo, formatMemoryInfo, formatStorageInfo, formatGPUInfo} from './util/formatUtils'; + + + +const LXDInstanceList = () => { + const [instances, setInstances] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedInstance, setSelectedInstance] = useState(null); + const [isInstanceDetailsModalVisible, setIsInstanceDetailsModalVisible] = useState(false); + + const [isUpdateConfigModalOpen, setIsUpdateConfigModalOpen] = useState(false); + const [editingInstance, setEditingInstance] = useState(null); + + const [updateConfigForm] = Form.useForm(); + + const [systemStats, setSystemStats] = useState({ + cpu: '', + memory: '', + storage: '', + gpu: '' + + }); + + const [isFullscreen, setIsFullscreen] = useState(false); // <-- Manage fullscreen state + + // const handleFullScreenRef = useRef(null); + const handleFullScreenRef = useRef(null); + const updateInstanceConfig = trpc.instance.editInstance.useMutation({ + onSuccess: () => { + void message.success('Instance configuration updated successfully.'); + }, + onError: (error) => { + void message.error(`Failed to update instance configuration: ${error.message}`); + } + }); + + const getInstances = trpc.lxd.getInstances.useQuery(undefined, { + refetchInterval: 60 * 1000 // Refetch every 60 seconds + }); + const getResources = trpc.lxd.getResources.useQuery(); + + + // Function to refresh the list of instances + const refreshInstancesList = async () => { + try { + await getInstances.refetch(); + } catch (error) { + console.error('Failed to refresh instances list:', error); + } + }; + + const startStopInstance = trpc.lxd.startStopInstance.useMutation({ + onSuccess: async () => { + void message.success('Operation successful'); + await refreshInstancesList(); + }, + onError: (error) => { + void message.error(`Failed operation: ${error.message}`); + } + }); + + const deleteInstance = trpc.lxd.deleteInstance.useMutation({ + onSuccess: async () => { + void message.success('Instance deleted successfully'); + await refreshInstancesList(); + }, + onError: (error) => { + void message.error(`Failed to delete instance: ${error.message}`); + } + }); + const handleStartStop = (instanceName: string , action: enumOpeType.START | enumOpeType.STOP, project: string) => { + startStopInstance.mutate({ instanceName, action, project}); + }; + + const handleDelete = (instanceName: string, project: string) => { + deleteInstance.mutate({ instanceName, project}); + }; + const enterFullScreen = () => { + if (handleFullScreenRef.current) { + handleFullScreenRef.current.handleFullScreen(); + setIsFullscreen(true); // Set fullscreen state + } + }; + + + const openUpdateConfigModal = (instance: LXDInstanceType) => { + setEditingInstance(instance); + setIsUpdateConfigModalOpen(true); + }; + + // Function to show the modal with the instance details + const showInstanceDetails = (instance: LXDInstanceType) => { + setSelectedInstance(instance); + setIsInstanceDetailsModalVisible(true); + }; + + + useEffect(() => { + if (!getInstances.isLoading && getInstances.data) { + const instancesList = getInstances.data.data as LXDInstanceType[]; + const formattedInstances = instancesList.map(instance => ({ + ...instance, // Spread all existing properties to cover all required fields + key: instance.name // Add 'key' if it's additional and not already included in the instance object + })); + setInstances(formattedInstances); + setLoading(false); + } + + if (getInstances.isError) { + void message.error('Failed to load instance state: ' + getInstances.error.message); + setLoading(false); + } + }, [getInstances.isLoading, getInstances.data, getInstances.isError, getInstances.error?.message]); + + + useEffect(() => { + if (!getResources.isLoading && getResources.data && typeof getResources.data.data !== 'string') { + + const data = getResources.data.data; + setSystemStats({ + cpu: formatCPUInfo(data.cpu), + memory: formatMemoryInfo(data.memory), + storage: formatStorageInfo(data.storage), + gpu: formatGPUInfo(data.gpu) + }); + } + + if (getResources.isError) { + void message.error('Error fetching system information'); // getResources?.error?.message + } + }, [getResources.isLoading, getResources.data, getResources.isError]); + + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (text, record) => ( + + ) + }, + + // username + { + title: 'Username', + dataIndex: 'username', + key: 'username', + render: text => {text} + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (status) => + }, + { + title: 'Actions', + key: 'actions', + render: (_, record) => ( + + + + + + + {/* Other actions can be added here */} + + ) + } + // Add more columns as needed + ]; + + const handleOpenConsole = (instance: LXDInstanceType) => { + setSelectedInstance(instance); + setIsModalVisible(true); + }; + + // TDDO: Reset the connect signal when the modal is closed + const handleCloseModal = () => { + setIsModalVisible(false); + setIsFullscreen(false); // Reset fullscreen state when modal is closed + }; + + const handleUpdateInstanceConfig = async (values: { cpuLimit: number; memoryLimit: number; }) => { + if (!editingInstance) return; // Guard clause in case no instance is selected + + try { + await updateInstanceConfig.mutateAsync({ + instanceName: editingInstance.key, + updates: { + cpuLimit: values.cpuLimit, + memoryLimit: `${values.memoryLimit}GB` + }, + instanceId: '' + }); + + setIsUpdateConfigModalOpen(false); + + } catch (error) { + void message.error(`Failed to update instance configuration: ${error instanceof Error ? error.message : String(error)}`); + } + }; + + // onChildMount function to pass the handleFullScreen function to the child component + // const onChildMount = (childHandleFullScreen) => { + // handleFullScreenRef.current = childHandleFullScreen; + // }; + + return ( +
+ +
+

System Resources

+

CPU: {systemStats.cpu}

+

Memory: {systemStats.memory}

+

Storage: {systemStats.storage}

+

GPU: {systemStats.gpu}

+
+ {loading ? : } + setIsInstanceDetailsModalVisible(false)} + footer={null} + > + {selectedInstance && ( +
+

Name: {selectedInstance.name}

+

Status: {selectedInstance.status}

+

Type: {selectedInstance.type}

+

Architecture: {selectedInstance.architecture}

+

Creation Date: {selectedInstance.creationDate}

+

Last Used Date: {selectedInstance.lastUsedDate}

+

Username: {selectedInstance.username}

+

CPU Limit: {selectedInstance.cpuLimit}

+

Memory Limit: {selectedInstance.memoryLimit}

+

Profiles: {selectedInstance.profiles?.join(', ')}

+
+ )} +
+ + + Cancel + , + + ]} + > + {selectedInstance?.name && (selectedInstance.type === 'container' ? ( + + ) : ( + + ))} + + + setIsUpdateConfigModalOpen(false)} + onOk={() => updateConfigForm.submit()} + > +
{ + void handleUpdateInstanceConfig(values); + }} + > + {/* CPU and Memory Form Items */} + + + + ({ + async validator() { + if (typeof getFieldValue('memoryLimit') === 'number') { + return Promise.resolve(); + } + return Promise.reject(new Error('Please input the memory limit!')); + } + }) + ]} + > + + + + + + + + +
+ + ); +}; + +export default LXDInstanceList; diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx b/packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx new file mode 100644 index 000000000..17b50cc3b --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx @@ -0,0 +1,350 @@ +import React, { useState, useEffect, useRef, useLayoutEffect, useCallback, useMemo, useImperativeHandle, forwardRef } from 'react'; +// import { Terminal } from 'xterm'; +import { Terminal } from '@xterm/xterm'; +import { FitAddon } from '@xterm/addon-fit'; +import { message } from 'antd'; +import { trpc } from '../../utils/trpc'; +import css from './lxd.module.css'; +import { LxdGetInstanceConsoleResponse } from '@itmat-broker/itmat-types'; + +interface LXDTextConsoleProps { + instanceName: string; +} + +export interface LXDTextConsoleRef { + handleFullScreen: () => void; +} + +export const LXDTextConsole = forwardRef(({ instanceName }, ref) => { + const terminalRef = useRef(null); + const terminalInstanceRef = useRef(null); + const fitAddonRef = useRef(null); + const [dataWs, setDataWs] = useState(null); + const [controlWs, setControlWs] = useState(null); + const [isLoading, setLoading] = useState(false); + const [consoleBuffer, setConsoleBuffer] = useState(''); + const isRendered = useRef(false); + const textEncoder = useMemo(() => new TextEncoder(), []); + // const textDecoder = useMemo(() => new TextDecoder('utf-8'), []); + const textDecoder = useMemo(() => new TextDecoder('utf-8', { fatal: false }), []); + + + + const getInstanceConsole = trpc.lxd.getInstanceConsole.useMutation(); + const getInstanceConsoleLog = trpc.lxd.getInstanceConsoleLog.useMutation(); + + let messageBuffer: string[] = []; + + const flushMessageBuffer = () => { + if (terminalInstanceRef.current) { + messageBuffer.forEach(message => terminalInstanceRef.current?.write(message)); + messageBuffer = []; + } + }; + + + // Fetch console log buffer before connecting the terminal. + const fetchConsoleLogBuffer = useCallback(async () => { + try { + const result = await getInstanceConsoleLog.mutateAsync({ container: instanceName }); + if (result?.data) { + setConsoleBuffer(result.data); + } + } catch (error) { + void message.error(`Failed to load console buffer: ${JSON.stringify(error)}`); + } + }, [getInstanceConsoleLog, instanceName]); + + const initTerminal = useCallback((dataWs: WebSocket) => { + if (terminalRef.current && !terminalInstanceRef.current) { + + const terminal = new Terminal({ + convertEol: true, + cursorStyle: 'block', + cursorWidth: 1, + cursorBlink: true, + fontFamily: 'Times New Roman', // Ensure it is truly monospaced + fontSize: 16, + fontWeight: 'normal', + letterSpacing: -1, // Adjust this to 1 or -1 if spacing issues persist + lineHeight: 1.2, // Adjust this to 1.2 or 1.5 if line spacing issues persist + scrollback: 1000, // Adding more scrollback to prevent visual overflow issues + theme: { + background: '#2b2b2b', + foreground: '#FFFFFF', + cursor: '#00FF00' + }, + disableStdin: false, // Ensure terminal input is enabled + allowProposedApi: true, // Allow the use of advanced APIs + windowsMode: true // Try setting this to true if using Windows (for line ending compatibility) + }); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + terminal.open(terminalRef.current); + + terminal.onData((data) => { + dataWs.send(textEncoder.encode(data)); + terminal.focus(); + }); + fitAddon.fit(); + terminal.focus(); + // // Adjust terminal size after initial messages + // setTimeout(() => { + // fitAddon.fit(); + // terminal.focus(); + // }, 100); + + + terminal.writeln('Connecting to console...\r\n'); + // Write initial message + setTimeout(() => { + fitAddon.fit(); // Ensure fit after writing + terminal.focus(); // Refocus after connecting message + }, 500); + + if (consoleBuffer) { + terminal.write(consoleBuffer); + setConsoleBuffer(''); + setTimeout(() => { + fitAddon.fit(); // Ensure fit after writing the buffer + terminal.focus(); // Refocus after writing + }, 200); + } + + terminalInstanceRef.current = terminal; + fitAddonRef.current = fitAddon; + + // Flush any buffered messages once the terminal is ready + flushMessageBuffer(); + // Ensure terminal height is adjusted after initialization + // updateMaxHeight('p-terminal', undefined, 10); + handleResize(); // Ensure terminal is resized properly on initialization + } + }, [consoleBuffer, textEncoder]); + + const initiateConnection = useCallback(async (width: number, height: number) => { + setLoading(true); + try { + await fetchConsoleLogBuffer(); + + const result: LxdGetInstanceConsoleResponse = await getInstanceConsole.mutateAsync({ + container: instanceName, + options: { height, width, type: 'console' } + }); + + // Check if the result contains the operationId (indicating success) + if ('operationId' in result && result.operationId) { + const baseUrl = new URL(window.location.href); + baseUrl.protocol = baseUrl.protocol === 'https:' ? 'wss:' : 'ws:'; + baseUrl.pathname = '/rtc'; + + // Data WebSocket connection + const dataUrl = `${baseUrl.href}?t=d&o=${result.operationId}&s=${result.operationSecrets['0']}`; + const data_ws = new WebSocket(dataUrl); + data_ws.binaryType = 'arraybuffer'; + + // Control WebSocket connection + const controlUrl = `${baseUrl.href}?t=c&o=${result.operationId}&s=${result.operationSecrets.control}`; + const control_ws = new WebSocket(controlUrl); + + // Setup Data WebSocket + data_ws.onopen = () => { + setDataWs(data_ws); + initTerminal(data_ws); + }; + + data_ws.onmessage = (event: MessageEvent) => { + try { + const messageData = new Uint8Array(event.data); + let decodedMessage = textDecoder.decode(messageData, { stream: true }); + + // Remove excessive carriage returns + decodedMessage = decodedMessage.replace(/\r+/g, ''); + + // console.log(decodedMessage); // For debugging + + if (terminalInstanceRef.current) { + setTimeout(() => { + terminalInstanceRef.current?.write(decodedMessage); + fitAddonRef.current?.fit(); // Fit after each message + terminalInstanceRef.current?.focus(); + }, 50); // Add a slight delay to prevent overflow + } else { + messageBuffer.push(decodedMessage); // Buffer messages until terminal is ready + } + } catch (error) { + console.error('Error decoding WebSocket message:', error); + } + }; + + + data_ws.onerror = (error) => { + void message.error(`Console connection error: ${JSON.stringify(error)}`); + }; + + data_ws.onclose = () => { + // Finalize the decoder + const finalMessage = textDecoder.decode(); + if (finalMessage) { + if (terminalInstanceRef.current) { + terminalInstanceRef.current.write(finalMessage); + } else { + messageBuffer.push(finalMessage); + } + } + setDataWs(null); + }; + + // Setup Control WebSocket + control_ws.onopen = () => { + setControlWs(control_ws); + }; + + control_ws.onerror = (error) => { + void message.error(`Control connection error: ${JSON.stringify(error)}`); + }; + + control_ws.onclose = () => { + setControlWs(null); + }; + + } else if ('error' in result && result.error) { + throw new Error(`Failed to initiate console session: ${result.data}`); + } + } catch (error) { + void message.error(`Error initiating connection: ${error instanceof Error ? error.message : String(error)}`); + } finally { + setLoading(false); + } + }, [fetchConsoleLogBuffer, getInstanceConsole, initTerminal, instanceName]); + + // Handle terminal resizing + const handleResize = () => { + if (fitAddonRef.current && terminalInstanceRef.current) { + fitAddonRef.current.fit(); + terminalInstanceRef.current?.focus(); + + // console.log('Resizing Terminal:', fitAddonRef.current.proposeDimensions()); + + const dimensions = fitAddonRef.current.proposeDimensions(); + + if (dimensions && controlWs?.readyState === WebSocket.OPEN) { + // Update the terminal dimensions based on the proposed dimensions + controlWs.send( + textEncoder.encode( + JSON.stringify({ + command: 'window-resize', + args: { + height: dimensions.rows.toString(), + width: dimensions.cols.toString() + } + }) + ) + ); + } + } + }; + + + const forceRedraw = () => { + if (terminalInstanceRef.current) { + const dimensions = fitAddonRef.current?.proposeDimensions(); + if (dimensions) { + terminalInstanceRef.current.resize(dimensions.cols, dimensions.rows); + terminalInstanceRef.current.refresh(0, dimensions.rows - 1); // Force a full refresh + } + } + }; + + // Call this after resize or after writing content + setTimeout(() => { + forceRedraw(); // Force terminal redraw after certain events + }, 200); + + + useEffect(() => { + if (consoleBuffer && terminalInstanceRef.current && !isLoading) { + // const consoleBufferData = textEncoder.encode(consoleBuffer); + terminalInstanceRef.current.write(consoleBuffer); + setConsoleBuffer(''); + } + }, [consoleBuffer, isLoading]); + + useEffect(() => { + if (!terminalRef.current || isRendered.current) return; + isRendered.current = true; + + const { clientWidth, clientHeight } = terminalRef.current || document.documentElement; + const width = Math.floor(clientWidth / 9); + const height = Math.floor(clientHeight / 17); + + initiateConnection(width, height).catch(error => { + void message.error(`Failed to initiate connection: ${error instanceof Error ? error.message : String(error)}`); + }); + + return () => { + if (dataWs) { + dataWs.close(); + } + if (controlWs) { + controlWs.close(); + } + if (terminalInstanceRef.current) { + terminalInstanceRef.current.dispose(); + terminalInstanceRef.current = null; + } + if (fitAddonRef.current) { + fitAddonRef.current.dispose(); + fitAddonRef.current = null; + } + }; + }, [dataWs, controlWs, initiateConnection, instanceName]); + + useLayoutEffect(() => { + const handleResizeWithDelay = () => { + handleResize(); + setTimeout(() => fitAddonRef.current?.fit(), 500); // Delay to avoid race condition + }; + + window.addEventListener('resize', handleResizeWithDelay); + return () => { + window.removeEventListener('resize', handleResizeWithDelay); + }; + }, [controlWs]); + + // Fullscreen handler similar to the VGA console's `handleFullScreen` + const handleFullScreen = () => { + const container = terminalRef.current; + if (!container) { + return; + } + container + .requestFullscreen() + .then(handleResize).then(() => { + // container.classList.add(css['fullscreen-terminal']); // Add the CSS module class for fullscreen + if (fitAddonRef.current) { + fitAddonRef.current.fit(); // Ensure terminal fits the fullscreen container + } + terminalInstanceRef.current?.focus(); // Focus the terminal after entering fullscreen + }) + .catch((e) => { + void message.error(`Failed to enter full-screen mode: ${JSON.stringify(e)}`); + }); + }; + + // Expose the fullscreen handler to the parent via `useImperativeHandle` + useImperativeHandle(ref, () => ({ + handleFullScreen + })); + + + return ( + isLoading ? ( +

Loading text console...

+ ) : ( +
+
+
+ ) + ); +}); diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.module.css b/packages/itmat-ui-react/src/components/lxd/lxd.module.css new file mode 100644 index 000000000..035a8e6f1 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/lxd.module.css @@ -0,0 +1,171 @@ +.page_container { + display: grid; + grid-template-columns: 100%; + grid-template-rows: auto; + height: 100%; + grid-template-areas: "lxdManagementArea"; + /* Additional styles as needed */ +} + +.lxdSection { + grid-area: lxdManagementArea; + padding: 20px; + background-color: #f4f4f4; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: auto; +} + +.pageAriane { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 20px; + color: #333; +} + +.pagePanel { + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1); + /* Ensure that the panel can scroll if content overflows */ + overflow: auto; +} + +/* InstanceStatusIcon.module.css */ +.icon_pinning { + animation: spin 1s infinite linear; +} + +@keyframes spin { + 100% { + transform: rotate(360deg); + } +} + +.pTerminalFullscreen { + width: 100%; + height: calc(100vh - 108px); + /* Adjust the height based on your header/footer sizes */ + overflow: hidden; + display: flex; + flex-direction: column; +} + +.pTerminal { + height: 100%; + width: 100%; + flex-grow: 1; + /* Allow terminal to fill the space */ +} + +.terminalInput { + width: 100%; + padding: 10px; + font-size: 16px; + box-sizing: border-box; + /* Include padding in the width */ +} + +.modalOverrides { + width: fit-content !important; + height: fit-content !important; + object-fit: contain !important; + margin: auto !important; + max-width: 100%; + max-height: 100%; + margin: 5vh auto; /* Center the modal vertically with 5% top and bottom margin */ + overflow: auto; /* Add scrollbars if the content is larger than the modal */ +} + +.spiceArea { + min-width: 500px; + min-height: 300px; + max-width: 80vw; + min-height: 50vh; + width: fit-content !important; + height: fit-content !important; + display: block; + background-color: black; +} + +.spiceScreen { + width: 100%; + height: 100%; + /* Ensure the spice screen fills the space */ +} + +.spiceScreen canvas { + max-width: 100%; + max-height: 100%; +} + +/* .consoleContainer { + width: 100%; + height: 100%; + padding: 16px; + background-color: #1e1e1e; + color: white; + box-sizing: border-box; + } */ + +html, +body, +#root { + height: 100%; + margin: 0; + padding: 0; +} + +.consoleContainer { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background-color: #000; +} + +.p_terminal { + flex-grow: 1; + width: 100%; + height: 100%; +} + +.pTerminalFullscreen { + width: 100%; + height: calc(100vh - 108px); /* Adjust based on your header/footer sizes */ + overflow: hidden; + display: flex; + flex-direction: column; +} + +.p-terminal { + font-family: "Courier New, monospace"; /* Same font as Terminal configuration */ + font-size: 13px; /* Same as terminal options */ + line-height: 1.3; + width: 100%; + height: 100%; + overflow: hidden; /* Prevents scrollbars */ +} + +/* console-modal CSS */ + +.console-modal { + width: 100%; + height: 100%; + padding: 0; + box-sizing: border-box; +} + +/* Fullscreen adjustments */ +.fullscreen-terminal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; + z-index: 9999; + background-color: #000; /* Optional: set background for full screen */ +} diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/atKeynames.js b/packages/itmat-ui-react/src/components/lxd/spice/src/atKeynames.js new file mode 100644 index 000000000..328424602 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/atKeynames.js @@ -0,0 +1,189 @@ + +/* + Copyright (C) 2012 by Aric Stewart + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ +/* + * Copyright 1990,91 by Thomas Roell, Dinkelscherben, Germany. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of Thomas Roell not be used in + * advertising or publicity pertaining to distribution of the software without + * specific, written prior permission. Thomas Roell makes no representations + * about the suitability of this software for any purpose. It is provided + * "as is" without express or implied warranty. + * + * THOMAS ROELL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO + * EVENT SHALL THOMAS ROELL BE LIABLE FOR ANY SPECIAL, INDIRECT OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, + * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ +/* + * Copyright (c) 1994-2003 by The XFree86 Project, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * Except as contained in this notice, the name of the copyright holder(s) + * and author(s) shall not be used in advertising or otherwise to promote + * the sale, use or other dealings in this Software without prior written + * authorization from the copyright holder(s) and author(s). + */ + +/* + * NOTE: The AT/MF keyboards can generate (via the 8042) two (MF: three) + * sets of scancodes. Set3 can only be generated by a MF keyboard. + * Set2 sends a makecode for keypress, and the same code prefixed by a + * F0 for keyrelease. This is a little bit ugly to handle. Thus we use + * here for X386 the PC/XT compatible Set1. This set uses 8bit scancodes. + * Bit 7 ist set if the key is released. The code E0 switches to a + * different meaning to add the new MF cursorkeys, while not breaking old + * applications. E1 is another special prefix. Since I assume that there + * will be further versions of PC/XT scancode compatible keyboards, we + * may be in trouble one day. + * + * IDEA: 1) Use Set2 on AT84 keyboards and translate it to MF Set3. + * 2) Use the keyboards native set and translate it to common keysyms. + */ + + +var KeyNames = { +/* + * definition of the AT84/MF101/MF102 Keyboard: + * ============================================================ + * Defined Key Cap Glyphs Pressed value + * Key Name Main Also (hex) (dec) + * ---------------- ---------- ------- ------ ------ + */ + KEY_Escape :/* Escape 0x01 */ 1, + KEY_1 :/* 1 ! 0x02 */ 2, + KEY_2 :/* 2 @ 0x03 */ 3, + KEY_3 :/* 3 # 0x04 */ 4, + KEY_4 :/* 4 $ 0x05 */ 5, + KEY_5 :/* 5 % 0x06 */ 6, + KEY_6 :/* 6 ^ 0x07 */ 7, + KEY_7 :/* 7 & 0x08 */ 8, + KEY_8 :/* 8 * 0x09 */ 9, + KEY_9 :/* 9 ( 0x0a */ 10, + KEY_0 :/* 0 ) 0x0b */ 11, + KEY_Minus :/* - (Minus) _ (Under) 0x0c */ 12, + KEY_Equal :/* = (Equal) + 0x0d */ 13, + KEY_BackSpace :/* Back Space 0x0e */ 14, + KEY_Tab :/* Tab 0x0f */ 15, + KEY_Q :/* Q 0x10 */ 16, + KEY_W :/* W 0x11 */ 17, + KEY_E :/* E 0x12 */ 18, + KEY_R :/* R 0x13 */ 19, + KEY_T :/* T 0x14 */ 20, + KEY_Y :/* Y 0x15 */ 21, + KEY_U :/* U 0x16 */ 22, + KEY_I :/* I 0x17 */ 23, + KEY_O :/* O 0x18 */ 24, + KEY_P :/* P 0x19 */ 25, + KEY_LBrace :/* [ { 0x1a */ 26, + KEY_RBrace :/* ] } 0x1b */ 27, + KEY_Enter :/* Enter 0x1c */ 28, + KEY_LCtrl :/* Ctrl(left) 0x1d */ 29, + KEY_A :/* A 0x1e */ 30, + KEY_S :/* S 0x1f */ 31, + KEY_D :/* D 0x20 */ 32, + KEY_F :/* F 0x21 */ 33, + KEY_G :/* G 0x22 */ 34, + KEY_H :/* H 0x23 */ 35, + KEY_J :/* J 0x24 */ 36, + KEY_K :/* K 0x25 */ 37, + KEY_L :/* L 0x26 */ 38, + KEY_SemiColon :/* ;(SemiColon) :(Colon) 0x27 */ 39, + KEY_Quote :/* ' (Apostr) " (Quote) 0x28 */ 40, + KEY_Tilde :/* ` (Accent) ~ (Tilde) 0x29 */ 41, + KEY_ShiftL :/* Shift(left) 0x2a */ 42, + KEY_BSlash :/* \(BckSlash) |(VertBar)0x2b */ 43, + KEY_Z :/* Z 0x2c */ 44, + KEY_X :/* X 0x2d */ 45, + KEY_C :/* C 0x2e */ 46, + KEY_V :/* V 0x2f */ 47, + KEY_B :/* B 0x30 */ 48, + KEY_N :/* N 0x31 */ 49, + KEY_M :/* M 0x32 */ 50, + KEY_Comma :/* , (Comma) < (Less) 0x33 */ 51, + KEY_Period :/* . (Period) >(Greater)0x34 */ 52, + KEY_Slash :/* / (Slash) ? 0x35 */ 53, + KEY_ShiftR :/* Shift(right) 0x36 */ 54, + KEY_KP_Multiply :/* * 0x37 */ 55, + KEY_Alt :/* Alt(left) 0x38 */ 56, + KEY_Space :/* (SpaceBar) 0x39 */ 57, + KEY_CapsLock :/* CapsLock 0x3a */ 58, + KEY_F1 :/* F1 0x3b */ 59, + KEY_F2 :/* F2 0x3c */ 60, + KEY_F3 :/* F3 0x3d */ 61, + KEY_F4 :/* F4 0x3e */ 62, + KEY_F5 :/* F5 0x3f */ 63, + KEY_F6 :/* F6 0x40 */ 64, + KEY_F7 :/* F7 0x41 */ 65, + KEY_F8 :/* F8 0x42 */ 66, + KEY_F9 :/* F9 0x43 */ 67, + KEY_F10 :/* F10 0x44 */ 68, + KEY_NumLock :/* NumLock 0x45 */ 69, + KEY_ScrollLock :/* ScrollLock 0x46 */ 70, + KEY_KP_7 :/* 7 Home 0x47 */ 71, + KEY_KP_8 :/* 8 Up 0x48 */ 72, + KEY_KP_9 :/* 9 PgUp 0x49 */ 73, + KEY_KP_Minus :/* - (Minus) 0x4a */ 74, + KEY_KP_4 :/* 4 Left 0x4b */ 75, + KEY_KP_5 :/* 5 0x4c */ 76, + KEY_KP_6 :/* 6 Right 0x4d */ 77, + KEY_KP_Plus :/* + (Plus) 0x4e */ 78, + KEY_KP_1 :/* 1 End 0x4f */ 79, + KEY_KP_2 :/* 2 Down 0x50 */ 80, + KEY_KP_3 :/* 3 PgDown 0x51 */ 81, + KEY_KP_0 :/* 0 Insert 0x52 */ 82, + KEY_KP_Decimal :/* . (Decimal) Delete 0x53 */ 83, + KEY_SysRequest :/* SysRequest 0x54 */ 84, + /* NOTUSED 0x55 */ + KEY_Less :/* < (Less) >(Greater) 0x56 */ 86, + KEY_F11 :/* F11 0x57 */ 87, + KEY_F12 :/* F12 0x58 */ 88, + + KEY_Prefix0 :/* special 0x60 */ 96, + KEY_Prefix1 :/* specail 0x61 */ 97 +}; + +export { + KeyNames +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/bitmap.js b/packages/itmat-ui-react/src/components/lxd/spice/src/bitmap.js new file mode 100644 index 000000000..c1956183b --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/bitmap.js @@ -0,0 +1,65 @@ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + + +/*---------------------------------------------------------------------------- +** bitmap.js +** Handle SPICE_IMAGE_TYPE_BITMAP +**--------------------------------------------------------------------------*/ + +import { Constants } from './enums.js'; + +function convert_spice_bitmap_to_web(context, spice_bitmap) +{ + var ret; + var offset, x, src_offset = 0, src_dec = 0; + var u8 = new Uint8Array(spice_bitmap.data); + if (spice_bitmap.format != Constants.SPICE_BITMAP_FMT_32BIT && + spice_bitmap.format != Constants.SPICE_BITMAP_FMT_RGBA) + return undefined; + + if (!(spice_bitmap.flags & Constants.SPICE_BITMAP_FLAGS_TOP_DOWN)) + { + src_offset = (spice_bitmap.y - 1 ) * spice_bitmap.stride; + src_dec = 2 * spice_bitmap.stride; + } + + ret = context.createImageData(spice_bitmap.x, spice_bitmap.y); + for (offset = 0; offset < (spice_bitmap.y * spice_bitmap.stride); src_offset -= src_dec) + for (x = 0; x < spice_bitmap.x; x++, offset += 4, src_offset += 4) + { + ret.data[offset + 0 ] = u8[src_offset + 2]; + ret.data[offset + 1 ] = u8[src_offset + 1]; + ret.data[offset + 2 ] = u8[src_offset + 0]; + + // FIXME - We effectively treat all images as having SPICE_IMAGE_FLAGS_HIGH_BITS_SET + if (spice_bitmap.format == Constants.SPICE_BITMAP_FMT_32BIT) + ret.data[offset + 3] = 255; + else + ret.data[offset + 3] = u8[src_offset]; + } + + return ret; +} + +export { + convert_spice_bitmap_to_web +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/code_to_scancode.js b/packages/itmat-ui-react/src/components/lxd/spice/src/code_to_scancode.js new file mode 100644 index 000000000..b23c81fee --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/code_to_scancode.js @@ -0,0 +1,149 @@ +/* + * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values + */ +export const code_to_scancode = []; +code_to_scancode['Escape'] = 0x01; +code_to_scancode['Digit1'] = 0x02; +code_to_scancode['Digit2'] = 0x03; +code_to_scancode['Digit3'] = 0x04; +code_to_scancode['Digit4'] = 0x05; +code_to_scancode['Digit5'] = 0x06; +code_to_scancode['Digit6'] = 0x07; +code_to_scancode['Digit7'] = 0x08; +code_to_scancode['Digit8'] = 0x09; +code_to_scancode['Digit9'] = 0x0A; +code_to_scancode['Digit0'] = 0x0B; +code_to_scancode['Minus'] = 0x0C; +code_to_scancode['Equal'] = 0x0D; +code_to_scancode['Backspace'] = 0x0E; +code_to_scancode['Tab'] = 0x0F; +code_to_scancode['KeyQ'] = 0x10; +code_to_scancode['KeyW'] = 0x11; +code_to_scancode['KeyE'] = 0x12; +code_to_scancode['KeyR'] = 0x13; +code_to_scancode['KeyT'] = 0x14; +code_to_scancode['KeyY'] = 0x15; +code_to_scancode['KeyU'] = 0x16; +code_to_scancode['KeyI'] = 0x17; +code_to_scancode['KeyO'] = 0x18; +code_to_scancode['KeyP'] = 0x19; +code_to_scancode['BracketLeft'] = 0x1A; +code_to_scancode['BracketRight'] = 0x1B; +code_to_scancode['Enter'] = 0x1C; +code_to_scancode['ControlLeft'] = 0x1D; +code_to_scancode['KeyA'] = 0x1E; +code_to_scancode['KeyS'] = 0x1F; +code_to_scancode['KeyD'] = 0x20; +code_to_scancode['KeyF'] = 0x21; +code_to_scancode['KeyG'] = 0x22; +code_to_scancode['KeyH'] = 0x23; +code_to_scancode['KeyJ'] = 0x24; +code_to_scancode['KeyK'] = 0x25; +code_to_scancode['KeyL'] = 0x26; +code_to_scancode['Semicolon'] = 0x27; +code_to_scancode['Quote'] = 0x28; +code_to_scancode['Backquote'] = 0x29; +code_to_scancode['ShiftLeft'] = 0x2A; +code_to_scancode['Backslash'] = 0x2B; +code_to_scancode['KeyZ'] = 0x2C; +code_to_scancode['KeyX'] = 0x2D; +code_to_scancode['KeyC'] = 0x2E; +code_to_scancode['KeyV'] = 0x2F; +code_to_scancode['KeyB'] = 0x30; +code_to_scancode['KeyN'] = 0x31; +code_to_scancode['KeyM'] = 0x32; +code_to_scancode['Comma'] = 0x33; +code_to_scancode['Period'] = 0x34; +code_to_scancode['Slash'] = 0x35; +code_to_scancode['ShiftRight'] = 0x36; +code_to_scancode['NumpadMultiply'] = 0x37; +code_to_scancode['AltLeft'] = 0x38; +code_to_scancode['Space'] = 0x39; +code_to_scancode['CapsLock'] = 0x3A; +code_to_scancode['F1'] = 0x3B; +code_to_scancode['F2'] = 0x3C; +code_to_scancode['F3'] = 0x3D; +code_to_scancode['F4'] = 0x3E; +code_to_scancode['F5'] = 0x3F; +code_to_scancode['F6'] = 0x40; +code_to_scancode['F7'] = 0x41; +code_to_scancode['F8'] = 0x42; +code_to_scancode['F9'] = 0x43; +code_to_scancode['F10'] = 0x44; +code_to_scancode['Pause'] = 0x45; +code_to_scancode['ScrollLock'] = 0x46; +code_to_scancode['Numpad7'] = 0x47; +code_to_scancode['Numpad8'] = 0x48; +code_to_scancode['Numpad9'] = 0x49; +code_to_scancode['NumpadSubtract'] = 0x4A; +code_to_scancode['Numpad4'] = 0x4B; +code_to_scancode['Numpad5'] = 0x4C; +code_to_scancode['Numpad6'] = 0x4D; +code_to_scancode['NumpadAdd'] = 0x4E; +code_to_scancode['Numpad1'] = 0x4F; +code_to_scancode['Numpad2'] = 0x50; +code_to_scancode['Numpad3'] = 0x51; +code_to_scancode['Numpad0'] = 0x52; +code_to_scancode['NumpadDecimal'] = 0x53; +code_to_scancode['PrintScreen'] = 0x54; +code_to_scancode['IntlBackslash'] = 0x56; +code_to_scancode['F11'] = 0x57; +code_to_scancode['F12'] = 0x58; +code_to_scancode['NumpadEqual'] = 0x59; +code_to_scancode['F13'] = 0x64; +code_to_scancode['F14'] = 0x65; +code_to_scancode['F15'] = 0x66; +code_to_scancode['F16'] = 0x67; +code_to_scancode['F17'] = 0x68; +code_to_scancode['F18'] = 0x69; +code_to_scancode['F19'] = 0x6A; +code_to_scancode['F20'] = 0x6B; +code_to_scancode['F21'] = 0x6C; +code_to_scancode['F22'] = 0x6D; +code_to_scancode['F23'] = 0x6E; +code_to_scancode['KanaMode'] = 0x70; +code_to_scancode['IntlRo'] = 0x73; +code_to_scancode['F24'] = 0x76; +code_to_scancode['Convert'] = 0x79; +code_to_scancode['NonConvert'] = 0x7B; +code_to_scancode['IntlYen'] = 0x7D; +code_to_scancode['NumpadComma'] = 0x7E; +code_to_scancode['MediaTrackPrevious'] = 0xE0 | (0x10 << 8); +code_to_scancode['MediaTrackNext'] = 0xE0 | (0x19 << 8); +code_to_scancode['NumpadEnter'] = 0xE0 | (0x1C << 8); +code_to_scancode['ControlRight'] = 0xE0 | (0x1D << 8); +code_to_scancode['AudioVolumeMute'] = 0xE0 | (0x20 << 8); +code_to_scancode['LaunchApp2'] = 0xE0 | (0x21 << 8); +code_to_scancode['MediaPlayPause'] = 0xE0 | (0x22 << 8); +code_to_scancode['MediaStop'] = 0xE0 | (0x24 << 8); +code_to_scancode['VolumeDown'] = 0xE0 | (0x2E << 8); +code_to_scancode['VolumeUp'] = 0xE0 | (0x30 << 8); +code_to_scancode['BrowserHome'] = 0xE0 | (0x32 << 8); +code_to_scancode['NumpadDivide'] = 0xE0 | (0x35 << 8); +code_to_scancode['PrintScreen'] = 0xE0 | (0x37 << 8); +code_to_scancode['AltRight'] = 0xE0 | (0x38 << 8); +code_to_scancode['NumLock'] = 0xE0 | (0x45 << 8); +code_to_scancode['Pause'] = 0xE0 | (0x46 << 8); +code_to_scancode['Home'] = 0xE0 | (0x47 << 8); +code_to_scancode['ArrowUp'] = 0xE0 | (0x48 << 8); +code_to_scancode['PageUp'] = 0xE0 | (0x49 << 8); +code_to_scancode['ArrowLeft'] = 0xE0 | (0x4B << 8); +code_to_scancode['ArrowRight'] = 0xE0 | (0x4D << 8); +code_to_scancode['End'] = 0xE0 | (0x4F << 8); +code_to_scancode['ArrowDown'] = 0xE0 | (0x50 << 8); +code_to_scancode['PageDown'] = 0xE0 | (0x51 << 8); +code_to_scancode['Insert'] = 0xE0 | (0x52 << 8); +code_to_scancode['Delete'] = 0xE0 | (0x53 << 8); +code_to_scancode['MetaLeft'] = 0xE0 | (0x5B << 8); +code_to_scancode['MetaRight'] = 0xE0 | (0x5C << 8); +code_to_scancode['ContextMenu'] = 0xE0 | (0x5D << 8); +code_to_scancode['Power'] = 0xE0 | (0x5E << 8); +code_to_scancode['BrowserSearch'] = 0xE0 | (0x65 << 8); +code_to_scancode['BrowserFavorites'] = 0xE0 | (0x66 << 8); +code_to_scancode['BrowserRefresh'] = 0xE0 | (0x67 << 8); +code_to_scancode['BrowserStop'] = 0xE0 | (0x68 << 8); +code_to_scancode['BrowserForward'] = 0xE0 | (0x69 << 8); +code_to_scancode['BrowserBack'] = 0xE0 | (0x6A << 8); +code_to_scancode['LaunchApp1'] = 0xE0 | (0x6B << 8); +code_to_scancode['LaunchMail'] = 0xE0 | (0x6C << 8); +code_to_scancode['MediaSelect'] = 0xE0 | (0x6D << 8); diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/cursor.js b/packages/itmat-ui-react/src/components/lxd/spice/src/cursor.js new file mode 100644 index 000000000..3efc62971 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/cursor.js @@ -0,0 +1,142 @@ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import { create_rgba_png } from './png.js'; +import { Constants } from './enums.js'; +import { DEBUG } from './utils.js'; +import { + SpiceMsgCursorInit, + SpiceMsgCursorSet +} from './spicemsg.js'; +import { SpiceSimulateCursor } from './simulatecursor.js'; +import { SpiceConn } from './spiceconn.js'; + +/*---------------------------------------------------------------------------- +** SpiceCursorConn +** Drive the Spice Cursor Channel +**--------------------------------------------------------------------------*/ +function SpiceCursorConn() +{ + SpiceConn.apply(this, arguments); +} + +SpiceCursorConn.prototype = Object.create(SpiceConn.prototype); +SpiceCursorConn.prototype.process_channel_message = function(msg) +{ + if (msg.type == Constants.SPICE_MSG_CURSOR_INIT) + { + var cursor_init = new SpiceMsgCursorInit(msg.data); + DEBUG > 1 && console.log('SpiceMsgCursorInit'); + if (this.parent && this.parent.inputs && + this.parent.inputs.mouse_mode == Constants.SPICE_MOUSE_MODE_SERVER) + { + // FIXME - this imagines that the server actually + // provides the current cursor position, + // instead of 0,0. As of May 11, 2012, + // that assumption was false :-(. + this.parent.inputs.mousex = cursor_init.position.x; + this.parent.inputs.mousey = cursor_init.position.y; + } + // FIXME - We don't handle most of the parameters here... + return true; + } + + if (msg.type == Constants.SPICE_MSG_CURSOR_SET) + { + var cursor_set = new SpiceMsgCursorSet(msg.data); + DEBUG > 1 && console.log('SpiceMsgCursorSet'); + if (cursor_set.flags & Constants.SPICE_CURSOR_FLAGS_NONE) + { + document.getElementById(this.parent.screen_id).style.cursor = 'none'; + return true; + } + + if (cursor_set.flags > 0) + this.log_warn('FIXME: No support for cursor flags ' + cursor_set.flags); + + if (cursor_set.cursor.header.type != Constants.SPICE_CURSOR_TYPE_ALPHA) + { + this.log_warn('FIXME: No support for cursor type ' + cursor_set.cursor.header.type); + return false; + } + + this.set_cursor(cursor_set.cursor); + + return true; + } + + if (msg.type == Constants.SPICE_MSG_CURSOR_MOVE) + { + this.known_unimplemented(msg.type, 'Cursor Move'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_CURSOR_HIDE) + { + DEBUG > 1 && console.log('SpiceMsgCursorHide'); + document.getElementById(this.parent.screen_id).style.cursor = 'none'; + return true; + } + + if (msg.type == Constants.SPICE_MSG_CURSOR_TRAIL) + { + this.known_unimplemented(msg.type, 'Cursor Trail'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_CURSOR_RESET) + { + DEBUG > 1 && console.log('SpiceMsgCursorReset'); + document.getElementById(this.parent.screen_id).style.cursor = 'auto'; + return true; + } + + if (msg.type == Constants.SPICE_MSG_CURSOR_INVAL_ONE) + { + this.known_unimplemented(msg.type, 'Cursor Inval One'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_CURSOR_INVAL_ALL) + { + DEBUG > 1 && console.log('SpiceMsgCursorInvalAll'); + // FIXME - There may be something useful to do here... + return true; + } + + return false; +}; + +SpiceCursorConn.prototype.set_cursor = function(cursor) +{ + var pngstr = create_rgba_png(cursor.header.width, cursor.header.height, cursor.data); + var curstr = 'url(data:image/png,' + pngstr + ') ' + + cursor.header.hot_spot_x + ' ' + cursor.header.hot_spot_y + ', default'; + var screen = document.getElementById(this.parent.screen_id); + screen.style.cursor = 'auto'; + screen.style.cursor = curstr; + if (window.getComputedStyle(screen, null).cursor == 'auto') + SpiceSimulateCursor.simulate_cursor(this, cursor, screen, pngstr); +}; + +export { + SpiceCursorConn +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/display.js b/packages/itmat-ui-react/src/components/lxd/spice/src/display.js new file mode 100644 index 000000000..4f10bd19d --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/display.js @@ -0,0 +1,1276 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-redeclare */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import * as Webm from './webm.js'; +import * as Messages from './spicemsg.js'; +import * as Quic from './quic.js'; +import * as Utils from './utils.js'; +import * as Inputs from './inputs.js'; +import { Constants } from './enums.js'; +import { SpiceConn } from './spiceconn.js'; +import { SpiceRect } from './spicetype.js'; +import { convert_spice_lz_to_web } from './lz.js'; +import { convert_spice_bitmap_to_web } from './bitmap.js'; + +/*---------------------------------------------------------------------------- +** FIXME: putImageData does not support Alpha blending +** or compositing. So if we have data in an ImageData +** format, we have to draw it onto a context, +** and then use drawImage to put it onto the target, +** as drawImage does alpha. +**--------------------------------------------------------------------------*/ +function putImageDataWithAlpha(context, d, x, y) +{ + var c = document.createElement('canvas'); + var t = c.getContext('2d'); + c.setAttribute('width', d.width); + c.setAttribute('height', d.height); + t.putImageData(d, 0, 0); + context.drawImage(c, x, y, d.width, d.height); +} + +/*---------------------------------------------------------------------------- +** FIXME: Spice will send an image with '0' alpha when it is intended to +** go on a surface w/no alpha. So in that case, we have to strip +** out the alpha. The test case for this was flux box; in a Xspice +** server, right click on the desktop to get the menu; the top bar +** doesn't paint/highlight correctly w/out this change. +**--------------------------------------------------------------------------*/ +function stripAlpha(d) +{ + var i; + for (i = 0; i < (d.width * d.height * 4); i += 4) + d.data[i + 3] = 255; +} + +/*---------------------------------------------------------------------------- +** SpiceDisplayConn +** Drive the Spice Display Channel +**--------------------------------------------------------------------------*/ +function SpiceDisplayConn() +{ + SpiceConn.apply(this, arguments); +} + +SpiceDisplayConn.prototype = Object.create(SpiceConn.prototype); +SpiceDisplayConn.prototype.process_channel_message = function(msg) +{ + if (msg.type == Constants.SPICE_MSG_DISPLAY_MODE) + { + this.known_unimplemented(msg.type, 'Display Mode'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_MARK) + { + // FIXME - DISPLAY_MARK not implemented (may be hard or impossible) + this.known_unimplemented(msg.type, 'Display Mark'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_RESET) + { + Utils.DEBUG > 2 && console.log('Display reset'); + this.surfaces[this.primary_surface].canvas.context.restore(); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_COPY) + { + var draw_copy = new Messages.SpiceMsgDisplayDrawCopy(msg.data); + + Utils.DEBUG > 1 && this.log_draw('DrawCopy', draw_copy); + + if (! draw_copy.base.box.is_same_size(draw_copy.data.src_area)) + this.log_warn('FIXME: DrawCopy src_area is a different size than base.box; we do not handle that yet.'); + if (draw_copy.base.clip.type != Constants.SPICE_CLIP_TYPE_NONE) + this.log_warn('FIXME: DrawCopy we don\'t handle clipping yet'); + if (draw_copy.data.rop_descriptor != Constants.SPICE_ROPD_OP_PUT) + this.log_warn('FIXME: DrawCopy we don\'t handle ropd type: ' + draw_copy.data.rop_descriptor); + if (draw_copy.data.mask.flags) + this.log_warn('FIXME: DrawCopy we don\'t handle mask flag: ' + draw_copy.data.mask.flags); + if (draw_copy.data.mask.bitmap) + this.log_warn('FIXME: DrawCopy we don\'t handle mask'); + + if (draw_copy.data && draw_copy.data.src_bitmap) + { + if (draw_copy.data.src_bitmap.descriptor.flags && + draw_copy.data.src_bitmap.descriptor.flags != Constants.SPICE_IMAGE_FLAGS_CACHE_ME && + draw_copy.data.src_bitmap.descriptor.flags != Constants.SPICE_IMAGE_FLAGS_HIGH_BITS_SET) + { + this.log_warn('FIXME: DrawCopy unhandled image flags: ' + draw_copy.data.src_bitmap.descriptor.flags); + Utils.DEBUG <= 1 && this.log_draw('DrawCopy', draw_copy); + } + + if (draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_QUIC) + { + var canvas = this.surfaces[draw_copy.base.surface_id].canvas; + if (! draw_copy.data.src_bitmap.quic) + { + this.log_warn('FIXME: DrawCopy could not handle this QUIC file.'); + return false; + } + var source_img = Quic.convert_spice_quic_to_web(canvas.context, + draw_copy.data.src_bitmap.quic); + + return this.draw_copy_helper( + { base: draw_copy.base, + src_area: draw_copy.data.src_area, + image_data: source_img, + tag: 'copyquic.' + draw_copy.data.src_bitmap.quic.type, + has_alpha: (draw_copy.data.src_bitmap.quic.type == Quic.Constants.QUIC_IMAGE_TYPE_RGBA ? true : false) , + descriptor : draw_copy.data.src_bitmap.descriptor + }); + } + else if (draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_FROM_CACHE || + draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_FROM_CACHE_LOSSLESS) + { + if (! this.cache || ! this.cache[draw_copy.data.src_bitmap.descriptor.id]) + { + this.log_warn('FIXME: DrawCopy did not find image id ' + draw_copy.data.src_bitmap.descriptor.id + ' in cache.'); + return false; + } + + return this.draw_copy_helper( + { base: draw_copy.base, + src_area: draw_copy.data.src_area, + image_data: this.cache[draw_copy.data.src_bitmap.descriptor.id], + tag: 'copycache.' + draw_copy.data.src_bitmap.descriptor.id, + has_alpha: true, /* FIXME - may want this to be false... */ + descriptor : draw_copy.data.src_bitmap.descriptor + }); + + /* FIXME - LOSSLESS CACHE ramifications not understood or handled */ + } + else if (draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_SURFACE) + { + var source_context = this.surfaces[draw_copy.data.src_bitmap.surface_id].canvas.context; + var target_context = this.surfaces[draw_copy.base.surface_id].canvas.context; + + var source_img = source_context.getImageData( + draw_copy.data.src_area.left, draw_copy.data.src_area.top, + draw_copy.data.src_area.right - draw_copy.data.src_area.left, + draw_copy.data.src_area.bottom - draw_copy.data.src_area.top); + var computed_src_area = new SpiceRect(); + computed_src_area.top = computed_src_area.left = 0; + computed_src_area.right = source_img.width; + computed_src_area.bottom = source_img.height; + + /* FIXME - there is a potential optimization here. + That is, if the surface is from 0,0, and + both surfaces are alpha surfaces, you should + be able to just do a drawImage, which should + save time. */ + + return this.draw_copy_helper( + { base: draw_copy.base, + src_area: computed_src_area, + image_data: source_img, + tag: 'copysurf.' + draw_copy.data.src_bitmap.surface_id, + has_alpha: this.surfaces[draw_copy.data.src_bitmap.surface_id].format == Constants.SPICE_SURFACE_FMT_32_xRGB ? false : true, + descriptor : draw_copy.data.src_bitmap.descriptor + }); + } + else if (draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_JPEG) + { + if (! draw_copy.data.src_bitmap.jpeg) + { + this.log_warn('FIXME: DrawCopy could not handle this JPEG file.'); + return false; + } + + // FIXME - how lame is this. Be have it in binary format, and we have + // to put it into string to get it back into jpeg. Blech. + var tmpstr = 'data:image/jpeg,'; + var img = new Image(); + var i; + var qdv = new Uint8Array(draw_copy.data.src_bitmap.jpeg.data); + for (i = 0; i < qdv.length; i++) + { + tmpstr += '%'; + if (qdv[i] < 16) + tmpstr += '0'; + tmpstr += qdv[i].toString(16); + } + + img.o = + { base: draw_copy.base, + tag: 'jpeg.' + draw_copy.data.src_bitmap.surface_id, + descriptor : draw_copy.data.src_bitmap.descriptor, + sc : this + }; + img.onload = handle_draw_jpeg_onload; + img.src = tmpstr; + + return true; + } + else if (draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_JPEG_ALPHA) + { + if (! draw_copy.data.src_bitmap.jpeg_alpha) + { + this.log_warn('FIXME: DrawCopy could not handle this JPEG ALPHA file.'); + return false; + } + + // FIXME - how lame is this. Be have it in binary format, and we have + // to put it into string to get it back into jpeg. Blech. + var tmpstr = 'data:image/jpeg,'; + var img = new Image(); + var i; + var qdv = new Uint8Array(draw_copy.data.src_bitmap.jpeg_alpha.data); + for (i = 0; i < qdv.length; i++) + { + tmpstr += '%'; + if (qdv[i] < 16) + tmpstr += '0'; + tmpstr += qdv[i].toString(16); + } + + img.o = + { base: draw_copy.base, + tag: 'jpeg.' + draw_copy.data.src_bitmap.surface_id, + descriptor : draw_copy.data.src_bitmap.descriptor, + sc : this + }; + + if (this.surfaces[draw_copy.base.surface_id].format == Constants.SPICE_SURFACE_FMT_32_ARGB) + { + + var canvas = this.surfaces[draw_copy.base.surface_id].canvas; + img.alpha_img = convert_spice_lz_to_web(canvas.context, + draw_copy.data.src_bitmap.jpeg_alpha.alpha); + } + img.onload = handle_draw_jpeg_onload; + img.src = tmpstr; + + return true; + } + else if (draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_BITMAP) + { + var canvas = this.surfaces[draw_copy.base.surface_id].canvas; + if (! draw_copy.data.src_bitmap.bitmap) + { + this.log_err('null bitmap'); + return false; + } + + var source_img = convert_spice_bitmap_to_web(canvas.context, + draw_copy.data.src_bitmap.bitmap); + if (! source_img) + { + this.log_warn('FIXME: Unable to interpret bitmap of format: ' + + draw_copy.data.src_bitmap.bitmap.format); + return false; + } + + return this.draw_copy_helper( + { base: draw_copy.base, + src_area: draw_copy.data.src_area, + image_data: source_img, + tag: 'bitmap.' + draw_copy.data.src_bitmap.bitmap.format, + has_alpha: draw_copy.data.src_bitmap.bitmap == Constants.SPICE_BITMAP_FMT_32BIT ? false : true, + descriptor : draw_copy.data.src_bitmap.descriptor + }); + } + else if (draw_copy.data.src_bitmap.descriptor.type == Constants.SPICE_IMAGE_TYPE_LZ_RGB) + { + var canvas = this.surfaces[draw_copy.base.surface_id].canvas; + if (! draw_copy.data.src_bitmap.lz_rgb) + { + this.log_err('null lz_rgb '); + return false; + } + + var source_img = convert_spice_lz_to_web(canvas.context, + draw_copy.data.src_bitmap.lz_rgb); + if (! source_img) + { + this.log_warn('FIXME: Unable to interpret bitmap of type: ' + + draw_copy.data.src_bitmap.lz_rgb.type); + return false; + } + + return this.draw_copy_helper( + { base: draw_copy.base, + src_area: draw_copy.data.src_area, + image_data: source_img, + tag: 'lz_rgb.' + draw_copy.data.src_bitmap.lz_rgb.type, + has_alpha: draw_copy.data.src_bitmap.lz_rgb.type == Constants.LZ_IMAGE_TYPE_RGBA ? true : false , + descriptor : draw_copy.data.src_bitmap.descriptor + }); + } + else + { + this.log_warn('FIXME: DrawCopy unhandled image type: ' + draw_copy.data.src_bitmap.descriptor.type); + this.log_draw('DrawCopy', draw_copy); + return false; + } + } + + this.log_warn('FIXME: DrawCopy no src_bitmap.'); + return false; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_FILL) + { + var draw_fill = new Messages.SpiceMsgDisplayDrawFill(msg.data); + + Utils.DEBUG > 1 && this.log_draw('DrawFill', draw_fill); + + if (draw_fill.data.rop_descriptor != Constants.SPICE_ROPD_OP_PUT) + this.log_warn('FIXME: DrawFill we don\'t handle ropd type: ' + draw_fill.data.rop_descriptor); + if (draw_fill.data.mask.flags) + this.log_warn('FIXME: DrawFill we don\'t handle mask flag: ' + draw_fill.data.mask.flags); + if (draw_fill.data.mask.bitmap) + this.log_warn('FIXME: DrawFill we don\'t handle mask'); + + if (draw_fill.data.brush.type == Constants.SPICE_BRUSH_TYPE_SOLID) + { + // FIXME - do brushes ever have alpha? + var color = draw_fill.data.brush.color & 0xffffff; + var color_str = 'rgb(' + (color >> 16) + ', ' + ((color >> 8) & 0xff) + ', ' + (color & 0xff) + ')'; + this.surfaces[draw_fill.base.surface_id].canvas.context.fillStyle = color_str; + + this.surfaces[draw_fill.base.surface_id].canvas.context.fillRect( + draw_fill.base.box.left, draw_fill.base.box.top, + draw_fill.base.box.right - draw_fill.base.box.left, + draw_fill.base.box.bottom - draw_fill.base.box.top); + + if (Utils.DUMP_DRAWS && this.parent.dump_id) + { + var debug_canvas = document.createElement('canvas'); + debug_canvas.setAttribute('width', this.surfaces[draw_fill.base.surface_id].canvas.width); + debug_canvas.setAttribute('height', this.surfaces[draw_fill.base.surface_id].canvas.height); + debug_canvas.setAttribute('id', 'fillbrush.' + draw_fill.base.surface_id + '.' + this.surfaces[draw_fill.base.surface_id].draw_count); + debug_canvas.getContext('2d').fillStyle = color_str; + debug_canvas.getContext('2d').fillRect( + draw_fill.base.box.left, draw_fill.base.box.top, + draw_fill.base.box.right - draw_fill.base.box.left, + draw_fill.base.box.bottom - draw_fill.base.box.top); + document.getElementById(this.parent.dump_id).appendChild(debug_canvas); + } + + this.surfaces[draw_fill.base.surface_id].draw_count++; + + } + else + { + this.log_warn('FIXME: DrawFill can\'t handle brush type: ' + draw_fill.data.brush.type); + } + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_OPAQUE) + { + this.known_unimplemented(msg.type, 'Display Draw Opaque'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_BLEND) + { + this.known_unimplemented(msg.type, 'Display Draw Blend'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_BLACKNESS) + { + this.known_unimplemented(msg.type, 'Display Draw Blackness'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_WHITENESS) + { + this.known_unimplemented(msg.type, 'Display Draw Whiteness'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_INVERS) + { + this.known_unimplemented(msg.type, 'Display Draw Invers'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_ROP3) + { + this.known_unimplemented(msg.type, 'Display Draw ROP3'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_STROKE) + { + this.known_unimplemented(msg.type, 'Display Draw Stroke'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_TRANSPARENT) + { + this.known_unimplemented(msg.type, 'Display Draw Transparent'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_ALPHA_BLEND) + { + this.known_unimplemented(msg.type, 'Display Draw Alpha Blend'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_COPY_BITS) + { + var copy_bits = new Messages.SpiceMsgDisplayCopyBits(msg.data); + + Utils.DEBUG > 1 && this.log_draw('CopyBits', copy_bits); + + var source_canvas = this.surfaces[copy_bits.base.surface_id].canvas; + var source_context = source_canvas.context; + + var width = source_canvas.width - copy_bits.src_pos.x; + var height = source_canvas.height - copy_bits.src_pos.y; + if (width > (copy_bits.base.box.right - copy_bits.base.box.left)) + width = copy_bits.base.box.right - copy_bits.base.box.left; + if (height > (copy_bits.base.box.bottom - copy_bits.base.box.top)) + height = copy_bits.base.box.bottom - copy_bits.base.box.top; + + var source_img = source_context.getImageData( + copy_bits.src_pos.x, copy_bits.src_pos.y, width, height); + //source_context.putImageData(source_img, copy_bits.base.box.left, copy_bits.base.box.top); + putImageDataWithAlpha(source_context, source_img, copy_bits.base.box.left, copy_bits.base.box.top); + + if (Utils.DUMP_DRAWS && this.parent.dump_id) + { + var debug_canvas = document.createElement('canvas'); + debug_canvas.setAttribute('width', width); + debug_canvas.setAttribute('height', height); + debug_canvas.setAttribute('id', 'copybits' + copy_bits.base.surface_id + '.' + this.surfaces[copy_bits.base.surface_id].draw_count); + debug_canvas.getContext('2d').putImageData(source_img, 0, 0); + document.getElementById(this.parent.dump_id).appendChild(debug_canvas); + } + + + this.surfaces[copy_bits.base.surface_id].draw_count++; + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_INVAL_ALL_PIXMAPS) + { + this.known_unimplemented(msg.type, 'Display Inval All Pixmaps'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_INVAL_PALETTE) + { + this.known_unimplemented(msg.type, 'Display Inval Palette'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_INVAL_ALL_PALETTES) + { + this.known_unimplemented(msg.type, 'Inval All Palettes'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_SURFACE_CREATE) + { + if (! ('surfaces' in this)) + this.surfaces = []; + + var m = new Messages.SpiceMsgSurfaceCreate(msg.data); + Utils.DEBUG > 1 && console.log(this.type + ': MsgSurfaceCreate id ' + m.surface.surface_id + + '; ' + m.surface.width + 'x' + m.surface.height + + '; format ' + m.surface.format + + '; flags ' + m.surface.flags); + if (m.surface.format != Constants.SPICE_SURFACE_FMT_32_xRGB && + m.surface.format != Constants.SPICE_SURFACE_FMT_32_ARGB) + { + this.log_warn('FIXME: cannot handle surface format ' + m.surface.format + ' yet.'); + return false; + } + + var canvas = document.createElement('canvas'); + canvas.setAttribute('width', m.surface.width); + canvas.setAttribute('height', m.surface.height); + canvas.setAttribute('id', 'spice_surface_' + m.surface.surface_id); + canvas.setAttribute('tabindex', m.surface.surface_id); + canvas.context = canvas.getContext('2d'); + + if (Utils.DUMP_CANVASES && this.parent.dump_id) + document.getElementById(this.parent.dump_id).appendChild(canvas); + + m.surface.canvas = canvas; + m.surface.draw_count = 0; + this.surfaces[m.surface.surface_id] = m.surface; + + if (m.surface.flags & Constants.SPICE_SURFACE_FLAGS_PRIMARY) + { + this.primary_surface = m.surface.surface_id; + + /* This .save() is done entirely to enable SPICE_MSG_DISPLAY_RESET */ + canvas.context.save(); + document.getElementById(this.parent.screen_id).appendChild(canvas); + + /* We're going to leave width dynamic, but correctly set the height */ + document.getElementById(this.parent.screen_id).style.height = m.surface.height + 'px'; + this.hook_events(); + } + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_SURFACE_DESTROY) + { + var m = new Messages.SpiceMsgSurfaceDestroy(msg.data); + Utils.DEBUG > 1 && console.log(this.type + ': MsgSurfaceDestroy id ' + m.surface_id); + this.delete_surface(m.surface_id); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_CREATE) + { + var m = new Messages.SpiceMsgDisplayStreamCreate(msg.data); + Utils.STREAM_DEBUG > 0 && console.log(this.type + ': MsgStreamCreate id' + m.id + '; type ' + m.codec_type + + '; width ' + m.stream_width + '; height ' + m.stream_height + + '; left ' + m.dest.left + '; top ' + m.dest.top + ); + if (!this.streams) + this.streams = []; + if (this.streams[m.id]) + console.log('Stream ' + m.id + ' already exists'); + else + this.streams[m.id] = m; + + if (m.codec_type == Constants.SPICE_VIDEO_CODEC_TYPE_VP8) + { + var media = new MediaSource(); + var v = document.createElement('video'); + v.src = window.URL.createObjectURL(media); + + v.setAttribute('muted', true); + v.setAttribute('autoplay', true); + v.setAttribute('width', m.stream_width); + v.setAttribute('height', m.stream_height); + + var left = m.dest.left; + var top = m.dest.top; + if (this.surfaces[m.surface_id] !== undefined) + { + left += this.surfaces[m.surface_id].canvas.offsetLeft; + top += this.surfaces[m.surface_id].canvas.offsetTop; + } + document.getElementById(this.parent.screen_id).appendChild(v); + v.setAttribute('style', 'pointer-events:none; position: absolute; top:' + top + 'px; left:' + left + 'px;'); + + media.addEventListener('sourceopen', handle_video_source_open, false); + media.addEventListener('sourceended', handle_video_source_ended, false); + media.addEventListener('sourceclosed', handle_video_source_closed, false); + + var s = this.streams[m.id]; + s.video = v; + s.media = media; + s.queue = []; + s.start_time = 0; + s.cluster_time = 0; + s.append_okay = false; + + media.stream = s; + media.spiceconn = this; + v.spice_stream = s; + } + else if (m.codec_type == Constants.SPICE_VIDEO_CODEC_TYPE_MJPEG) + this.streams[m.id].frames_loading = 0; + else + console.log('Unhandled stream codec: '+m.codec_type); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_DATA || + msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) + { + var m; + if (msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) + m = new Messages.SpiceMsgDisplayStreamDataSized(msg.data); + else + m = new Messages.SpiceMsgDisplayStreamData(msg.data); + + if (!this.streams[m.base.id]) + { + console.log('no stream for data'); + return false; + } + + var time_until_due = m.base.multi_media_time - this.parent.relative_now(); + + if (this.streams[m.base.id].codec_type === Constants.SPICE_VIDEO_CODEC_TYPE_MJPEG) + process_mjpeg_stream_data(this, m, time_until_due); + + if (this.streams[m.base.id].codec_type === Constants.SPICE_VIDEO_CODEC_TYPE_VP8) + process_video_stream_data(this.streams[m.base.id], m); + + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_ACTIVATE_REPORT) + { + var m = new Messages.SpiceMsgDisplayStreamActivateReport(msg.data); + + var report = new Messages.SpiceMsgcDisplayStreamReport(m.stream_id, m.unique_id); + if (this.streams[m.stream_id]) + { + this.streams[m.stream_id].report = report; + this.streams[m.stream_id].max_window_size = m.max_window_size; + this.streams[m.stream_id].timeout_ms = m.timeout_ms; + } + + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_CLIP) + { + var m = new Messages.SpiceMsgDisplayStreamClip(msg.data); + Utils.STREAM_DEBUG > 1 && console.log(this.type + ': MsgStreamClip id' + m.id); + this.streams[m.id].clip = m.clip; + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_DESTROY) + { + var m = new Messages.SpiceMsgDisplayStreamDestroy(msg.data); + Utils.STREAM_DEBUG > 0 && console.log(this.type + ': MsgStreamDestroy id' + m.id); + + if (this.streams[m.id].codec_type == Constants.SPICE_VIDEO_CODEC_TYPE_VP8) + { + document.getElementById(this.parent.screen_id).removeChild(this.streams[m.id].video); + this.streams[m.id].source_buffer = null; + this.streams[m.id].media = null; + this.streams[m.id].video = null; + } + this.streams[m.id] = undefined; + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_STREAM_DESTROY_ALL) + { + this.known_unimplemented(msg.type, 'Display Stream Destroy All'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_INVAL_LIST) + { + var m = new Messages.SpiceMsgDisplayInvalList(msg.data); + var i; + Utils.DEBUG > 1 && console.log(this.type + ': MsgInvalList ' + m.count + ' items'); + for (i = 0; i < m.count; i++) + if (this.cache[m.resources[i].id] != undefined) + delete this.cache[m.resources[i].id]; + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_MONITORS_CONFIG) + { + this.known_unimplemented(msg.type, 'Display Monitors Config'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_DISPLAY_DRAW_COMPOSITE) + { + this.known_unimplemented(msg.type, 'Display Draw Composite'); + return true; + } + + return false; +}; + +SpiceDisplayConn.prototype.delete_surface = function(surface_id) +{ + var canvas = document.getElementById('spice_surface_' + surface_id); + if (Utils.DUMP_CANVASES && this.parent.dump_id) + document.getElementById(this.parent.dump_id).removeChild(canvas); + if (this.primary_surface == surface_id) + { + this.unhook_events(); + this.primary_surface = undefined; + document.getElementById(this.parent.screen_id)?.removeChild(canvas); + } + + delete this.surfaces[surface_id]; +}; + + +SpiceDisplayConn.prototype.draw_copy_helper = function(o) +{ + + var canvas = this.surfaces[o.base.surface_id].canvas; + if (o.has_alpha) + { + /* FIXME - This is based on trial + error, not a serious thoughtful + analysis of what Spice requires. See display.js for more. */ + if (this.surfaces[o.base.surface_id].format == Constants.SPICE_SURFACE_FMT_32_xRGB) + { + stripAlpha(o.image_data); + canvas.context.putImageData(o.image_data, o.base.box.left, o.base.box.top); + } + else + putImageDataWithAlpha(canvas.context, o.image_data, + o.base.box.left, o.base.box.top); + } + else + canvas.context.putImageData(o.image_data, o.base.box.left, o.base.box.top); + + if (o.src_area.left > 0 || o.src_area.top > 0) + { + this.log_warn('FIXME: DrawCopy not shifting draw copies just yet...'); + } + + if (o.descriptor && (o.descriptor.flags & Constants.SPICE_IMAGE_FLAGS_CACHE_ME)) + { + if (! ('cache' in this)) + this.cache = {}; + this.cache[o.descriptor.id] = o.image_data; + } + + if (Utils.DUMP_DRAWS && this.parent.dump_id) + { + var debug_canvas = document.createElement('canvas'); + debug_canvas.setAttribute('width', o.image_data.width); + debug_canvas.setAttribute('height', o.image_data.height); + debug_canvas.setAttribute('id', o.tag + '.' + + this.surfaces[o.base.surface_id].draw_count + '.' + + o.base.surface_id + '@' + o.base.box.left + 'x' + o.base.box.top); + debug_canvas.getContext('2d').putImageData(o.image_data, 0, 0); + document.getElementById(this.parent.dump_id).appendChild(debug_canvas); + } + + this.surfaces[o.base.surface_id].draw_count++; + + return true; +}; + + +SpiceDisplayConn.prototype.log_draw = function(prefix, draw) +{ + var str = prefix + '.' + draw.base.surface_id + '.' + this.surfaces[draw.base.surface_id].draw_count + ': '; + str += 'base.box ' + draw.base.box.left + ', ' + draw.base.box.top + ' to ' + + draw.base.box.right + ', ' + draw.base.box.bottom; + str += '; clip.type ' + draw.base.clip.type; + + if (draw.data) + { + if (draw.data.src_area) + str += '; src_area ' + draw.data.src_area.left + ', ' + draw.data.src_area.top + ' to ' + + draw.data.src_area.right + ', ' + draw.data.src_area.bottom; + + if (draw.data.src_bitmap && draw.data.src_bitmap != null) + { + str += '; src_bitmap id: ' + draw.data.src_bitmap.descriptor.id; + str += '; src_bitmap width ' + draw.data.src_bitmap.descriptor.width + ', height ' + draw.data.src_bitmap.descriptor.height; + str += '; src_bitmap type ' + draw.data.src_bitmap.descriptor.type + ', flags ' + draw.data.src_bitmap.descriptor.flags; + if (draw.data.src_bitmap.surface_id !== undefined) + str += '; src_bitmap surface_id ' + draw.data.src_bitmap.surface_id; + if (draw.data.src_bitmap.bitmap) + str += '; BITMAP format ' + draw.data.src_bitmap.bitmap.format + + '; flags ' + draw.data.src_bitmap.bitmap.flags + + '; x ' + draw.data.src_bitmap.bitmap.x + + '; y ' + draw.data.src_bitmap.bitmap.y + + '; stride ' + draw.data.src_bitmap.bitmap.stride ; + if (draw.data.src_bitmap.quic) + str += '; QUIC type ' + draw.data.src_bitmap.quic.type + + '; width ' + draw.data.src_bitmap.quic.width + + '; height ' + draw.data.src_bitmap.quic.height ; + if (draw.data.src_bitmap.lz_rgb) + str += '; LZ_RGB length ' + draw.data.src_bitmap.lz_rgb.length + + '; magic ' + draw.data.src_bitmap.lz_rgb.magic + + '; version 0x' + draw.data.src_bitmap.lz_rgb.version.toString(16) + + '; type ' + draw.data.src_bitmap.lz_rgb.type + + '; width ' + draw.data.src_bitmap.lz_rgb.width + + '; height ' + draw.data.src_bitmap.lz_rgb.height + + '; stride ' + draw.data.src_bitmap.lz_rgb.stride + + '; top down ' + draw.data.src_bitmap.lz_rgb.top_down; + } + else + str += '; src_bitmap is null'; + + if (draw.data.brush) + { + if (draw.data.brush.type == Constants.SPICE_BRUSH_TYPE_SOLID) + str += '; brush.color 0x' + draw.data.brush.color.toString(16); + if (draw.data.brush.type == Constants.SPICE_BRUSH_TYPE_PATTERN) + { + str += '; brush.pat '; + if (draw.data.brush.pattern.pat != null) + str += '[SpiceImage]'; + else + str += '[null]'; + str += ' at ' + draw.data.brush.pattern.pos.x + ', ' + draw.data.brush.pattern.pos.y; + } + } + + str += '; rop_descriptor ' + draw.data.rop_descriptor; + if (draw.data.scale_mode !== undefined) + str += '; scale_mode ' + draw.data.scale_mode; + str += '; mask.flags ' + draw.data.mask.flags; + str += '; mask.pos ' + draw.data.mask.pos.x + ', ' + draw.data.mask.pos.y; + if (draw.data.mask.bitmap != null) + { + str += '; mask.bitmap width ' + draw.data.mask.bitmap.descriptor.width + ', height ' + draw.data.mask.bitmap.descriptor.height; + str += '; mask.bitmap type ' + draw.data.mask.bitmap.descriptor.type + ', flags ' + draw.data.mask.bitmap.descriptor.flags; + } + else + str += '; mask.bitmap is null'; + } + + console.log(str); +}; + +SpiceDisplayConn.prototype.hook_events = function() +{ + if (this.primary_surface !== undefined) + { + var canvas = this.surfaces[this.primary_surface].canvas; + canvas.sc = this.parent; + canvas.addEventListener('mousemove', Inputs.handle_mousemove); + canvas.addEventListener('mousedown', Inputs.handle_mousedown); + canvas.addEventListener('contextmenu', Inputs.handle_contextmenu); + canvas.addEventListener('mouseup', Inputs.handle_mouseup); + canvas.addEventListener('keydown', Inputs.handle_keydown); + canvas.addEventListener('keyup', Inputs.handle_keyup); + canvas.addEventListener('mouseout', handle_mouseout); + canvas.addEventListener('mouseover', handle_mouseover); + canvas.addEventListener('wheel', Inputs.handle_mousewheel); + canvas.focus(); + } +}; + +SpiceDisplayConn.prototype.unhook_events = function() +{ + if (this.primary_surface !== undefined) + { + var canvas = this.surfaces[this.primary_surface].canvas; + canvas.removeEventListener('mousemove', Inputs.handle_mousemove); + canvas.removeEventListener('mousedown', Inputs.handle_mousedown); + canvas.removeEventListener('contextmenu', Inputs.handle_contextmenu); + canvas.removeEventListener('mouseup', Inputs.handle_mouseup); + canvas.removeEventListener('keydown', Inputs.handle_keydown); + canvas.removeEventListener('keyup', Inputs.handle_keyup); + canvas.removeEventListener('mouseout', handle_mouseout); + canvas.removeEventListener('mouseover', handle_mouseover); + canvas.removeEventListener('wheel', Inputs.handle_mousewheel); + } +}; + + +SpiceDisplayConn.prototype.destroy_surfaces = function() +{ + for (var s in this.surfaces) + { + this.delete_surface(this.surfaces[s].surface_id); + } + + this.surfaces = undefined; +}; + + +function handle_mouseover(e) +{ + this.focus(); +} + +function handle_mouseout(e) +{ + if (this.sc && this.sc.cursor && this.sc.cursor.spice_simulated_cursor) + this.sc.cursor.spice_simulated_cursor.style.display = 'none'; + this.blur(); +} + +function handle_draw_jpeg_onload() +{ + var temp_canvas = null; + var context; + + if ('streams' in this.o.sc && this.o.sc.streams[this.o.id]) + this.o.sc.streams[this.o.id].frames_loading--; + + /*------------------------------------------------------------ + ** FIXME: + ** The helper should be extended to be able to handle actual HtmlImageElements + ** ...and the cache should be modified to do so as well + **----------------------------------------------------------*/ + if (this.o.sc.surfaces[this.o.base.surface_id] === undefined) + { + // This can happen; if the jpeg image loads after our surface + // has been destroyed (e.g. open a menu, close it quickly), + // we'll find we have no surface. + Utils.DEBUG > 2 && this.o.sc.log_info('Discarding jpeg; presumed lost surface ' + this.o.base.surface_id); + temp_canvas = document.createElement('canvas'); + temp_canvas.setAttribute('width', this.o.base.box.right); + temp_canvas.setAttribute('height', this.o.base.box.bottom); + context = temp_canvas.getContext('2d'); + } + else + context = this.o.sc.surfaces[this.o.base.surface_id].canvas.context; + + if (this.alpha_img) + { + var c = document.createElement('canvas'); + var t = c.getContext('2d'); + c.setAttribute('width', this.alpha_img.width); + c.setAttribute('height', this.alpha_img.height); + t.putImageData(this.alpha_img, 0, 0); + t.globalCompositeOperation = 'source-in'; + t.drawImage(this, 0, 0); + + context.drawImage(c, this.o.base.box.left, this.o.base.box.top); + + if (this.o.descriptor && + (this.o.descriptor.flags & Constants.SPICE_IMAGE_FLAGS_CACHE_ME)) + { + if (! ('cache' in this.o.sc)) + this.o.sc.cache = {}; + + this.o.sc.cache[this.o.descriptor.id] = + t.getImageData(0, 0, + this.alpha_img.width, + this.alpha_img.height); + } + } + else + { + context.drawImage(this, this.o.base.box.left, this.o.base.box.top); + + // Give the Garbage collector a clue to recycle this; avoids + // fairly massive memory leaks during video playback + this.onload = undefined; + this.src = Utils.EMPTY_GIF_IMAGE; + + if (this.o.descriptor && + (this.o.descriptor.flags & Constants.SPICE_IMAGE_FLAGS_CACHE_ME)) + { + if (! ('cache' in this.o.sc)) + this.o.sc.cache = {}; + + this.o.sc.cache[this.o.descriptor.id] = + context.getImageData(this.o.base.box.left, this.o.base.box.top, + this.o.base.box.right - this.o.base.box.left, + this.o.base.box.bottom - this.o.base.box.top); + } + } + + if (temp_canvas == null) + { + if (Utils.DUMP_DRAWS && this.o.sc.parent.dump_id) + { + var debug_canvas = document.createElement('canvas'); + debug_canvas.setAttribute('id', this.o.tag + '.' + + this.o.sc.surfaces[this.o.base.surface_id].draw_count + '.' + + this.o.base.surface_id + '@' + this.o.base.box.left + 'x' + this.o.base.box.top); + debug_canvas.getContext('2d').drawImage(this, 0, 0); + document.getElementById(this.o.sc.parent.dump_id).appendChild(debug_canvas); + } + + this.o.sc.surfaces[this.o.base.surface_id].draw_count++; + } + + if (this.o.sc.streams[this.o.id] && 'report' in this.o.sc.streams[this.o.id]) + process_stream_data_report(this.o.sc, this.o.id, this.o.msg_mmtime, this.o.msg_mmtime - this.o.sc.parent.relative_now()); +} + +function process_mjpeg_stream_data(sc, m, time_until_due) +{ + /* If we are currently processing an mjpeg frame when a new one arrives, + and the new one is 'late', drop the new frame. This helps the browsers + keep up, and provides rate control feedback as well */ + if (time_until_due < 0 && sc.streams[m.base.id].frames_loading > 0) + { + if ('report' in sc.streams[m.base.id]) + sc.streams[m.base.id].report.num_drops++; + return; + } + + var tmpstr = 'data:image/jpeg,'; + var img = new Image(); + var i; + for (i = 0; i < m.data.length; i++) + { + tmpstr += '%'; + if (m.data[i] < 16) + tmpstr += '0'; + tmpstr += m.data[i].toString(16); + } + var strm_base = new Messages.SpiceMsgDisplayBase(); + strm_base.surface_id = sc.streams[m.base.id].surface_id; + strm_base.box = m.dest || sc.streams[m.base.id].dest; + strm_base.clip = sc.streams[m.base.id].clip; + img.o = + { base: strm_base, + tag: 'mjpeg.' + m.base.id, + descriptor: null, + sc : sc, + id : m.base.id, + msg_mmtime : m.base.multi_media_time + }; + img.onload = handle_draw_jpeg_onload; + img.src = tmpstr; + + sc.streams[m.base.id].frames_loading++; +} + +function process_stream_data_report(sc, id, msg_mmtime, time_until_due) +{ + sc.streams[id].report.num_frames++; + if (sc.streams[id].report.start_frame_mm_time == 0) + sc.streams[id].report.start_frame_mm_time = msg_mmtime; + + if (sc.streams[id].report.num_frames > sc.streams[id].max_window_size || + (msg_mmtime - sc.streams[id].report.start_frame_mm_time) > sc.streams[id].timeout_ms) + { + sc.streams[id].report.end_frame_mm_time = msg_mmtime; + sc.streams[id].report.last_frame_delay = time_until_due; + + var msg = new Messages.SpiceMiniData(); + msg.build_msg(Constants.SPICE_MSGC_DISPLAY_STREAM_REPORT, sc.streams[id].report); + sc.send_msg(msg); + + sc.streams[id].report.start_frame_mm_time = 0; + sc.streams[id].report.num_frames = 0; + sc.streams[id].report.num_drops = 0; + } +} + +function handle_video_source_open(e) +{ + var stream = this.stream; + var p = this.spiceconn; + + if (stream.source_buffer) + return; + + var s = this.addSourceBuffer(Webm.Constants.SPICE_VP8_CODEC); + if (! s) + { + p.log_err('Codec ' + Webm.Constants.SPICE_VP8_CODEC + ' not available.'); + return; + } + + stream.source_buffer = s; + s.spiceconn = p; + s.stream = stream; + + listen_for_video_events(stream); + + var h = new Webm.Header(); + var te = new Webm.VideoTrackEntry(this.stream.stream_width, this.stream.stream_height); + var t = new Webm.Tracks(te); + + var mb = new ArrayBuffer(h.buffer_size() + t.buffer_size()); + + var b = h.to_buffer(mb); + t.to_buffer(mb, b); + + s.addEventListener('error', handle_video_buffer_error, false); + s.addEventListener('updateend', handle_append_video_buffer_done, false); + + append_video_buffer(s, mb); +} + +function handle_video_source_ended(e) +{ + var p = this.spiceconn; + p.log_err('Video source unexpectedly ended.'); +} + +function handle_video_source_closed(e) +{ + var p = this.spiceconn; + p.log_err('Video source unexpectedly closed.'); +} + +function append_video_buffer(sb, mb) +{ + try + { + sb.stream.append_okay = false; + sb.appendBuffer(mb); + } + catch (e) + { + var p = sb.spiceconn; + p.log_err('Error invoking appendBuffer: ' + e.message); + } +} + +function handle_append_video_buffer_done(e) +{ + var stream = this.stream; + + if (stream.current_frame && 'report' in stream) + { + var sc = this.stream.media.spiceconn; + var t = this.stream.current_frame.msg_mmtime; + process_stream_data_report(sc, stream.id, t, t - sc.parent.relative_now()); + } + + if (stream.queue.length > 0) + { + stream.current_frame = stream.queue.shift(); + append_video_buffer(stream.source_buffer, stream.current_frame.mb); + } + else + { + stream.append_okay = true; + } + + if (!stream.video) + { + if (Utils.STREAM_DEBUG > 0) + console.log('Stream id ' + stream.id + ' received updateend after video is gone.'); + return; + } + + if (stream.video.buffered.length > 0 && + stream.video.currentTime < stream.video.buffered.start(stream.video.buffered.length - 1)) + { + console.log('Video appears to have fallen behind; advancing to ' + + stream.video.buffered.start(stream.video.buffered.length - 1)); + stream.video.currentTime = stream.video.buffered.start(stream.video.buffered.length - 1); + } + + /* Modern browsers try not to auto play video. */ + if (this.stream.video.paused && this.stream.video.readyState >= 2) + var promise = this.stream.video.play(); + + if (Utils.STREAM_DEBUG > 1) + console.log(stream.video.currentTime + ':id ' + stream.id + ' updateend ' + Utils.dump_media_element(stream.video)); +} + +function handle_video_buffer_error(e) +{ + var p = this.spiceconn; + p.log_err('source_buffer error ' + e.message); +} + +function push_or_queue(stream, msg, mb) +{ + var frame = + { + msg_mmtime : msg.base.multi_media_time + }; + + if (stream.append_okay) + { + stream.current_frame = frame; + append_video_buffer(stream.source_buffer, mb); + } + else + { + frame.mb = mb; + stream.queue.push(frame); + } +} + +function video_simple_block(stream, msg, keyframe) +{ + var simple = new Webm.SimpleBlock(msg.base.multi_media_time - stream.cluster_time, msg.data, keyframe); + var mb = new ArrayBuffer(simple.buffer_size()); + simple.to_buffer(mb); + + push_or_queue(stream, msg, mb); +} + +function new_video_cluster(stream, msg) +{ + stream.cluster_time = msg.base.multi_media_time; + var c = new Webm.Cluster(stream.cluster_time - stream.start_time, msg.data); + + var mb = new ArrayBuffer(c.buffer_size()); + c.to_buffer(mb); + + push_or_queue(stream, msg, mb); + + video_simple_block(stream, msg, true); +} + +function process_video_stream_data(stream, msg) +{ + if (stream.start_time == 0) + { + stream.start_time = msg.base.multi_media_time; + new_video_cluster(stream, msg); + } + + else if (msg.base.multi_media_time - stream.cluster_time >= Webm.Constants.MAX_CLUSTER_TIME) + new_video_cluster(stream, msg); + else + video_simple_block(stream, msg, false); +} + +function video_handle_event_debug(e) +{ + var s = this.spice_stream; + if (s.video) + { + if (Utils.STREAM_DEBUG > 0 || s.video.buffered.len > 1) + console.log(s.video.currentTime + ':id ' + s.id + ' event ' + e.type + + Utils.dump_media_element(s.video)); + } + + if (Utils.STREAM_DEBUG > 1 && s.media) + console.log(' media_source ' + Utils.dump_media_source(s.media)); + + if (Utils.STREAM_DEBUG > 1 && s.source_buffer) + console.log(' source_buffer ' + Utils.dump_source_buffer(s.source_buffer)); + + if (Utils.STREAM_DEBUG > 1 || s.queue.length > 1) + console.log(' queue len ' + s.queue.length + '; append_okay: ' + s.append_okay); +} + +function video_debug_listen_for_one_event(name) +{ + this.addEventListener(name, video_handle_event_debug); +} + +function listen_for_video_events(stream) +{ + var video_0_events = [ + 'abort', 'error' + ]; + + var video_1_events = [ + 'loadstart', 'suspend', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', + 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', + 'play', 'pause', 'ratechange' + ]; + + var video_2_events = [ + 'timeupdate', + 'progress', + 'resize', + 'volumechange' + ]; + + video_0_events.forEach(video_debug_listen_for_one_event, stream.video); + if (Utils.STREAM_DEBUG > 0) + video_1_events.forEach(video_debug_listen_for_one_event, stream.video); + if (Utils.STREAM_DEBUG > 1) + video_2_events.forEach(video_debug_listen_for_one_event, stream.video); +} + +export { + SpiceDisplayConn +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/enums.js b/packages/itmat-ui-react/src/components/lxd/spice/src/enums.js new file mode 100644 index 000000000..9f6cd14fe --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/enums.js @@ -0,0 +1,372 @@ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + + +/*---------------------------------------------------------------------------- +** enums.js +** 'constants' for Spice +**--------------------------------------------------------------------------*/ +export var Constants = { + SPICE_MAGIC: 'REDQ', + SPICE_VERSION_MAJOR: 2, + SPICE_VERSION_MINOR: 2, + + SPICE_CONNECT_TIMEOUT: (30 * 1000), + + SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION: 0, + SPICE_COMMON_CAP_AUTH_SPICE: 1, + SPICE_COMMON_CAP_AUTH_SASL: 2, + SPICE_COMMON_CAP_MINI_HEADER: 3, + + SPICE_TICKET_KEY_PAIR_LENGTH: 1024, + SPICE_TICKET_PUBKEY_BYTES: (1024 / 8 + 34), // (SPICE_TICKET_KEY_PAIR_LENGTH / 8 + 34) + + SPICE_LINK_ERR_OK: 0, + SPICE_LINK_ERR_ERROR: 1, + SPICE_LINK_ERR_INVALID_MAGIC: 2, + SPICE_LINK_ERR_INVALID_DATA: 3, + SPICE_LINK_ERR_VERSION_MISMATCH: 4, + SPICE_LINK_ERR_NEED_SECURED: 5, + SPICE_LINK_ERR_NEED_UNSECURED: 6, + SPICE_LINK_ERR_PERMISSION_DENIED: 7, + SPICE_LINK_ERR_BAD_CONNECTION_ID: 8, + SPICE_LINK_ERR_CHANNEL_NOT_AVAILABLE: 9, + + SPICE_MSG_MIGRATE: 1, + SPICE_MSG_MIGRATE_DATA: 2, + SPICE_MSG_SET_ACK: 3, + SPICE_MSG_PING: 4, + SPICE_MSG_WAIT_FOR_CHANNELS: 5, + SPICE_MSG_DISCONNECTING: 6, + SPICE_MSG_NOTIFY: 7, + SPICE_MSG_LIST: 8, + + SPICE_MSG_MAIN_MIGRATE_BEGIN: 101, + SPICE_MSG_MAIN_MIGRATE_CANCEL: 102, + SPICE_MSG_MAIN_INIT: 103, + SPICE_MSG_MAIN_CHANNELS_LIST: 104, + SPICE_MSG_MAIN_MOUSE_MODE: 105, + SPICE_MSG_MAIN_MULTI_MEDIA_TIME: 106, + SPICE_MSG_MAIN_AGENT_CONNECTED: 107, + SPICE_MSG_MAIN_AGENT_DISCONNECTED: 108, + SPICE_MSG_MAIN_AGENT_DATA: 109, + SPICE_MSG_MAIN_AGENT_TOKEN: 110, + SPICE_MSG_MAIN_MIGRATE_SWITCH_HOST: 111, + SPICE_MSG_MAIN_MIGRATE_END: 112, + SPICE_MSG_MAIN_NAME: 113, + SPICE_MSG_MAIN_UUID: 114, + SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS: 115, + SPICE_MSG_MAIN_MIGRATE_BEGIN_SEAMLESS: 116, + SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_ACK: 117, + SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_NACK: 118, + SPICE_MSG_END_MAIN: 119, + + + + SPICE_MSGC_ACK_SYNC: 1, + SPICE_MSGC_ACK: 2, + SPICE_MSGC_PONG: 3, + SPICE_MSGC_MIGRATE_FLUSH_MARK: 4, + SPICE_MSGC_MIGRATE_DATA: 5, + SPICE_MSGC_DISCONNECTING: 6, + + + SPICE_MSGC_MAIN_CLIENT_INFO: 101, + SPICE_MSGC_MAIN_MIGRATE_CONNECTED: 102, + SPICE_MSGC_MAIN_MIGRATE_CONNECT_ERROR: 103, + SPICE_MSGC_MAIN_ATTACH_CHANNELS: 104, + SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST: 105, + SPICE_MSGC_MAIN_AGENT_START: 106, + SPICE_MSGC_MAIN_AGENT_DATA: 107, + SPICE_MSGC_MAIN_AGENT_TOKEN: 108, + SPICE_MSGC_MAIN_MIGRATE_END: 109, + SPICE_MSGC_END_MAIN: 110, + + SPICE_MSG_DISPLAY_MODE: 101, + SPICE_MSG_DISPLAY_MARK: 102, + SPICE_MSG_DISPLAY_RESET: 103, + SPICE_MSG_DISPLAY_COPY_BITS: 104, + SPICE_MSG_DISPLAY_INVAL_LIST: 105, + SPICE_MSG_DISPLAY_INVAL_ALL_PIXMAPS: 106, + SPICE_MSG_DISPLAY_INVAL_PALETTE: 107, + SPICE_MSG_DISPLAY_INVAL_ALL_PALETTES: 108, + + SPICE_MSG_DISPLAY_STREAM_CREATE: 122, + SPICE_MSG_DISPLAY_STREAM_DATA: 123, + SPICE_MSG_DISPLAY_STREAM_CLIP: 124, + SPICE_MSG_DISPLAY_STREAM_DESTROY: 125, + SPICE_MSG_DISPLAY_STREAM_DESTROY_ALL: 126, + + SPICE_MSG_DISPLAY_DRAW_FILL: 302, + SPICE_MSG_DISPLAY_DRAW_OPAQUE: 303, + SPICE_MSG_DISPLAY_DRAW_COPY: 304, + SPICE_MSG_DISPLAY_DRAW_BLEND: 305, + SPICE_MSG_DISPLAY_DRAW_BLACKNESS: 306, + SPICE_MSG_DISPLAY_DRAW_WHITENESS: 307, + SPICE_MSG_DISPLAY_DRAW_INVERS: 308, + SPICE_MSG_DISPLAY_DRAW_ROP3: 309, + SPICE_MSG_DISPLAY_DRAW_STROKE: 310, + SPICE_MSG_DISPLAY_DRAW_TEXT: 311, + SPICE_MSG_DISPLAY_DRAW_TRANSPARENT: 312, + SPICE_MSG_DISPLAY_DRAW_ALPHA_BLEND: 313, + SPICE_MSG_DISPLAY_SURFACE_CREATE: 314, + SPICE_MSG_DISPLAY_SURFACE_DESTROY: 315, + SPICE_MSG_DISPLAY_STREAM_DATA_SIZED: 316, + SPICE_MSG_DISPLAY_MONITORS_CONFIG: 317, + SPICE_MSG_DISPLAY_DRAW_COMPOSITE: 318, + SPICE_MSG_DISPLAY_STREAM_ACTIVATE_REPORT: 319, + + SPICE_MSGC_DISPLAY_INIT: 101, + SPICE_MSGC_DISPLAY_STREAM_REPORT: 102, + + SPICE_MSG_INPUTS_INIT: 101, + SPICE_MSG_INPUTS_KEY_MODIFIERS: 102, + + SPICE_MSG_INPUTS_MOUSE_MOTION_ACK: 111, + + SPICE_MSGC_INPUTS_KEY_DOWN: 101, + SPICE_MSGC_INPUTS_KEY_UP: 102, + SPICE_MSGC_INPUTS_KEY_MODIFIERS: 103, + + SPICE_MSGC_INPUTS_MOUSE_MOTION: 111, + SPICE_MSGC_INPUTS_MOUSE_POSITION: 112, + SPICE_MSGC_INPUTS_MOUSE_PRESS: 113, + SPICE_MSGC_INPUTS_MOUSE_RELEASE: 114, + + SPICE_MSG_CURSOR_INIT: 101, + SPICE_MSG_CURSOR_RESET: 102, + SPICE_MSG_CURSOR_SET: 103, + SPICE_MSG_CURSOR_MOVE: 104, + SPICE_MSG_CURSOR_HIDE: 105, + SPICE_MSG_CURSOR_TRAIL: 106, + SPICE_MSG_CURSOR_INVAL_ONE: 107, + SPICE_MSG_CURSOR_INVAL_ALL: 108, + + SPICE_MSG_PLAYBACK_DATA: 101, + SPICE_MSG_PLAYBACK_MODE: 102, + SPICE_MSG_PLAYBACK_START: 103, + SPICE_MSG_PLAYBACK_STOP: 104, + SPICE_MSG_PLAYBACK_VOLUME: 105, + SPICE_MSG_PLAYBACK_MUTE: 106, + SPICE_MSG_PLAYBACK_LATENCY: 107, + + SPICE_MSG_SPICEVMC_DATA: 101, + SPICE_MSG_PORT_INIT: 201, + SPICE_MSG_PORT_EVENT: 202, + SPICE_MSG_END_PORT: 203, + + SPICE_MSGC_SPICEVMC_DATA: 101, + SPICE_MSGC_PORT_EVENT: 201, + SPICE_MSGC_END_PORT: 202, + + SPICE_PLAYBACK_CAP_CELT_0_5_1: 0, + SPICE_PLAYBACK_CAP_VOLUME: 1, + SPICE_PLAYBACK_CAP_LATENCY: 2, + SPICE_PLAYBACK_CAP_OPUS: 3, + + SPICE_MAIN_CAP_SEMI_SEAMLESS_MIGRATE: 0, + SPICE_MAIN_CAP_NAME_AND_UUID: 1, + SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS: 2, + SPICE_MAIN_CAP_SEAMLESS_MIGRATE: 3, + + SPICE_DISPLAY_CAP_SIZED_STREAM: 0, + SPICE_DISPLAY_CAP_MONITORS_CONFIG: 1, + SPICE_DISPLAY_CAP_COMPOSITE: 2, + SPICE_DISPLAY_CAP_A8_SURFACE: 3, + SPICE_DISPLAY_CAP_STREAM_REPORT: 4, + SPICE_DISPLAY_CAP_LZ4_COMPRESSION: 5, + SPICE_DISPLAY_CAP_PREF_COMPRESSION: 6, + SPICE_DISPLAY_CAP_GL_SCANOUT: 7, + SPICE_DISPLAY_CAP_MULTI_CODEC: 8, + SPICE_DISPLAY_CAP_CODEC_MJPEG: 9, + SPICE_DISPLAY_CAP_CODEC_VP8: 10, + + SPICE_AUDIO_DATA_MODE_INVALID: 0, + SPICE_AUDIO_DATA_MODE_RAW: 1, + SPICE_AUDIO_DATA_MODE_CELT_0_5_1: 2, + SPICE_AUDIO_DATA_MODE_OPUS: 3, + + SPICE_AUDIO_FMT_INVALID: 0, + SPICE_AUDIO_FMT_S16: 1, + + SPICE_CHANNEL_MAIN: 1, + SPICE_CHANNEL_DISPLAY: 2, + SPICE_CHANNEL_INPUTS: 3, + SPICE_CHANNEL_CURSOR: 4, + SPICE_CHANNEL_PLAYBACK: 5, + SPICE_CHANNEL_RECORD: 6, + SPICE_CHANNEL_TUNNEL: 7, + SPICE_CHANNEL_SMARTCARD: 8, + SPICE_CHANNEL_USBREDIR: 9, + SPICE_CHANNEL_PORT: 10, + SPICE_CHANNEL_WEBDAV: 11, + + SPICE_SURFACE_FLAGS_PRIMARY: (1 << 0), + + SPICE_NOTIFY_SEVERITY_INFO: 0, + SPICE_NOTIFY_SEVERITY_WARN: 1, + SPICE_NOTIFY_SEVERITY_ERROR: 2, + + SPICE_MOUSE_MODE_SERVER: (1 << 0), + SPICE_MOUSE_MODE_CLIENT: (1 << 1), + SPICE_MOUSE_MODE_MASK: 0x3, + + SPICE_CLIP_TYPE_NONE: 0, + SPICE_CLIP_TYPE_RECTS: 1, + + SPICE_IMAGE_TYPE_BITMAP: 0, + SPICE_IMAGE_TYPE_QUIC: 1, + SPICE_IMAGE_TYPE_RESERVED: 2, + SPICE_IMAGE_TYPE_LZ_PLT: 100, + SPICE_IMAGE_TYPE_LZ_RGB: 101, + SPICE_IMAGE_TYPE_GLZ_RGB: 102, + SPICE_IMAGE_TYPE_FROM_CACHE: 103, + SPICE_IMAGE_TYPE_SURFACE: 104, + SPICE_IMAGE_TYPE_JPEG: 105, + SPICE_IMAGE_TYPE_FROM_CACHE_LOSSLESS: 106, + SPICE_IMAGE_TYPE_ZLIB_GLZ_RGB: 107, + SPICE_IMAGE_TYPE_JPEG_ALPHA: 108, + + SPICE_IMAGE_FLAGS_CACHE_ME: (1 << 0), + SPICE_IMAGE_FLAGS_HIGH_BITS_SET: (1 << 1), + SPICE_IMAGE_FLAGS_CACHE_REPLACE_ME: (1 << 2), + + SPICE_BITMAP_FLAGS_PAL_CACHE_ME: (1 << 0), + SPICE_BITMAP_FLAGS_PAL_FROM_CACHE: (1 << 1), + SPICE_BITMAP_FLAGS_TOP_DOWN: (1 << 2), + SPICE_BITMAP_FLAGS_MASK: 0x7, + + SPICE_BITMAP_FMT_INVALID: 0, + SPICE_BITMAP_FMT_1BIT_LE: 1, + SPICE_BITMAP_FMT_1BIT_BE: 2, + SPICE_BITMAP_FMT_4BIT_LE: 3, + SPICE_BITMAP_FMT_4BIT_BE: 4, + SPICE_BITMAP_FMT_8BIT: 5, + SPICE_BITMAP_FMT_16BIT: 6, + SPICE_BITMAP_FMT_24BIT: 7, + SPICE_BITMAP_FMT_32BIT: 8, + SPICE_BITMAP_FMT_RGBA: 9, + + + SPICE_CURSOR_FLAGS_NONE: (1 << 0), + SPICE_CURSOR_FLAGS_CACHE_ME: (1 << 1), + SPICE_CURSOR_FLAGS_FROM_CACHE: (1 << 2), + SPICE_CURSOR_FLAGS_MASK: 0x7, + + SPICE_MOUSE_BUTTON_MASK_LEFT: (1 << 0), + SPICE_MOUSE_BUTTON_MASK_MIDDLE: (1 << 1), + SPICE_MOUSE_BUTTON_MASK_RIGHT: (1 << 2), + SPICE_MOUSE_BUTTON_MASK_MASK: 0x7, + + SPICE_MOUSE_BUTTON_INVALID: 0, + SPICE_MOUSE_BUTTON_LEFT: 1, + SPICE_MOUSE_BUTTON_MIDDLE: 2, + SPICE_MOUSE_BUTTON_RIGHT: 3, + SPICE_MOUSE_BUTTON_UP: 4, + SPICE_MOUSE_BUTTON_DOWN: 5, + + SPICE_BRUSH_TYPE_NONE: 0, + SPICE_BRUSH_TYPE_SOLID: 1, + SPICE_BRUSH_TYPE_PATTERN: 2, + + SPICE_SURFACE_FMT_INVALID: 0, + SPICE_SURFACE_FMT_1_A: 1, + SPICE_SURFACE_FMT_8_A: 8, + SPICE_SURFACE_FMT_16_555: 16, + SPICE_SURFACE_FMT_32_xRGB: 32, + SPICE_SURFACE_FMT_16_565: 80, + SPICE_SURFACE_FMT_32_ARGB: 96, + + SPICE_ROPD_INVERS_SRC: (1 << 0), + SPICE_ROPD_INVERS_BRUSH: (1 << 1), + SPICE_ROPD_INVERS_DEST: (1 << 2), + SPICE_ROPD_OP_PUT: (1 << 3), + SPICE_ROPD_OP_OR: (1 << 4), + SPICE_ROPD_OP_AND: (1 << 5), + SPICE_ROPD_OP_XOR: (1 << 6), + SPICE_ROPD_OP_BLACKNESS: (1 << 7), + SPICE_ROPD_OP_WHITENESS: (1 << 8), + SPICE_ROPD_OP_INVERS: (1 << 9), + SPICE_ROPD_INVERS_RES: (1 << 10), + SPICE_ROPD_MASK: 0x7ff, + + LZ_IMAGE_TYPE_INVALID: 0, + LZ_IMAGE_TYPE_PLT1_LE: 1, + LZ_IMAGE_TYPE_PLT1_BE: 2, // PLT stands for palette + LZ_IMAGE_TYPE_PLT4_LE: 3, + LZ_IMAGE_TYPE_PLT4_BE: 4, + LZ_IMAGE_TYPE_PLT8: 5, + LZ_IMAGE_TYPE_RGB16: 6, + LZ_IMAGE_TYPE_RGB24: 7, + LZ_IMAGE_TYPE_RGB32: 8, + LZ_IMAGE_TYPE_RGBA: 9, + LZ_IMAGE_TYPE_XXXA: 10, + + + SPICE_INPUT_MOTION_ACK_BUNCH: 4, + + + SPICE_CURSOR_TYPE_ALPHA: 0, + SPICE_CURSOR_TYPE_MONO: 1, + SPICE_CURSOR_TYPE_COLOR4: 2, + SPICE_CURSOR_TYPE_COLOR8: 3, + SPICE_CURSOR_TYPE_COLOR16: 4, + SPICE_CURSOR_TYPE_COLOR24: 5, + SPICE_CURSOR_TYPE_COLOR32: 6, + + SPICE_VIDEO_CODEC_TYPE_MJPEG: 1, + SPICE_VIDEO_CODEC_TYPE_VP8: 2, + + VD_AGENT_PROTOCOL: 1, + VD_AGENT_MAX_DATA_SIZE: 2048, + + VD_AGENT_MOUSE_STATE: 1, + VD_AGENT_MONITORS_CONFIG: 2, + VD_AGENT_REPLY: 3, + VD_AGENT_CLIPBOARD: 4, + VD_AGENT_DISPLAY_CONFIG: 5, + VD_AGENT_ANNOUNCE_CAPABILITIES: 6, + VD_AGENT_CLIPBOARD_GRAB: 7, + VD_AGENT_CLIPBOARD_REQUEST: 8, + VD_AGENT_CLIPBOARD_RELEASE: 9, + VD_AGENT_FILE_XFER_START: 10, + VD_AGENT_FILE_XFER_STATUS: 11, + VD_AGENT_FILE_XFER_DATA: 12, + VD_AGENT_CLIENT_DISCONNECTED: 13, + VD_AGENT_MAX_CLIPBOARD: 14, + + VD_AGENT_CAP_MOUSE_STATE: 0, + VD_AGENT_CAP_MONITORS_CONFIG: 1, + VD_AGENT_CAP_REPLY: 2, + VD_AGENT_CAP_CLIPBOARD: 3, + VD_AGENT_CAP_DISPLAY_CONFIG: 4, + VD_AGENT_CAP_CLIPBOARD_BY_DEMAND: 5, + VD_AGENT_CAP_CLIPBOARD_SELECTION: 6, + VD_AGENT_CAP_SPARSE_MONITORS_CONFIG: 7, + VD_AGENT_CAP_GUEST_LINEEND_LF: 8, + VD_AGENT_CAP_GUEST_LINEEND_CRLF: 9, + VD_AGENT_CAP_MAX_CLIPBOARD: 10, + VD_AGENT_END_CAP: 11, + + VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA: 0, + VD_AGENT_FILE_XFER_STATUS_CANCELLED: 1, + VD_AGENT_FILE_XFER_STATUS_ERROR: 2, + VD_AGENT_FILE_XFER_STATUS_SUCCESS: 3 +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/filexfer.js b/packages/itmat-ui-react/src/components/lxd/spice/src/filexfer.js new file mode 100644 index 000000000..92b8b3ad3 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/filexfer.js @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-this-alias */ + +/* + Copyright (C) 2014 Red Hat, Inc. + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +function SpiceFileXferTask(id, file) +{ + this.id = id; + this.file = file; +} + +SpiceFileXferTask.prototype.create_progressbar = function() +{ + var _this = this; + var cancel = document.createElement('input'); + this.progressbar_container = document.createElement('div'); + this.progressbar = document.createElement('progress'); + + cancel.type = 'button'; + cancel.value = 'Cancel'; + cancel.style.float = 'right'; + cancel.onclick = function() + { + _this.cancelled = true; + _this.remove_progressbar(); + }; + + this.progressbar.setAttribute('max', this.file.size); + this.progressbar.setAttribute('value', 0); + this.progressbar.style.width = '100%'; + this.progressbar.style.margin = '4px auto'; + this.progressbar.style.display = 'inline-block'; + this.progressbar_container.style.width = '90%'; + this.progressbar_container.style.margin = 'auto'; + this.progressbar_container.style.padding = '4px'; + this.progressbar_container.textContent = this.file.name; + this.progressbar_container.appendChild(cancel); + this.progressbar_container.appendChild(this.progressbar); + document.getElementById('spice-xfer-area').appendChild(this.progressbar_container); +}; + +SpiceFileXferTask.prototype.update_progressbar = function(value) +{ + this.progressbar.setAttribute('value', value); +}; + +SpiceFileXferTask.prototype.remove_progressbar = function() +{ + if (this.progressbar_container && this.progressbar_container.parentNode) + this.progressbar_container.parentNode.removeChild(this.progressbar_container); +}; + +function handle_file_dragover(e) +{ + e.stopPropagation(); + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; +} + +function handle_file_drop(e) +{ + var sc = window.spice_connection; + var files = e.dataTransfer.files; + + e.stopPropagation(); + e.preventDefault(); + for (var i = files.length - 1; i >= 0; i--) + { + if (files[i].type) // do not copy a directory + sc.file_xfer_start(files[i]); + } + +} + +export { + SpiceFileXferTask, + handle_file_dragover, + handle_file_drop +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/inputs.js b/packages/itmat-ui-react/src/components/lxd/spice/src/inputs.js new file mode 100644 index 000000000..2f7c47431 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/inputs.js @@ -0,0 +1,325 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable no-redeclare */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import * as Messages from './spicemsg.js'; +import { Constants } from './enums.js'; +import { KeyNames } from './atKeynames.js'; +import { SpiceConn } from './spiceconn.js'; +import { DEBUG } from './utils.js'; + +/*---------------------------------------------------------------------------- + ** Modifier Keystates + ** These need to be tracked because focus in and out can get the keyboard + ** out of sync. + **------------------------------------------------------------------------*/ +var Shift_state = -1; +var Ctrl_state = -1; +var Alt_state = -1; +var Meta_state = -1; + +/*---------------------------------------------------------------------------- +** SpiceInputsConn +** Drive the Spice Inputs channel (e.g. mouse + keyboard) +**--------------------------------------------------------------------------*/ +function SpiceInputsConn() +{ + SpiceConn.apply(this, arguments); + + this.mousex = undefined; + this.mousey = undefined; + this.button_state = 0; + this.waiting_for_ack = 0; +} + +SpiceInputsConn.prototype = Object.create(SpiceConn.prototype); +SpiceInputsConn.prototype.process_channel_message = function(msg) +{ + if (msg.type == Constants.SPICE_MSG_INPUTS_INIT) + { + var inputs_init = new Messages.SpiceMsgInputsInit(msg.data); + this.keyboard_modifiers = inputs_init.keyboard_modifiers; + DEBUG > 1 && console.log('MsgInputsInit - modifier ' + this.keyboard_modifiers); + // FIXME - We don't do anything with the keyboard modifiers... + return true; + } + if (msg.type == Constants.SPICE_MSG_INPUTS_KEY_MODIFIERS) + { + var key = new Messages.SpiceMsgInputsKeyModifiers(msg.data); + this.keyboard_modifiers = key.keyboard_modifiers; + DEBUG > 1 && console.log('MsgInputsKeyModifiers - modifier ' + this.keyboard_modifiers); + // FIXME - We don't do anything with the keyboard modifiers... + return true; + } + if (msg.type == Constants.SPICE_MSG_INPUTS_MOUSE_MOTION_ACK) + { + DEBUG > 1 && console.log('mouse motion ack'); + this.waiting_for_ack -= Constants.SPICE_INPUT_MOTION_ACK_BUNCH; + return true; + } + return false; +}; + + + +function handle_mousemove(e) +{ + var msg = new Messages.SpiceMiniData(); + var move; + if (this.sc.mouse_mode == Constants.SPICE_MOUSE_MODE_CLIENT) + { + move = new Messages.SpiceMsgcMousePosition(this.sc, e); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_MOUSE_POSITION, move); + } + else + { + move = new Messages.SpiceMsgcMouseMotion(this.sc, e); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_MOUSE_MOTION, move); + } + if (this.sc && this.sc.inputs && this.sc.inputs.state === 'ready') + { + if (this.sc.inputs.waiting_for_ack < (2 * Constants.SPICE_INPUT_MOTION_ACK_BUNCH)) + { + this.sc.inputs.send_msg(msg); + this.sc.inputs.waiting_for_ack++; + } + else + { + DEBUG > 0 && this.sc.log_info('Discarding mouse motion'); + } + } + + if (this.sc && this.sc.cursor && this.sc.cursor.spice_simulated_cursor) + { + this.sc.cursor.spice_simulated_cursor.style.display = 'block'; + this.sc.cursor.spice_simulated_cursor.style.left = e.pageX - this.sc.cursor.spice_simulated_cursor.spice_hot_x + 'px'; + this.sc.cursor.spice_simulated_cursor.style.top = e.pageY - this.sc.cursor.spice_simulated_cursor.spice_hot_y + 'px'; + e.preventDefault(); + } + +} + +function handle_mousedown(e) +{ + var press = new Messages.SpiceMsgcMousePress(this.sc, e); + var msg = new Messages.SpiceMiniData(); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_MOUSE_PRESS, press); + if (this.sc && this.sc.inputs && this.sc.inputs.state === 'ready') + this.sc.inputs.send_msg(msg); + + e.preventDefault(); +} + +function handle_contextmenu(e) +{ + e.preventDefault(); + return false; +} + +function handle_mouseup(e) +{ + var release = new Messages.SpiceMsgcMouseRelease(this.sc, e); + var msg = new Messages.SpiceMiniData(); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_MOUSE_RELEASE, release); + if (this.sc && this.sc.inputs && this.sc.inputs.state === 'ready') + this.sc.inputs.send_msg(msg); + + e.preventDefault(); +} + +function handle_mousewheel(e) +{ + var press = new Messages.SpiceMsgcMousePress(); + var release = new Messages.SpiceMsgcMouseRelease(); + if (e.deltaY < 0) + press.button = release.button = Constants.SPICE_MOUSE_BUTTON_UP; + else + press.button = release.button = Constants.SPICE_MOUSE_BUTTON_DOWN; + press.buttons_state = 0; + release.buttons_state = 0; + + var msg = new Messages.SpiceMiniData(); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_MOUSE_PRESS, press); + if (this.sc && this.sc.inputs && this.sc.inputs.state === 'ready') + this.sc.inputs.send_msg(msg); + + msg.build_msg(Constants.SPICE_MSGC_INPUTS_MOUSE_RELEASE, release); + if (this.sc && this.sc.inputs && this.sc.inputs.state === 'ready') + this.sc.inputs.send_msg(msg); + + e.preventDefault(); +} + +function handle_keydown(e) +{ + var key = new Messages.SpiceMsgcKeyDown(e); + var msg = new Messages.SpiceMiniData(); + check_and_update_modifiers(e, key.code, this.sc); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_KEY_DOWN, key); + if (this.sc && this.sc.inputs && this.sc.inputs.state === 'ready') + this.sc.inputs.send_msg(msg); + + e.preventDefault(); +} + +function handle_keyup(e) +{ + var key = new Messages.SpiceMsgcKeyUp(e); + var msg = new Messages.SpiceMiniData(); + check_and_update_modifiers(e, key.code, this.sc); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_KEY_UP, key); + if (this.sc && this.sc.inputs && this.sc.inputs.state === 'ready') + this.sc.inputs.send_msg(msg); + + e.preventDefault(); +} + +function send_key(sc, keyCode) +{ + var msg = new Messages.SpiceMiniData(); + var key = new Messages.SpiceMsgcKeyDown(); + + key.code = keyCode; + msg.build_msg(Constants.SPICE_MSGC_INPUTS_KEY_DOWN, key); + sc.inputs.send_msg(msg); + + key.code = 0x80|keyCode; + msg.build_msg(Constants.SPICE_MSGC_INPUTS_KEY_UP, key); + sc.inputs.send_msg(msg); +} + +function sendCtrlAltDel(sc) +{ + if (sc && sc.inputs && sc.inputs.state === 'ready'){ + update_modifier(true, KeyNames.KEY_LCtrl, sc); + update_modifier(true, KeyNames.KEY_Alt, sc); + send_key(sc, KeyNames.KEY_KP_Decimal); + if(Ctrl_state == false) update_modifier(false, KeyNames.KEY_LCtrl, sc); + if(Alt_state == false) update_modifier(false, KeyNames.KEY_Alt, sc); + } +} + +function sendAltF4(sc) +{ + if (sc && sc.inputs && sc.inputs.state === 'ready'){ + update_modifier(true, KeyNames.KEY_Alt, sc); + send_key(sc, KeyNames.KEY_F4); + if(Alt_state == false) update_modifier(false, KeyNames.KEY_Alt, sc); + } +} + +function sendAltTab(sc) +{ + if (sc && sc.inputs && sc.inputs.state === 'ready'){ + update_modifier(true, KeyNames.KEY_Alt, sc); + send_key(sc, KeyNames.KEY_Tab); + if(Alt_state == false) update_modifier(false, KeyNames.KEY_Alt, sc); + } +} + +function update_modifier(state, code, sc) +{ + var msg = new Messages.SpiceMiniData(); + if (!state) + { + var key = new Messages.SpiceMsgcKeyUp(); + key.code =(0x80|code); + msg.build_msg(Constants.SPICE_MSGC_INPUTS_KEY_UP, key); + } + else + { + var key = new Messages.SpiceMsgcKeyDown(); + key.code = code; + msg.build_msg(Constants.SPICE_MSGC_INPUTS_KEY_DOWN, key); + } + + sc.inputs.send_msg(msg); +} + +function check_and_update_modifiers(e, code, sc) +{ + if (Shift_state === -1) + { + Shift_state = e.shiftKey; + Ctrl_state = e.ctrlKey; + Alt_state = e.altKey; + Meta_state = e.metaKey; + } + + if (code === KeyNames.KEY_ShiftL) + Shift_state = true; + else if (code === KeyNames.KEY_Alt) + Alt_state = true; + else if (code === KeyNames.KEY_LCtrl) + Ctrl_state = true; + else if (code === 0xE0B5) + Meta_state = true; + else if (code === (0x80|KeyNames.KEY_ShiftL)) + Shift_state = false; + else if (code === (0x80|KeyNames.KEY_Alt)) + Alt_state = false; + else if (code === (0x80|KeyNames.KEY_LCtrl)) + Ctrl_state = false; + else if (code === (0x80|0xE0B5)) + Meta_state = false; + + if (sc && sc.inputs && sc.inputs.state === 'ready') + { + if (Shift_state != e.shiftKey) + { + console.log('Shift state out of sync'); + update_modifier(e.shiftKey, KeyNames.KEY_ShiftL, sc); + Shift_state = e.shiftKey; + } + if (Alt_state != e.altKey) + { + console.log('Alt state out of sync'); + update_modifier(e.altKey, KeyNames.KEY_Alt, sc); + Alt_state = e.altKey; + } + if (Ctrl_state != e.ctrlKey) + { + console.log('Ctrl state out of sync'); + update_modifier(e.ctrlKey, KeyNames.KEY_LCtrl, sc); + Ctrl_state = e.ctrlKey; + } + if (Meta_state != e.metaKey) + { + console.log('Meta state out of sync'); + update_modifier(e.metaKey, 0xE0B5, sc); + Meta_state = e.metaKey; + } + } +} + +export { + SpiceInputsConn, + handle_mousemove, + handle_mousedown, + handle_contextmenu, + handle_mouseup, + handle_mousewheel, + handle_keydown, + handle_keyup, + sendCtrlAltDel, + sendAltTab, + sendAltF4 +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/lz.js b/packages/itmat-ui-react/src/components/lxd/spice/src/lz.js new file mode 100644 index 000000000..0f9d6cd17 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/lz.js @@ -0,0 +1,192 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-redeclare */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import { Constants } from './enums.js'; + +/*---------------------------------------------------------------------------- +** lz.js +** Functions for handling SPICE_IMAGE_TYPE_LZ_RGB +** Adapted from lz.c . +**--------------------------------------------------------------------------*/ +function lz_rgb32_decompress(in_buf, at, out_buf, type, default_alpha) +{ + var encoder = at; + var op = 0; + var ctrl; + var ctr = 0; + var i = 0; + + for (ctrl = in_buf[encoder++]; (op * 4) < out_buf.length; ctrl = in_buf[encoder++]) + { + var ref = op; + var len = ctrl >> 5; + var ofs = (ctrl & 31) << 8; + + //if (type == LZ_IMAGE_TYPE_RGBA) + //console.log(ctr++ + ": from " + (encoder + 28) + ", ctrl " + ctrl + ", len " + len + ", ofs " + ofs + ", op " + op); + if (ctrl >= 32) { + + var code; + len--; + + if (len == 7 - 1) { + do { + code = in_buf[encoder++]; + len += code; + } while (code == 255); + } + code = in_buf[encoder++]; + ofs += code; + + + if (code == 255) { + if ((ofs - code) == (31 << 8)) { + ofs = in_buf[encoder++] << 8; + ofs += in_buf[encoder++]; + ofs += 8191; + } + } + len += 1; + if (type == Constants.LZ_IMAGE_TYPE_RGBA) + len += 2; + + ofs += 1; + + ref -= ofs; + if (ref == (op - 1)) { + var b = ref; + //if (type == LZ_IMAGE_TYPE_RGBA) console.log("alpha " + out_buf[(b*4)+3] + " dupped into pixel " + op + " through pixel " + (op + len)); + for (; len; --len) { + if (type == Constants.LZ_IMAGE_TYPE_RGBA) + { + out_buf[(op*4) + 3] = out_buf[(b*4)+3]; + } + else + { + for (i = 0; i < 4; i++) + out_buf[(op*4) + i] = out_buf[(b*4)+i]; + } + op++; + } + } else { + //if (type == LZ_IMAGE_TYPE_RGBA) console.log("alpha copied to pixel " + op + " through " + (op + len) + " from " + ref); + for (; len; --len) { + if (type == Constants.LZ_IMAGE_TYPE_RGBA) + { + out_buf[(op*4) + 3] = out_buf[(ref*4)+3]; + } + else + { + for (i = 0; i < 4; i++) + out_buf[(op*4) + i] = out_buf[(ref*4)+i]; + } + op++; ref++; + } + } + } else { + ctrl++; + + if (type == Constants.LZ_IMAGE_TYPE_RGBA) + { + //console.log("alpha " + in_buf[encoder] + " set into pixel " + op); + out_buf[(op*4) + 3] = in_buf[encoder++]; + } + else + { + out_buf[(op*4) + 0] = in_buf[encoder + 2]; + out_buf[(op*4) + 1] = in_buf[encoder + 1]; + out_buf[(op*4) + 2] = in_buf[encoder + 0]; + if (default_alpha) + out_buf[(op*4) + 3] = 255; + encoder += 3; + } + op++; + + + for (--ctrl; ctrl; ctrl--) { + if (type == Constants.LZ_IMAGE_TYPE_RGBA) + { + //console.log("alpha " + in_buf[encoder] + " set into pixel " + op); + out_buf[(op*4) + 3] = in_buf[encoder++]; + } + else + { + out_buf[(op*4) + 0] = in_buf[encoder + 2]; + out_buf[(op*4) + 1] = in_buf[encoder + 1]; + out_buf[(op*4) + 2] = in_buf[encoder + 0]; + if (default_alpha) + out_buf[(op*4) + 3] = 255; + encoder += 3; + } + op++; + } + } + + } + return encoder - 1; +} + +function flip_image_data(img) +{ + var wb = img.width * 4; + var h = img.height; + var temp_h = h; + var buff = new Uint8Array(img.width * img.height * 4); + while (temp_h--) + { + buff.set(img.data.subarray(temp_h * wb, (temp_h + 1) * wb), (h - temp_h - 1) * wb); + } + img.data.set(buff); +} + +function convert_spice_lz_to_web(context, lz_image) +{ + var at; + if (lz_image.type === Constants.LZ_IMAGE_TYPE_RGB32 || lz_image.type === Constants.LZ_IMAGE_TYPE_RGBA) + { + var u8 = new Uint8Array(lz_image.data); + var ret = context.createImageData(lz_image.width, lz_image.height); + + at = lz_rgb32_decompress(u8, 0, ret.data, Constants.LZ_IMAGE_TYPE_RGB32, lz_image.type != Constants.LZ_IMAGE_TYPE_RGBA); + if (!lz_image.top_down) + flip_image_data(ret); + + if (lz_image.type == Constants.LZ_IMAGE_TYPE_RGBA) + lz_rgb32_decompress(u8, at, ret.data, Constants.LZ_IMAGE_TYPE_RGBA, false); + } + else if (lz_image.type === Constants.LZ_IMAGE_TYPE_XXXA) + { + var u8 = new Uint8Array(lz_image.data); + var ret = context.createImageData(lz_image.width, lz_image.height); + lz_rgb32_decompress(u8, 0, ret.data, Constants.LZ_IMAGE_TYPE_RGBA, false); + } + else + return undefined; + + return ret; +} + +export { + convert_spice_lz_to_web +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/main.js b/packages/itmat-ui-react/src/components/lxd/spice/src/main.js new file mode 100644 index 000000000..7a11d18c1 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/main.js @@ -0,0 +1,523 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-this-alias */ +/* eslint-disable no-unused-vars */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import * as Messages from './spicemsg.js'; +import { Constants } from './enums.js'; +import { SpiceCursorConn } from './cursor.js'; +import { SpiceConn } from './spiceconn.js'; +import { DEBUG } from './utils.js'; +import { SpiceFileXferTask } from './filexfer.js'; +import { SpiceInputsConn, sendCtrlAltDel } from './inputs.js'; +import { SpiceDisplayConn } from './display.js'; +import { SpicePlaybackConn } from './playback.js'; +import { SpicePortConn } from './port.js'; +import { handle_file_dragover, handle_file_drop } from './filexfer.js'; +import { resize_helper, handle_resize } from './resize.js'; + +/*---------------------------------------------------------------------------- +** SpiceMainConn +** This is the main Javascript class for establishing and +** managing a connection to a Spice Server. +** +** Invocation: You must pass an object with properties as follows: +** uri (required) Uri of a WebSocket listener that is +** connected to a spice server. +** password (required) Password to send to the spice server +** message_id (optional) Identifier of an element in the DOM +** where SpiceConn will write messages. +** It will use classes spice-messages-x, +** where x is one of info, warning, or error. +** screen_id (optional) Identifier of an element in the DOM +** where SpiceConn will create any new +** client screens. This is the main UI. +** dump_id (optional) If given, an element to use for +** dumping every single image + canvas drawn. +** Sometimes useful for debugging. +** onerror (optional) If given, a function to receive async +** errors. Note that you should also catch +** errors for ones that occur inline +** onagent (optional) If given, a function to be called when +** a VD agent is connected; a good opportunity +** to request a resize +** onsuccess (optional) If given, a function to be called when the +** session is successfully connected +** +** Throws error if there are troubles. Requires a modern (by 2012 standards) +** browser, including WebSocket and WebSocket.binaryType == arraybuffer +** +**--------------------------------------------------------------------------*/ +function SpiceMainConn() +{ + if (typeof WebSocket === 'undefined') + throw new Error('WebSocket unavailable. You need to use a different browser.'); + + SpiceConn.apply(this, arguments); + + this.agent_msg_queue = []; + this.file_xfer_tasks = {}; + this.file_xfer_task_id = 0; + this.file_xfer_read_queue = []; + this.ports = []; +} + +SpiceMainConn.prototype = Object.create(SpiceConn.prototype); +SpiceMainConn.prototype.process_channel_message = function(msg) +{ + if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_BEGIN) + { + this.known_unimplemented(msg.type, 'Main Migrate Begin'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_CANCEL) + { + this.known_unimplemented(msg.type, 'Main Migrate Cancel'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_INIT) + { + this.log_info('Connected to ' + this.ws.url); + this.report_success('Connected'); + this.main_init = new Messages.SpiceMsgMainInit(msg.data); + this.connection_id = this.main_init.session_id; + this.agent_tokens = this.main_init.agent_tokens; + + if (DEBUG > 0) + { + // FIXME - there is a lot here we don't handle; mouse modes, agent, + // ram_hint, multi_media_time + this.log_info('session id ' + this.main_init.session_id + + ' ; display_channels_hint ' + this.main_init.display_channels_hint + + ' ; supported_mouse_modes ' + this.main_init.supported_mouse_modes + + ' ; current_mouse_mode ' + this.main_init.current_mouse_mode + + ' ; agent_connected ' + this.main_init.agent_connected + + ' ; agent_tokens ' + this.main_init.agent_tokens + + ' ; multi_media_time ' + this.main_init.multi_media_time + + ' ; ram_hint ' + this.main_init.ram_hint); + } + + this.our_mm_time = Date.now(); + this.mm_time = this.main_init.multi_media_time; + + this.handle_mouse_mode(this.main_init.current_mouse_mode, + this.main_init.supported_mouse_modes); + + if (this.main_init.agent_connected) + this.connect_agent(); + + var attach = new Messages.SpiceMiniData(); + attach.type = Constants.SPICE_MSGC_MAIN_ATTACH_CHANNELS; + attach.size = attach.buffer_size(); + this.send_msg(attach); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MOUSE_MODE) + { + var mode = new Messages.SpiceMsgMainMouseMode(msg.data); + DEBUG > 0 && this.log_info('Mouse supported modes ' + mode.supported_modes + '; current ' + mode.current_mode); + this.handle_mouse_mode(mode.current_mode, mode.supported_modes); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MULTI_MEDIA_TIME) + { + this.known_unimplemented(msg.type, 'Main Multi Media Time'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_CHANNELS_LIST) + { + var i; + var chans; + DEBUG > 0 && console.log('channels'); + chans = new Messages.SpiceMsgChannels(msg.data); + for (i = 0; i < chans.channels.length; i++) + { + var conn = { + uri: this.ws.url, + parent: this, + connection_id : this.connection_id, + type : chans.channels[i].type, + chan_id : chans.channels[i].id + }; + if (chans.channels[i].type == Constants.SPICE_CHANNEL_DISPLAY) + { + if (chans.channels[i].id == 0) { + this.display = new SpiceDisplayConn(conn); + } else { + this.log_warn('The spice-html5 client does not handle multiple heads.'); + } + } + else if (chans.channels[i].type == Constants.SPICE_CHANNEL_INPUTS) + { + this.inputs = new SpiceInputsConn(conn); + this.inputs.mouse_mode = this.mouse_mode; + } + else if (chans.channels[i].type == Constants.SPICE_CHANNEL_CURSOR) + this.cursor = new SpiceCursorConn(conn); + else if (chans.channels[i].type == Constants.SPICE_CHANNEL_PLAYBACK) + this.cursor = new SpicePlaybackConn(conn); + else if (chans.channels[i].type == Constants.SPICE_CHANNEL_PORT) + this.ports.push(new SpicePortConn(conn)); + else + { + if (! ('extra_channels' in this)) + this.extra_channels = []; + this.extra_channels[i] = new SpiceConn(conn); + this.log_err('Channel type ' + this.extra_channels[i].channel_type() + ' not implemented'); + } + + } + + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_CONNECTED) + { + this.connect_agent(); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS) + { + var connected_tokens = new Messages.SpiceMsgMainAgentTokens(msg.data); + this.agent_tokens = connected_tokens.num_tokens; + this.connect_agent(); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_TOKEN) + { + var remaining_tokens, tokens = new Messages.SpiceMsgMainAgentTokens(msg.data); + this.agent_tokens += tokens.num_tokens; + this.send_agent_message_queue(); + + remaining_tokens = this.agent_tokens; + while (remaining_tokens > 0 && this.file_xfer_read_queue.length > 0) + { + var xfer_task = this.file_xfer_read_queue.shift(); + this.file_xfer_read(xfer_task, xfer_task.read_bytes); + remaining_tokens--; + } + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_DISCONNECTED) + { + this.agent_connected = false; + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_DATA) + { + var agent_data = new Messages.SpiceMsgMainAgentData(msg.data); + if (agent_data.type == Constants.VD_AGENT_ANNOUNCE_CAPABILITIES) + { + var agent_caps = new Messages.VDAgentAnnounceCapabilities(agent_data.data); + if (agent_caps.request) + this.announce_agent_capabilities(0); + return true; + } + else if (agent_data.type == Constants.VD_AGENT_FILE_XFER_STATUS) + { + this.handle_file_xfer_status(new Messages.VDAgentFileXferStatusMessage(agent_data.data)); + return true; + } + + return false; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_SWITCH_HOST) + { + this.known_unimplemented(msg.type, 'Main Migrate Switch Host'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_END) + { + this.known_unimplemented(msg.type, 'Main Migrate End'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_NAME) + { + this.known_unimplemented(msg.type, 'Main Name'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_UUID) + { + this.known_unimplemented(msg.type, 'Main UUID'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_BEGIN_SEAMLESS) + { + this.known_unimplemented(msg.type, 'Main Migrate Begin Seamless'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_ACK) + { + this.known_unimplemented(msg.type, 'Main Migrate Dst Seamless ACK'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_NACK) + { + this.known_unimplemented(msg.type, 'Main Migrate Dst Seamless NACK'); + return true; + } + + return false; +}; + +SpiceMainConn.prototype.stop = function(msg) +{ + this.state = 'closing'; + + if (this.inputs) + { + this.inputs.cleanup(); + this.inputs = undefined; + } + + if (this.cursor) + { + this.cursor.cleanup(); + this.cursor = undefined; + } + + if (this.display) + { + this.display.cleanup(); + this.display.destroy_surfaces(); + this.display = undefined; + } + + this.cleanup(); + + if ('extra_channels' in this) + for (var e in this.extra_channels) + this.extra_channels[e].cleanup(); + this.extra_channels = undefined; +}; + +SpiceMainConn.prototype.send_agent_message_queue = function(message) +{ + if (!this.agent_connected) + return; + + if (message) + this.agent_msg_queue.push(message); + + while (this.agent_tokens > 0 && this.agent_msg_queue.length > 0) + { + var mr = this.agent_msg_queue.shift(); + this.send_msg(mr); + this.agent_tokens--; + } +}; + +SpiceMainConn.prototype.send_agent_message = function(type, message) +{ + var agent_data = new Messages.SpiceMsgcMainAgentData(type, message); + var sb = 0, maxsize = Constants.VD_AGENT_MAX_DATA_SIZE - Messages.SpiceMiniData.prototype.buffer_size(); + var data = new ArrayBuffer(agent_data.buffer_size()); + agent_data.to_buffer(data); + while (sb < agent_data.buffer_size()) + { + var eb = Math.min(sb + maxsize, agent_data.buffer_size()); + var mr = new Messages.SpiceMiniData(); + mr.type = Constants.SPICE_MSGC_MAIN_AGENT_DATA; + mr.size = eb - sb; + mr.data = data.slice(sb, eb); + this.send_agent_message_queue(mr); + sb = eb; + } +}; + +SpiceMainConn.prototype.announce_agent_capabilities = function(request) +{ + var caps = new Messages.VDAgentAnnounceCapabilities(request, (1 << Constants.VD_AGENT_CAP_MOUSE_STATE) | + (1 << Constants.VD_AGENT_CAP_MONITORS_CONFIG) | + (1 << Constants.VD_AGENT_CAP_REPLY)); + this.send_agent_message(Constants.VD_AGENT_ANNOUNCE_CAPABILITIES, caps); +}; + +SpiceMainConn.prototype.resize_window = function(flags, width, height, depth, x, y) +{ + var monitors_config = new Messages.VDAgentMonitorsConfig(flags, width, height, depth, x, y); + this.send_agent_message(Constants.VD_AGENT_MONITORS_CONFIG, monitors_config); +}; + +SpiceMainConn.prototype.file_xfer_start = function(file) +{ + var task_id, xfer_start, task; + + task_id = this.file_xfer_task_id++; + task = new SpiceFileXferTask(task_id, file); + task.create_progressbar(); + this.file_xfer_tasks[task_id] = task; + xfer_start = new Messages.VDAgentFileXferStartMessage(task_id, file.name, file.size); + this.send_agent_message(Constants.VD_AGENT_FILE_XFER_START, xfer_start); +}; + +SpiceMainConn.prototype.handle_file_xfer_status = function(file_xfer_status) +{ + var xfer_error, xfer_task; + if (!this.file_xfer_tasks[file_xfer_status.id]) + { + return; + } + xfer_task = this.file_xfer_tasks[file_xfer_status.id]; + switch (file_xfer_status.result) + { + case Constants.VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA: + this.file_xfer_read(xfer_task); + return; + case Constants.VD_AGENT_FILE_XFER_STATUS_CANCELLED: + xfer_error = 'transfer is cancelled by spice agent'; + break; + case Constants.VD_AGENT_FILE_XFER_STATUS_ERROR: + xfer_error = 'some errors occurred in the spice agent'; + break; + case Constants.VD_AGENT_FILE_XFER_STATUS_SUCCESS: + break; + default: + xfer_error = 'unhandled status type: ' + file_xfer_status.result; + break; + } + + this.file_xfer_completed(xfer_task, xfer_error); +}; + +SpiceMainConn.prototype.file_xfer_read = function(file_xfer_task, start_byte) +{ + var FILE_XFER_CHUNK_SIZE = 32 * Constants.VD_AGENT_MAX_DATA_SIZE; + var _this = this; + var sb, eb; + var slice, reader; + + if (!file_xfer_task || + !this.file_xfer_tasks[file_xfer_task.id] || + (start_byte > 0 && start_byte == file_xfer_task.file.size)) + { + return; + } + + if (file_xfer_task.cancelled) + { + var xfer_status = new Messages.VDAgentFileXferStatusMessage(file_xfer_task.id, + Constants.VD_AGENT_FILE_XFER_STATUS_CANCELLED); + this.send_agent_message(Constants.VD_AGENT_FILE_XFER_STATUS, xfer_status); + delete this.file_xfer_tasks[file_xfer_task.id]; + return; + } + + sb = start_byte || 0; + eb = Math.min(sb + FILE_XFER_CHUNK_SIZE, file_xfer_task.file.size); + + if (!this.agent_tokens) + { + file_xfer_task.read_bytes = sb; + this.file_xfer_read_queue.push(file_xfer_task); + return; + } + + reader = new FileReader(); + reader.onload = function(e) + { + var xfer_data = new Messages.VDAgentFileXferDataMessage(file_xfer_task.id, + e.target.result.byteLength, + e.target.result); + _this.send_agent_message(Constants.VD_AGENT_FILE_XFER_DATA, xfer_data); + _this.file_xfer_read(file_xfer_task, eb); + file_xfer_task.update_progressbar(eb); + }; + + slice = file_xfer_task.file.slice(sb, eb); + reader.readAsArrayBuffer(slice); +}; + +SpiceMainConn.prototype.file_xfer_completed = function(file_xfer_task, error) +{ + if (error) + this.log_err(error); + else + this.log_info('transfer of \'' + file_xfer_task.file.name +'\' was successful'); + + file_xfer_task.remove_progressbar(); + + delete this.file_xfer_tasks[file_xfer_task.id]; +}; + +SpiceMainConn.prototype.connect_agent = function() +{ + this.agent_connected = true; + + var agent_start = new Messages.SpiceMsgcMainAgentStart(~0); + var mr = new Messages.SpiceMiniData(); + mr.build_msg(Constants.SPICE_MSGC_MAIN_AGENT_START, agent_start); + this.send_msg(mr); + + this.announce_agent_capabilities(1); + + if (this.onagent !== undefined) + this.onagent(this); + +}; + +SpiceMainConn.prototype.handle_mouse_mode = function(current, supported) +{ + this.mouse_mode = current; + if (current != Constants.SPICE_MOUSE_MODE_CLIENT && (supported & Constants.SPICE_MOUSE_MODE_CLIENT)) + { + var mode_request = new Messages.SpiceMsgcMainMouseModeRequest(Constants.SPICE_MOUSE_MODE_CLIENT); + var mr = new Messages.SpiceMiniData(); + mr.build_msg(Constants.SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST, mode_request); + this.send_msg(mr); + } + + if (this.inputs) + this.inputs.mouse_mode = current; +}; + +/* Shift current time to attempt to get a time matching that of the server */ +SpiceMainConn.prototype.relative_now = function() +{ + var ret = (Date.now() - this.our_mm_time) + this.mm_time; + return ret; +}; + +export { + SpiceMainConn, + handle_file_dragover, + handle_file_drop, + resize_helper, + handle_resize, + sendCtrlAltDel +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/playback.js b/packages/itmat-ui-react/src/components/lxd/spice/src/playback.js new file mode 100644 index 000000000..eb51a9df0 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/playback.js @@ -0,0 +1,411 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2014 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** SpicePlaybackConn +** Drive the Spice Playback channel (sound out) +**--------------------------------------------------------------------------*/ + +import * as Utils from './utils.js'; +import * as Webm from './webm.js'; +import * as Messages from './spicemsg.js'; +import { Constants } from './enums.js'; +import { SpiceConn } from './spiceconn.js'; + +function SpicePlaybackConn() +{ + SpiceConn.apply(this, arguments); + + this.queue = []; + this.append_okay = false; + this.start_time = 0; +} + +SpicePlaybackConn.prototype = Object.create(SpiceConn.prototype); +SpicePlaybackConn.prototype.process_channel_message = function(msg) +{ + if (!window.MediaSource) + { + this.log_err('MediaSource API is not available'); + return false; + } + + if (msg.type == Constants.SPICE_MSG_PLAYBACK_START) + { + var start = new Messages.SpiceMsgPlaybackStart(msg.data); + + Utils.PLAYBACK_DEBUG > 0 && console.log('PlaybackStart; frequency ' + start.frequency); + + if (start.frequency != Webm.Constants.OPUS_FREQUENCY) + { + this.log_err('This player cannot handle frequency ' + start.frequency); + return false; + } + + if (start.channels != Webm.Constants.OPUS_CHANNELS) + { + this.log_err('This player cannot handle ' + start.channels + ' channels'); + return false; + } + + if (start.format != Constants.SPICE_AUDIO_FMT_S16) + { + this.log_err('This player cannot format ' + start.format); + return false; + } + + if (! this.source_buffer) + { + this.media_source = new MediaSource(); + this.media_source.spiceconn = this; + + this.audio = document.createElement('audio'); + this.audio.spiceconn = this; + this.audio.setAttribute('autoplay', true); + this.audio.src = window.URL.createObjectURL(this.media_source); + document.getElementById(this.parent.screen_id).appendChild(this.audio); + + this.media_source.addEventListener('sourceopen', handle_source_open, false); + this.media_source.addEventListener('sourceended', handle_source_ended, false); + this.media_source.addEventListener('sourceclosed', handle_source_closed, false); + + this.bytes_written = 0; + + return true; + } + } + + if (msg.type == Constants.SPICE_MSG_PLAYBACK_DATA) + { + var data = new Messages.SpiceMsgPlaybackData(msg.data); + + if (! this.source_buffer) + return true; + + if (this.audio.readyState >= 3 && this.audio.buffered.length > 1 && + this.audio.currentTime == this.audio.buffered.end(0) && + this.audio.currentTime < this.audio.buffered.start(this.audio.buffered.length - 1)) + { + console.log('Audio underrun: we appear to have fallen behind; advancing to ' + + this.audio.buffered.start(this.audio.buffered.length - 1)); + this.audio.currentTime = this.audio.buffered.start(this.audio.buffered.length - 1); + } + + /* Around version 45, Firefox started being very particular about the + time stamps put into the Opus stream. The time stamps from the Spice server are + somewhat irregular. They mostly arrive every 10 ms, but sometimes it is 11, or sometimes + with two time stamps the same in a row. The previous logic resulted in fuzzy and/or + distorted audio streams in Firefox in a row. + + In theory, the sequence mode should be appropriate for us, but as of 09/27/2016, + I was unable to make sequence mode work with Firefox. + + Thus, we end up with an inelegant hack. Essentially, we force every packet to have + a 10ms time delta, unless there is an obvious gap in time stream, in which case we + will resync. + */ + + if (this.start_time != 0 && data.time != (this.last_data_time + Webm.Constants.EXPECTED_PACKET_DURATION)) + { + if (Math.abs(data.time - (Webm.Constants.EXPECTED_PACKET_DURATION + this.last_data_time)) < Webm.Constants.MAX_CLUSTER_TIME) + { + Utils.PLAYBACK_DEBUG > 1 && console.log('Hacking time of ' + data.time + ' to ' + + (this.last_data_time + Webm.Constants.EXPECTED_PACKET_DURATION)); + data.time = this.last_data_time + Webm.Constants.EXPECTED_PACKET_DURATION; + } + else + { + Utils.PLAYBACK_DEBUG > 1 && console.log('Apparent gap in audio time; now is ' + data.time + ' last was ' + this.last_data_time); + } + } + + this.last_data_time = data.time; + + Utils.PLAYBACK_DEBUG > 1 && console.log('PlaybackData; time ' + data.time + '; length ' + data.data.byteLength); + + if (this.start_time == 0) + this.start_playback(data); + + else if (data.time - this.cluster_time >= Webm.Constants.MAX_CLUSTER_TIME) + this.new_cluster(data); + + else + this.simple_block(data, false); + + return true; + } + + if (msg.type == Constants.SPICE_MSG_PLAYBACK_MODE) + { + var mode = new Messages.SpiceMsgPlaybackMode(msg.data); + if (mode.mode != Constants.SPICE_AUDIO_DATA_MODE_OPUS) + { + this.log_err('This player cannot handle mode ' + mode.mode); + delete this.source_buffer; + } + return true; + } + + if (msg.type == Constants.SPICE_MSG_PLAYBACK_STOP) + { + Utils.PLAYBACK_DEBUG > 0 && console.log('PlaybackStop'); + if (this.source_buffer) + { + document.getElementById(this.parent.screen_id).removeChild(this.audio); + window.URL.revokeObjectURL(this.audio.src); + + delete this.source_buffer; + delete this.media_source; + delete this.audio; + + this.append_okay = false; + this.queue = []; + this.start_time = 0; + + return true; + } + } + + if (msg.type == Constants.SPICE_MSG_PLAYBACK_VOLUME) + { + this.known_unimplemented(msg.type, 'Playback Volume'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_PLAYBACK_MUTE) + { + this.known_unimplemented(msg.type, 'Playback Mute'); + return true; + } + + if (msg.type == Constants.SPICE_MSG_PLAYBACK_LATENCY) + { + this.known_unimplemented(msg.type, 'Playback Latency'); + return true; + } + + return false; +}; + +SpicePlaybackConn.prototype.start_playback = function(data) +{ + this.start_time = data.time; + + var h = new Webm.Header(); + var te = new Webm.AudioTrackEntry(); + var t = new Webm.Tracks(te); + + var mb = new ArrayBuffer(h.buffer_size() + t.buffer_size()); + + this.bytes_written = h.to_buffer(mb); + this.bytes_written = t.to_buffer(mb, this.bytes_written); + + this.source_buffer.addEventListener('error', handle_sourcebuffer_error, false); + this.source_buffer.addEventListener('updateend', handle_append_buffer_done, false); + playback_append_buffer(this, mb); + + this.new_cluster(data); +}; + +SpicePlaybackConn.prototype.new_cluster = function(data) +{ + this.cluster_time = data.time; + + var c = new Webm.Cluster(data.time - this.start_time); + + var mb = new ArrayBuffer(c.buffer_size()); + this.bytes_written += c.to_buffer(mb); + + if (this.append_okay) + playback_append_buffer(this, mb); + else + this.queue.push(mb); + + this.simple_block(data, true); +}; + +SpicePlaybackConn.prototype.simple_block = function(data, keyframe) +{ + var sb = new Webm.SimpleBlock(data.time - this.cluster_time, data.data, keyframe); + var mb = new ArrayBuffer(sb.buffer_size()); + + this.bytes_written += sb.to_buffer(mb); + + if (this.append_okay) + playback_append_buffer(this, mb); + else + this.queue.push(mb); +}; + +function handle_source_open(e) +{ + var p = this.spiceconn; + + if (p.source_buffer) + return; + + p.source_buffer = this.addSourceBuffer(Webm.Constants.SPICE_PLAYBACK_CODEC); + if (! p.source_buffer) + { + p.log_err('Codec ' + Webm.Constants.SPICE_PLAYBACK_CODEC + ' not available.'); + return; + } + + if (Utils.PLAYBACK_DEBUG > 0) + playback_handle_event_debug.call(this, e); + + listen_for_audio_events(p); + + p.source_buffer.spiceconn = p; + p.source_buffer.mode = 'segments'; + + // FIXME - Experimentation with segments and sequences was unsatisfying. + // Switching to sequence did not solve our gap problem, + // but the browsers didn't fully support the time seek capability + // we would expect to gain from 'segments'. + // Segments worked at the time of this patch, so segments it is for now. + +} + +function handle_source_ended(e) +{ + var p = this.spiceconn; + p.log_err('Audio source unexpectedly ended.'); +} + +function handle_source_closed(e) +{ + var p = this.spiceconn; + p.log_err('Audio source unexpectedly closed.'); +} + +function condense_playback_queue(queue) +{ + if (queue.length == 1) + return queue.shift(); + + var len = 0; + var i = 0; + for (i = 0; i < queue.length; i++) + len += queue[i].byteLength; + + var mb = new ArrayBuffer(len); + var tmp = new Uint8Array(mb); + len = 0; + for (i = 0; i < queue.length; i++) + { + tmp.set(new Uint8Array(queue[i]), len); + len += queue[i].byteLength; + } + queue.length = 0; + return mb; +} + +function handle_append_buffer_done(e) +{ + var p = this.spiceconn; + + if (Utils.PLAYBACK_DEBUG > 1) + playback_handle_event_debug.call(this, e); + + if (p.queue.length > 0) + { + var mb = condense_playback_queue(p.queue); + playback_append_buffer(p, mb); + } + else + p.append_okay = true; + +} + +function handle_sourcebuffer_error(e) +{ + var p = this.spiceconn; + p.log_err('source_buffer error ' + e.message); +} + +function playback_append_buffer(p, b) +{ + try + { + p.source_buffer.appendBuffer(b); + p.append_okay = false; + } + catch (e) + { + p.log_err('Error invoking appendBuffer: ' + e.message); + } +} + +function playback_handle_event_debug(e) +{ + var p = this.spiceconn; + if (p.audio) + { + if (Utils.PLAYBACK_DEBUG > 0 || p.audio.buffered.len > 1) + console.log(p.audio.currentTime + ': event ' + e.type + + Utils.dump_media_element(p.audio)); + } + + if (Utils.PLAYBACK_DEBUG > 1 && p.media_source) + console.log(' media_source ' + Utils.dump_media_source(p.media_source)); + + if (Utils.PLAYBACK_DEBUG > 1 && p.source_buffer) + console.log(' source_buffer ' + Utils.dump_source_buffer(p.source_buffer)); + + if (Utils.PLAYBACK_DEBUG > 0 || p.queue.length > 1) + console.log(' queue len ' + p.queue.length + '; append_okay: ' + p.append_okay); +} + +function playback_debug_listen_for_one_event(name) +{ + this.addEventListener(name, playback_handle_event_debug); +} + +function listen_for_audio_events(spiceconn) +{ + var audio_0_events = [ + 'abort', 'error' + ]; + + var audio_1_events = [ + 'loadstart', 'suspend', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', + 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', + 'timeupdate', 'play', 'pause', 'ratechange' + ]; + + var audio_2_events = [ + 'progress', + 'resize', + 'volumechange' + ]; + + audio_0_events.forEach(playback_debug_listen_for_one_event, spiceconn.audio); + if (Utils.PLAYBACK_DEBUG > 0) + audio_1_events.forEach(playback_debug_listen_for_one_event, spiceconn.audio); + if (Utils.PLAYBACK_DEBUG > 1) + audio_2_events.forEach(playback_debug_listen_for_one_event, spiceconn.audio); +} + +export { + SpicePlaybackConn +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/png.js b/packages/itmat-ui-react/src/components/lxd/spice/src/png.js new file mode 100644 index 000000000..c21264263 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/png.js @@ -0,0 +1,265 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** crc logic from rfc2083 ported to Javascript +**--------------------------------------------------------------------------*/ + +import { SpiceDataView } from './spicedataview.js'; + +var rfc2083_crc_table = Array(256); +var rfc2083_crc_table_computed = 0; +/* Make the table for a fast CRC. */ +function rfc2083_make_crc_table() +{ + var c; + var n, k; + for (n = 0; n < 256; n++) + { + c = n; + for (k = 0; k < 8; k++) + { + if (c & 1) + c = ((0xedb88320 ^ (c >>> 1)) >>> 0) & 0xffffffff; + else + c = c >>> 1; + } + rfc2083_crc_table[n] = c; + } + + rfc2083_crc_table_computed = 1; +} + +/* Update a running CRC with the bytes buf[0..len-1]--the CRC + should be initialized to all 1's, and the transmitted value + is the 1's complement of the final running CRC (see the + crc() routine below)). */ + +function rfc2083_update_crc(crc, u8buf, at, len) +{ + var c = crc; + var n; + + if (!rfc2083_crc_table_computed) + rfc2083_make_crc_table(); + + for (n = 0; n < len; n++) + { + c = rfc2083_crc_table[(c ^ u8buf[at + n]) & 0xff] ^ (c >>> 8); + } + + return c; +} + +function rfc2083_crc(u8buf, at, len) +{ + return rfc2083_update_crc(0xffffffff, u8buf, at, len) ^ 0xffffffff; +} + +function crc32(mb, at, len) +{ + var u8 = new Uint8Array(mb); + return rfc2083_crc(u8, at, len); +} + +function PngIHDR(width, height) +{ + this.width = width; + this.height = height; + this.depth = 8; + this.type = 6; + this.compression = 0; + this.filter = 0; + this.interlace = 0; +} + +PngIHDR.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var orig = at; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.buffer_size() - 12); at += 4; + dv.setUint8(at, 'I'.charCodeAt(0)); at++; + dv.setUint8(at, 'H'.charCodeAt(0)); at++; + dv.setUint8(at, 'D'.charCodeAt(0)); at++; + dv.setUint8(at, 'R'.charCodeAt(0)); at++; + dv.setUint32(at, this.width); at += 4; + dv.setUint32(at, this.height); at += 4; + dv.setUint8(at, this.depth); at++; + dv.setUint8(at, this.type); at++; + dv.setUint8(at, this.compression); at++; + dv.setUint8(at, this.filter); at++; + dv.setUint8(at, this.interlace); at++; + dv.setUint32(at, crc32(a, orig + 4, this.buffer_size() - 8)); at += 4; + return at; + }, + buffer_size: function() + { + return 12 + 13; + } +}; + + +function adler() +{ + this.s1 = 1; + this.s2 = 0; +} + +adler.prototype.update = function(b) +{ + this.s1 += b; + this.s1 %= 65521; + this.s2 += this.s1; + this.s2 %= 65521; +}; + +function PngIDAT(width, height, bytes) +{ + if (bytes.byteLength > 65535) + { + throw new Error('Cannot handle more than 64K'); + } + this.data = bytes; + this.width = width; + this.height = height; +} + +PngIDAT.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var orig = at; + var x, y, i, j; + var dv = new SpiceDataView(a); + var zsum = new adler(); + dv.setUint32(at, this.buffer_size() - 12); at += 4; + dv.setUint8(at, 'I'.charCodeAt(0)); at++; + dv.setUint8(at, 'D'.charCodeAt(0)); at++; + dv.setUint8(at, 'A'.charCodeAt(0)); at++; + dv.setUint8(at, 'T'.charCodeAt(0)); at++; + + /* zlib header. */ + dv.setUint8(at, 0x78); at++; + dv.setUint8(at, 0x01); at++; + + /* Deflate header. Specifies uncompressed, final bit */ + dv.setUint8(at, 0x80); at++; + dv.setUint16(at, this.data.byteLength + this.height); at += 2; + dv.setUint16(at, ~(this.data.byteLength + this.height)); at += 2; + var u8 = new Uint8Array(this.data); + for (i = 0, y = 0; y < this.height; y++) + { + /* Filter type 0 - uncompressed */ + dv.setUint8(at, 0); at++; + zsum.update(0); + for (x = 0; x < this.width && i < this.data.byteLength; x++) + { + zsum.update(u8[i]); + dv.setUint8(at, u8[i++]); at++; + zsum.update(u8[i]); + dv.setUint8(at, u8[i++]); at++; + zsum.update(u8[i]); + dv.setUint8(at, u8[i++]); at++; + zsum.update(u8[i]); + dv.setUint8(at, u8[i++]); at++; + } + } + + /* zlib checksum. */ + dv.setUint16(at, zsum.s2); at+=2; + dv.setUint16(at, zsum.s1); at+=2; + + /* FIXME - something is not quite right with the zlib code; + you get an error from libpng if you open the image in + gimp. But it works, so it's good enough for now... */ + + dv.setUint32(at, crc32(a, orig + 4, this.buffer_size() - 8)); at += 4; + return at; + }, + buffer_size: function() + { + return 12 + this.data.byteLength + this.height + 4 + 2 + 1 + 2 + 2; + } +}; + + +function PngIEND() +{ +} + +PngIEND.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var orig = at; + var i; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.buffer_size() - 12); at += 4; + dv.setUint8(at, 'I'.charCodeAt(0)); at++; + dv.setUint8(at, 'E'.charCodeAt(0)); at++; + dv.setUint8(at, 'N'.charCodeAt(0)); at++; + dv.setUint8(at, 'D'.charCodeAt(0)); at++; + dv.setUint32(at, crc32(a, orig + 4, this.buffer_size() - 8)); at += 4; + return at; + }, + buffer_size: function() + { + return 12; + } +}; + + +function create_rgba_png(width, height, bytes) +{ + var i; + var ihdr = new PngIHDR(width, height); + var idat = new PngIDAT(width, height, bytes); + var iend = new PngIEND(); + + var mb = new ArrayBuffer(ihdr.buffer_size() + idat.buffer_size() + iend.buffer_size()); + var at = ihdr.to_buffer(mb); + at = idat.to_buffer(mb, at); + at = iend.to_buffer(mb, at); + + var u8 = new Uint8Array(mb); + var str = ''; + for (i = 0; i < at; i++) + { + str += '%'; + if (u8[i] < 16) + str += '0'; + str += u8[i].toString(16); + } + + + return '%89PNG%0D%0A%1A%0A' + str; +} + +export { + create_rgba_png +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/port.js b/packages/itmat-ui-react/src/components/lxd/spice/src/port.js new file mode 100644 index 000000000..ac6d23740 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/port.js @@ -0,0 +1,96 @@ +/* eslint-disable no-redeclare */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2016 by Oliver Gutierrez + Miroslav Chodil + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import { Constants } from './enums.js'; +import { DEBUG, arraybuffer_to_str } from './utils.js'; +import { SpiceConn } from './spiceconn.js'; +import { SpiceMsgPortInit } from './spicemsg.js'; + +/*---------------------------------------------------------------------------- +** SpicePortConn +** Drive the Spice Port Channel +**--------------------------------------------------------------------------*/ +function SpicePortConn() +{ + DEBUG > 0 && console.log('SPICE port: created SPICE port channel. Args:', arguments); + SpiceConn.apply(this, arguments); + this.port_name = null; +} + +SpicePortConn.prototype = Object.create(SpiceConn.prototype); + +SpicePortConn.prototype.process_channel_message = function(msg) +{ + if (msg.type == Constants.SPICE_MSG_PORT_INIT) + { + if (this.port_name === null) + { + var m = new SpiceMsgPortInit(msg.data); + this.portName = arraybuffer_to_str(new Uint8Array(m.name)); + this.portOpened = m.opened; + DEBUG > 0 && console.log('SPICE port: Port', this.portName, 'initialized'); + return true; + } + + DEBUG > 0 && console.log('SPICE port: Port', this.port_name, 'is already initialized.'); + } + else if (msg.type == Constants.SPICE_MSG_PORT_EVENT) + { + DEBUG > 0 && console.log('SPICE port: Port event received for', this.portName, msg); + var event = new CustomEvent('spice-port-event', { + detail: { + channel: this, + spiceEvent: new Uint8Array(msg.data) + }, + bubbles: true, + cancelable: true + }); + + window.dispatchEvent(event); + return true; + } + else if (msg.type == Constants.SPICE_MSG_SPICEVMC_DATA) + { + DEBUG > 0 && console.log('SPICE port: Data received in port', this.portName, msg); + var event = new CustomEvent('spice-port-data', { + detail: { + channel: this, + data: msg.data + }, + bubbles: true, + cancelable: true + }); + window.dispatchEvent(event); + return true; + } + else + { + DEBUG > 0 && console.log('SPICE port: SPICE message type not recognized:', msg); + } + + return false; +}; + +export { + SpicePortConn +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/quic.js b/packages/itmat-ui-react/src/components/lxd/spice/src/quic.js new file mode 100644 index 000000000..c46c280d4 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/quic.js @@ -0,0 +1,1353 @@ +/* eslint-disable no-unreachable */ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable no-undef */ +/* eslint-disable no-cond-assign */ +/* eslint-disable no-constant-condition */ +/* eslint-disable no-redeclare */ +/* eslint-disable no-throw-literal */ +/* eslint-disable eqeqeq */ +/*"use strict";*/ +/* use strict is commented out because it results in a 5x slowdone in chrome */ +/* + * Copyright (C) 2012 by Jeremy P. White + * Copyright (C) 2012 by Aric Stewart + * + * This file is part of spice-html5. + * + * spice-html5 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * spice-html5 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with spice-html5. If not, see . + */ + +var encoder; + +var Constants = { + QUIC_IMAGE_TYPE_INVALID : 0, + QUIC_IMAGE_TYPE_GRAY : 1, + QUIC_IMAGE_TYPE_RGB16 : 2, + QUIC_IMAGE_TYPE_RGB24 : 3, + QUIC_IMAGE_TYPE_RGB32 : 4, + QUIC_IMAGE_TYPE_RGBA : 5 +}; + +var DEFevol = 3; +var DEFwmimax = 6; +var DEFwminext = 2048; +var need_init = true; +var DEFmaxclen = 26; +var evol = DEFevol; +var wmimax = DEFwmimax; +var wminext = DEFwminext; +var family_5bpc = { nGRcodewords:[0,0,0,0,0,0,0,0], + notGRcwlen:[0,0,0,0,0,0,0,0], + notGRprefixmask:[0,0,0,0,0,0,0,0], + notGRsuffixlen:[0,0,0,0,0,0,0,0], + xlatU2L:[0,0,0,0,0,0,0,0], + xlatL2U:[0,0,0,0,0,0,0,0] +}; +var family_8bpc = { nGRcodewords:[0,0,0,0,0,0,0,0], + notGRcwlen:[0,0,0,0,0,0,0,0], + notGRprefixmask:[0,0,0,0,0,0,0,0], + notGRsuffixlen:[0,0,0,0,0,0,0,0], + xlatU2L:[0,0,0,0,0,0,0,0], + xlatL2U:[0,0,0,0,0,0,0,0] +}; +var bppmask = [ 0x00000000, + 0x00000001, 0x00000003, 0x00000007, 0x0000000f, + 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, + 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, + 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff, + 0x0001ffff, 0x0003ffff, 0x0007ffff, 0x000fffff, + 0x001fffff, 0x003fffff, 0x007fffff, 0x00ffffff, + 0x01ffffff, 0x03ffffff, 0x07ffffff, 0x0fffffff, + 0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff]; + +var zeroLUT = []; + +var besttrigtab = [ + [ 550, 900, 800, 700, 500, 350, 300, 200, 180, 180, 160], + [ 110, 550, 900, 800, 550, 400, 350, 250, 140, 160, 140], + [ 100, 120, 550, 900, 700, 500, 400, 300, 220, 250, 160]]; + +var J = [ 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + +var lzeroes = [ + 8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0]; + +var tabrand_chaos = [ + 0x02c57542, 0x35427717, 0x2f5a2153, 0x9244f155, 0x7bd26d07, 0x354c6052, + 0x57329b28, 0x2993868e, 0x6cd8808c, 0x147b46e0, 0x99db66af, 0xe32b4cac, + 0x1b671264, 0x9d433486, 0x62a4c192, 0x06089a4b, 0x9e3dce44, 0xdaabee13, + 0x222425ea, 0xa46f331d, 0xcd589250, 0x8bb81d7f, 0xc8b736b9, 0x35948d33, + 0xd7ac7fd0, 0x5fbe2803, 0x2cfbc105, 0x013dbc4e, 0x7a37820f, 0x39f88e9e, + 0xedd58794, 0xc5076689, 0xfcada5a4, 0x64c2f46d, 0xb3ba3243, 0x8974b4f9, + 0x5a05aebd, 0x20afcd00, 0x39e2b008, 0x88a18a45, 0x600bde29, 0xf3971ace, + 0xf37b0a6b, 0x7041495b, 0x70b707ab, 0x06beffbb, 0x4206051f, 0xe13c4ee3, + 0xc1a78327, 0x91aa067c, 0x8295f72a, 0x732917a6, 0x1d871b4d, 0x4048f136, + 0xf1840e7e, 0x6a6048c1, 0x696cb71a, 0x7ff501c3, 0x0fc6310b, 0x57e0f83d, + 0x8cc26e74, 0x11a525a2, 0x946934c7, 0x7cd888f0, 0x8f9d8604, 0x4f86e73b, + 0x04520316, 0xdeeea20c, 0xf1def496, 0x67687288, 0xf540c5b2, 0x22401484, + 0x3478658a, 0xc2385746, 0x01979c2c, 0x5dad73c8, 0x0321f58b, 0xf0fedbee, + 0x92826ddf, 0x284bec73, 0x5b1a1975, 0x03df1e11, 0x20963e01, 0xa17cf12b, + 0x740d776e, 0xa7a6bf3c, 0x01b5cce4, 0x1118aa76, 0xfc6fac0a, 0xce927e9b, + 0x00bf2567, 0x806f216c, 0xbca69056, 0x795bd3e9, 0xc9dc4557, 0x8929b6c2, + 0x789d52ec, 0x3f3fbf40, 0xb9197368, 0xa38c15b5, 0xc3b44fa8, 0xca8333b0, + 0xb7e8d590, 0xbe807feb, 0xbf5f8360, 0xd99e2f5c, 0x372928e1, 0x7c757c4c, + 0x0db5b154, 0xc01ede02, 0x1fc86e78, 0x1f3985be, 0xb4805c77, 0x00c880fa, + 0x974c1b12, 0x35ab0214, 0xb2dc840d, 0x5b00ae37, 0xd313b026, 0xb260969d, + 0x7f4c8879, 0x1734c4d3, 0x49068631, 0xb9f6a021, 0x6b863e6f, 0xcee5debf, + 0x29f8c9fb, 0x53dd6880, 0x72b61223, 0x1f67a9fd, 0x0a0f6993, 0x13e59119, + 0x11cca12e, 0xfe6b6766, 0x16b6effc, 0x97918fc4, 0xc2b8a563, 0x94f2f741, + 0x0bfa8c9a, 0xd1537ae8, 0xc1da349c, 0x873c60ca, 0x95005b85, 0x9b5c080e, + 0xbc8abbd9, 0xe1eab1d2, 0x6dac9070, 0x4ea9ebf1, 0xe0cf30d4, 0x1ef5bd7b, + 0xd161043e, 0x5d2fa2e2, 0xff5d3cae, 0x86ed9f87, 0x2aa1daa1, 0xbd731a34, + 0x9e8f4b22, 0xb1c2c67a, 0xc21758c9, 0xa182215d, 0xccb01948, 0x8d168df7, + 0x04238cfe, 0x368c3dbc, 0x0aeadca5, 0xbad21c24, 0x0a71fee5, 0x9fc5d872, + 0x54c152c6, 0xfc329483, 0x6783384a, 0xeddb3e1c, 0x65f90e30, 0x884ad098, + 0xce81675a, 0x4b372f7d, 0x68bf9a39, 0x43445f1e, 0x40f8d8cb, 0x90d5acb6, + 0x4cd07282, 0x349eeb06, 0x0c9d5332, 0x520b24ef, 0x80020447, 0x67976491, + 0x2f931ca3, 0xfe9b0535, 0xfcd30220, 0x61a9e6cc, 0xa487d8d7, 0x3f7c5dd1, + 0x7d0127c5, 0x48f51d15, 0x60dea871, 0xc9a91cb7, 0x58b53bb3, 0x9d5e0b2d, + 0x624a78b4, 0x30dbee1b, 0x9bdf22e7, 0x1df5c299, 0x2d5643a7, 0xf4dd35ff, + 0x03ca8fd6, 0x53b47ed8, 0x6f2c19aa, 0xfeb0c1f4, 0x49e54438, 0x2f2577e6, + 0xbf876969, 0x72440ea9, 0xfa0bafb8, 0x74f5b3a0, 0x7dd357cd, 0x89ce1358, + 0x6ef2cdda, 0x1e7767f3, 0xa6be9fdb, 0x4f5f88f8, 0xba994a3a, 0x08ca6b65, + 0xe0893818, 0x9e00a16a, 0xf42bfc8f, 0x9972eedc, 0x749c8b51, 0x32c05f5e, + 0xd706805f, 0x6bfbb7cf, 0xd9210a10, 0x31a1db97, 0x923a9559, 0x37a7a1f6, + 0x059f8861, 0xca493e62, 0x65157e81, 0x8f6467dd, 0xab85ff9f, 0x9331aff2, + 0x8616b9f5, 0xedbd5695, 0xee7e29b1, 0x313ac44f, 0xb903112f, 0x432ef649, + 0xdc0a36c0, 0x61cf2bba, 0x81474925, 0xa8b6c7ad, 0xee5931de, 0xb2f8158d, + 0x59fb7409, 0x2e3dfaed, 0x9af25a3f, 0xe1fed4d5 ]; + +var rgb32_pixel_pad = 3; +var rgb32_pixel_r = 2; +var rgb32_pixel_g = 1; +var rgb32_pixel_b = 0; +var rgb32_pixel_size = 4; + +/* Helper Functions */ + +function ceil_log_2(val) +{ + if (val === 1) + return 0; + + var result = 1; + val -= 1; + while (val = val >>> 1) + result++; + + return result; +} + +function family_init(family, bpc, limit) +{ + var l; + for (l = 0; l < bpc; l++) + { + var altprefixlen, altcodewords; + altprefixlen = limit - bpc; + if (altprefixlen > bppmask[bpc - l]) + altprefixlen = bppmask[bpc - l]; + + altcodewords = bppmask[bpc] + 1 - (altprefixlen << l); + family.nGRcodewords[l] = (altprefixlen << l); + family.notGRcwlen[l] = altprefixlen + ceil_log_2(altcodewords); + family.notGRprefixmask[l] = bppmask[32 - altprefixlen]>>>0; + family.notGRsuffixlen[l] = ceil_log_2(altcodewords); + } + + /* decorelate_init */ + var pixelbitmask = bppmask[bpc]; + var pixelbitmaskshr = pixelbitmask >>> 1; + var s; + for (s = 0; s <= pixelbitmask; s++) { + if (s <= pixelbitmaskshr) { + family.xlatU2L[s] = s << 1; + } else { + family.xlatU2L[s] = ((pixelbitmask - s) << 1) + 1; + } + } + + /* corelate_init */ + for (s = 0; s <= pixelbitmask; s++) { + if (s & 0x01) { + family.xlatL2U[s] = pixelbitmask - (s >>> 1); + } else { + family.xlatL2U[s] = (s >>> 1); + } + } +} + +function quic_image_bpc(type) +{ + switch (type) { + case Constants.QUIC_IMAGE_TYPE_GRAY: + return 8; + case Constants.QUIC_IMAGE_TYPE_RGB16: + return 5; + case Constants.QUIC_IMAGE_TYPE_RGB24: + return 8; + case Constants.QUIC_IMAGE_TYPE_RGB32: + return 8; + case Constants.QUIC_IMAGE_TYPE_RGBA: + return 8; + case Constants.QUIC_IMAGE_TYPE_INVALID: + default: + console.log('quic: bad image type\n'); + return 0; + } +} + +function cnt_l_zeroes(bits) +{ + if (bits & 0xff800000) { + return lzeroes[bits >>> 24]; + } else if (bits & 0xffff8000) { + return 8 + lzeroes[(bits >>> 16) & 0x000000ff]; + } else if (bits & 0xffffff80) { + return 16 + lzeroes[(bits >>> 8) & 0x000000ff]; + } else { + return 24 + lzeroes[bits & 0x000000ff]; + } +} + +function golomb_decoding_8bpc(l, bits) +{ + var rc; + var cwlen; + + if (bits < 0 || bits > family_8bpc.notGRprefixmask[l]) + { + var zeroprefix = cnt_l_zeroes(bits); + cwlen = zeroprefix + 1 + l; + rc = (zeroprefix << l) | (bits >> (32-cwlen)) & bppmask[l]; + } + else + { + cwlen = family_8bpc.notGRcwlen[l]; + rc = family_8bpc.nGRcodewords[l] + ((bits >> (32-cwlen)) & bppmask[family_8bpc.notGRsuffixlen[l]]); + } + return {codewordlen:cwlen, rc:rc}; +} + +function golomb_code_len_8bpc(n, l) +{ + if (n < family_8bpc.nGRcodewords[l]) { + return (n >>> l) + 1 + l; + } else { + return family_8bpc.notGRcwlen[l]; + } +} + +function QuicModel(bpc) +{ + var bstart; + var bend = 0; + + this.levels = 0x1 << bpc; + this.n_buckets_ptrs = 0; + + switch (evol) { + case 1: + this.repfirst = 3; + this.firstsize = 1; + this.repnext = 2; + this.mulsize = 2; + break; + case 3: + this.repfirst = 1; + this.firstsize = 1; + this.repnext = 1; + this.mulsize = 2; + break; + case 5: + this.repfirst = 1; + this.firstsize = 1; + this.repnext = 1; + this.mulsize = 4; + break; + case 0: + case 2: + case 4: + console.log('quic: findmodelparams(): evol value obsolete!!!\n'); + break; + default: + console.log('quic: findmodelparams(): evol out of range!!!\n'); + } + + this.n_buckets = 0; + var repcntr = this.repfirst + 1; + var bsize = this.firstsize; + + do { + if (this.n_buckets) { + bstart = bend + 1; + } else { + bstart = 0; + } + + if (!--repcntr) { + repcntr = this.repnext; + bsize *= this.mulsize; + } + + bend = bstart + bsize - 1; + if (bend + bsize >= this.levels) { + bend = this.levels - 1; + } + + if (!this.n_buckets_ptrs) { + this.n_buckets_ptrs = this.levels; + } + + (this.n_buckets)++; + } while (bend < this.levels - 1); +} + +QuicModel.prototype = { + n_buckets : 0, + n_buckets_ptrs : 0, + repfirst : 0, + firstsize : 0, + repnext : 0, + mulsize : 0, + levels :0 +}; + +function QuicBucket() +{ + this.counters = [0,0,0,0,0,0,0,0]; +} + +QuicBucket.prototype = { + bestcode: 0, + + reste : function (bpp) + { + this.bestcode = bpp; + this.counters = [0,0,0,0,0,0,0,0]; + }, + + update_model_8bpc : function (state, curval, bpp) + { + var i; + + var bestcode = bpp - 1; + var bestcodelen = (this.counters[bestcode] += golomb_code_len_8bpc(curval, bestcode)); + + for (i = bpp - 2; i >= 0; i--) { + var ithcodelen = (this.counters[i] += golomb_code_len_8bpc(curval, i)); + + if (ithcodelen < bestcodelen) { + bestcode = i; + bestcodelen = ithcodelen; + } + } + + this.bestcode = bestcode; + + if (bestcodelen > state.wm_trigger) { + for (i = 0; i < bpp; i++) { + this.counters[i] = this.counters[i] >>> 1; + } + } + } +}; + +function QuicFamilyStat() +{ + this.buckets_ptrs = []; + this.buckets_buf = []; +} + +QuicFamilyStat.prototype = { + + fill_model_structures : function(model) + { + var bstart; + var bend = 0; + var bnumber = 0; + + var repcntr = model.repfirst + 1; + var bsize = model.firstsize; + + do { + if (bnumber) { + bstart = bend + 1; + } else { + bstart = 0; + } + + if (!--repcntr) { + repcntr = model.repnext; + bsize *= model.mulsize; + } + + bend = bstart + bsize - 1; + if (bend + bsize >= model.levels) { + bend = model.levels - 1; + } + + this.buckets_buf[bnumber] = new QuicBucket(); + + var i; + for (i = bstart; i <= bend; i++) { + this.buckets_ptrs[i] = this.buckets_buf[bnumber]; + } + + bnumber++; + } while (bend < model.levels - 1); + return true; + } +}; + +function QuicChannel(model_8bpc, model_5bpc) +{ + this.state = new CommonState(); + this.family_stat_8bpc = new QuicFamilyStat(); + this.family_stat_5bpc = new QuicFamilyStat(); + this.correlate_row = { zero: 0 , row:[] }; + this.model_8bpc = model_8bpc; + this.model_5bpc = model_5bpc; + this.buckets_ptrs = []; + + if (!this.family_stat_8bpc.fill_model_structures(this.model_8bpc)) + return undefined; + + if (!this.family_stat_5bpc.fill_model_structures(this.model_5bpc)) + return undefined; +} + +QuicChannel.prototype = { + + reste : function (bpc) + { + var j; + this.correlate_row = { zero: 0 , row: []}; + + if (bpc == 8) { + for (j = 0; j < this.model_8bpc.n_buckets; j++) + this.family_stat_8bpc.buckets_buf[j].reste(7); + this.buckets_ptrs = this.family_stat_8bpc.buckets_ptrs; + } else if (bpc == 5) { + for (j = 0; j < this.model_5bpc.n_buckets; j++) + this.family_stat_8bpc.buckets_buf[j].reste(4); + this.buckets_ptrs = this.family_stat_5bpc.buckets_ptrs; + } else { + console.log('quic: %s: bad bpc %d\n', __FUNCTION__, bpc); + return false; + } + + this.state.reste(); + return true; + } +}; + +function CommonState() +{ +} + +CommonState.prototype = { + waitcnt: 0, + tabrand_seed: 0xff, + wm_trigger: 0, + wmidx: 0, + wmileft: wminext, + melcstate: 0, + melclen: 0, + melcorder: 0, + + set_wm_trigger : function() + { + var wm = this.wmidx; + if (wm > 10) { + wm = 10; + } + + this.wm_trigger = besttrigtab[Math.floor(evol / 2)][wm]; + }, + + reste : function() + { + this.waitcnt = 0; + this.tabrand_seed = 0x0ff; + this.wmidx = 0; + this.wmileft = wminext; + + this.set_wm_trigger(); + + this.melcstate = 0; + this.melclen = J[0]; + this.melcorder = 1 << this.melclen; + }, + + tabrand : function() + { + this.tabrand_seed++; + return tabrand_chaos[this.tabrand_seed & 0x0ff]; + } +}; + + +function QuicEncoder() +{ + this.rgb_state = new CommonState(); + this.model_8bpc = new QuicModel(8); + this.model_5bpc = new QuicModel(5); + this.channels = []; + + var i; + for (i = 0; i < 4; i++) { + this.channels[i] = new QuicChannel(this.model_8bpc, this.model_5bpc); + if (!this.channels[i]) + { + console.log('quic: failed to create channel'); + return undefined; + } + } +} + +QuicEncoder.prototype = { + type: 0, + width: 0, + height: 0, + io_idx: 0, + io_available_bits: 0, + io_word: 0, + io_next_word: 0, + io_now: 0, + io_end: 0, + rows_completed: 0 +}; + +QuicEncoder.prototype.reste = function(io_ptr) +{ + this.rgb_state.reste(); + + this.io_now = io_ptr; + this.io_end = this.io_now.length; + this.io_idx = 0; + this.rows_completed = 0; + return true; +}; + +QuicEncoder.prototype.read_io_word = function() +{ + if (this.io_idx >= this.io_end) + throw('quic: out of data'); + this.io_next_word = this.io_now[this.io_idx++] | this.io_now[this.io_idx++]<<8 | this.io_now[this.io_idx++]<<16 | this.io_now[this.io_idx++]<<24; +}; + +QuicEncoder.prototype.decode_eatbits = function (len) +{ + this.io_word = this.io_word << len; + + var delta = (this.io_available_bits - len); + if (delta >= 0) + { + this.io_available_bits = delta; + this.io_word |= this.io_next_word >>> this.io_available_bits; + } + else + { + delta = -1 * delta; + this.io_word |= this.io_next_word << delta; + this.read_io_word(); + this.io_available_bits = 32 - delta; + this.io_word |= this.io_next_word >>> this.io_available_bits; + } +}; + +QuicEncoder.prototype.decode_eat32bits = function() +{ + this.decode_eatbits(16); + this.decode_eatbits(16); +}; + +QuicEncoder.prototype.reste_channels = function(bpc) +{ + var i; + + for (i = 0; i < 4; i++) + if (!this.channels[i].reste(bpc)) + return false; + return true; +}; + +QuicEncoder.prototype.quic_decode_begin = function(io_ptr) +{ + if (!this.reste(io_ptr)) { + return false; + } + + this.io_idx = 0; + this.io_next_word = this.io_now[this.io_idx++] | this.io_now[this.io_idx++]<<8 | this.io_now[this.io_idx++]<<16 | this.io_now[this.io_idx++]<<24; + this.io_word = this.io_next_word; + this.io_available_bits = 0; + + var magic = this.io_word; + this.decode_eat32bits(); + if (magic != 0x43495551) /*QUIC*/ { + console.log('quic: bad magic '+magic.toString(16)); + return false; + } + + var version = this.io_word; + this.decode_eat32bits(); + if (version != ((0 << 16) | (0 & 0xffff))) { + console.log('quic: bad version '+version.toString(16)); + return false; + } + + this.type = this.io_word; + this.decode_eat32bits(); + + this.width = this.io_word; + this.decode_eat32bits(); + + this.height = this.io_word; + this.decode_eat32bits(); + + var bpc = quic_image_bpc(this.type); + + if (!this.reste_channels(bpc)) + return false; + + return true; +}; + +QuicEncoder.prototype.quic_rgb32_uncompress_row0_seg = function (i, cur_row, end, + waitmask, bpc, bpc_mask) +{ + var stopidx; + var n_channels = 3; + var c; + var a; + + if (!i) { + cur_row[rgb32_pixel_pad] = 0; + c = 0; + do + { + a = golomb_decoding_8bpc(this.channels[c].buckets_ptrs[this.channels[c].correlate_row.zero].bestcode, this.io_word); + this.channels[c].correlate_row.row[0] = a.rc; + cur_row[2-c] = (family_8bpc.xlatL2U[a.rc]&0xFF); + this.decode_eatbits(a.codewordlen); + } while (++c < n_channels); + + if (this.rgb_state.waitcnt) { + --this.rgb_state.waitcnt; + } else { + this.rgb_state.waitcnt = (this.rgb_state.tabrand() & waitmask); + c = 0; + do + { + this.channels[c].buckets_ptrs[this.channels[c].correlate_row.zero].update_model_8bpc(this.rgb_state, this.channels[c].correlate_row.row[0], bpc); + } while (++c < n_channels); + } + stopidx = ++i + this.rgb_state.waitcnt; + } else { + stopidx = i + this.rgb_state.waitcnt; + } + + while (stopidx < end) { + for (; i <= stopidx; i++) { + cur_row[(i* rgb32_pixel_size)+rgb32_pixel_pad] = 0; + c = 0; + do + { + a = golomb_decoding_8bpc(this.channels[c].buckets_ptrs[this.channels[c].correlate_row.row[i - 1]].bestcode, this.io_word); + this.channels[c].correlate_row.row[i] = a.rc; + cur_row[(i* rgb32_pixel_size)+(2-c)] = (family_8bpc.xlatL2U[a.rc] + cur_row[((i-1) * rgb32_pixel_size) + (2-c)]) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } while (++c < n_channels); + } + c = 0; + do + { + this.channels[c].buckets_ptrs[this.channels[c].correlate_row.row[stopidx - 1]].update_model_8bpc(this.rgb_state, this.channels[c].correlate_row.row[stopidx], bpc); + } while (++c < n_channels); + stopidx = i + (this.rgb_state.tabrand() & waitmask); + } + + for (; i < end; i++) { + cur_row[(i* rgb32_pixel_size)+rgb32_pixel_pad] = 0; + c = 0; + do + { + a = golomb_decoding_8bpc(this.channels[c].buckets_ptrs[this.channels[c].correlate_row.row[i - 1]].bestcode, this.io_word); + this.channels[c].correlate_row.row[i] = a.rc; + cur_row[(i* rgb32_pixel_size)+(2-c)] = (family_8bpc.xlatL2U[a.rc] + cur_row[((i-1) * rgb32_pixel_size) + (2-c)]) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } while (++c < n_channels); + } + this.rgb_state.waitcnt = stopidx - end; +}; + +QuicEncoder.prototype.quic_rgb32_uncompress_row0 = function (cur_row) +{ + var bpc = 8; + var bpc_mask = 0xff; + var pos = 0; + var width = this.width; + + while ((wmimax > this.rgb_state.wmidx) && (this.rgb_state.wmileft <= width)) { + if (this.rgb_state.wmileft) { + this.quic_rgb32_uncompress_row0_seg(pos, cur_row, + pos + this.rgb_state.wmileft, + bppmask[this.rgb_state.wmidx], + bpc, bpc_mask); + pos += this.rgb_state.wmileft; + width -= this.rgb_state.wmileft; + } + + this.rgb_state.wmidx++; + this.rgb_state.set_wm_trigger(); + this.rgb_state.wmileft = wminext; + } + + if (width) { + this.quic_rgb32_uncompress_row0_seg(pos, cur_row, pos + width, + bppmask[this.rgb_state.wmidx], bpc, bpc_mask); + if (wmimax > this.rgb_state.wmidx) { + this.rgb_state.wmileft -= width; + } + } +}; + +QuicEncoder.prototype.quic_rgb32_uncompress_row_seg = function( prev_row, cur_row, i, end, bpc, bpc_mask) +{ + var n_channels = 3; + var waitmask = bppmask[this.rgb_state.wmidx]; + + var a; + var run_index = 0; + var stopidx = 0; + var run_end = 0; + var c; + + if (!i) + { + cur_row[rgb32_pixel_pad] = 0; + + c = 0; + do { + a = golomb_decoding_8bpc(this.channels[c].buckets_ptrs[this.channels[c].correlate_row.zero].bestcode, this.io_word); + this.channels[c].correlate_row.row[0] = a.rc; + cur_row[2-c] = (family_8bpc.xlatL2U[this.channels[c].correlate_row.row[0]] + prev_row[2-c]) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } while (++c < n_channels); + + if (this.rgb_state.waitcnt) { + --this.rgb_state.waitcnt; + } else { + this.rgb_state.waitcnt = (this.rgb_state.tabrand() & waitmask); + c = 0; + do { + this.channels[c].buckets_ptrs[this.channels[c].correlate_row.zero].update_model_8bpc(this.rgb_state, this.channels[c].correlate_row.row[0], bpc); + } while (++c < n_channels); + } + stopidx = ++i + this.rgb_state.waitcnt; + } else { + stopidx = i + this.rgb_state.waitcnt; + } + for (;;) { + var rc = 0; + while (stopidx < end && !rc) { + for (; i <= stopidx && !rc; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + var pixelm2 = (i-2) * rgb32_pixel_size; + + if ( prev_row[pixelm1+rgb32_pixel_r] == prev_row[pixel+rgb32_pixel_r] && prev_row[pixelm1+rgb32_pixel_g] == prev_row[pixel+rgb32_pixel_g] && prev_row[pixelm1 + rgb32_pixel_b] == prev_row[pixel+rgb32_pixel_b]) + { + if (run_index != i && i > 2 && (cur_row[pixelm1+rgb32_pixel_r] == cur_row[pixelm2+rgb32_pixel_r] && cur_row[pixelm1+rgb32_pixel_g] == cur_row[pixelm2+rgb32_pixel_g] && cur_row[pixelm1+rgb32_pixel_b] == cur_row[pixelm2+rgb32_pixel_b])) + { + /* do run */ + this.rgb_state.waitcnt = stopidx - i; + run_index = i; + run_end = i + this.decode_run(this.rgb_state); + + for (; i < run_end; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + cur_row[pixel+rgb32_pixel_pad] = 0; + cur_row[pixel+rgb32_pixel_r] = cur_row[pixelm1+rgb32_pixel_r]; + cur_row[pixel+rgb32_pixel_g] = cur_row[pixelm1+rgb32_pixel_g]; + cur_row[pixel+rgb32_pixel_b] = cur_row[pixelm1+rgb32_pixel_b]; + } + + if (i == end) { + return; + } + else + { + stopidx = i + this.rgb_state.waitcnt; + rc = 1; + break; + } + } + } + + c = 0; + cur_row[pixel+rgb32_pixel_pad] = 0; + do { + var cc = this.channels[c]; + var cr = cc.correlate_row; + + a = golomb_decoding_8bpc(cc.buckets_ptrs[cr.row[i-1]].bestcode, this.io_word); + cr.row[i] = a.rc; + cur_row[pixel+(2-c)] = (family_8bpc.xlatL2U[a.rc] + ((cur_row[pixelm1+(2-c)] + prev_row[pixel+(2-c)]) >> 1)) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } while (++c < n_channels); + } + if (rc) + break; + + c = 0; + do { + this.channels[c].buckets_ptrs[this.channels[c].correlate_row.row[stopidx - 1]].update_model_8bpc(this.rgb_state, this.channels[c].correlate_row.row[stopidx], bpc); + } while (++c < n_channels); + + stopidx = i + (this.rgb_state.tabrand() & waitmask); + } + + for (; i < end && !rc; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + var pixelm2 = (i-2) * rgb32_pixel_size; + + if (prev_row[pixelm1+rgb32_pixel_r] == prev_row[pixel+rgb32_pixel_r] && prev_row[pixelm1+rgb32_pixel_g] == prev_row[pixel+rgb32_pixel_g] && prev_row[pixelm1+rgb32_pixel_b] == prev_row[pixel+rgb32_pixel_b]) + { + if (run_index != i && i > 2 && (cur_row[pixelm1+rgb32_pixel_r] == cur_row[pixelm2+rgb32_pixel_r] && cur_row[pixelm1+rgb32_pixel_g] == cur_row[pixelm2+rgb32_pixel_g] && cur_row[pixelm1+rgb32_pixel_b] == cur_row[pixelm2+rgb32_pixel_b])) + { + /* do run */ + this.rgb_state.waitcnt = stopidx - i; + run_index = i; + run_end = i + this.decode_run(this.rgb_state); + + for (; i < run_end; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + cur_row[pixel+rgb32_pixel_pad] = 0; + cur_row[pixel+rgb32_pixel_r] = cur_row[pixelm1+rgb32_pixel_r]; + cur_row[pixel+rgb32_pixel_g] = cur_row[pixelm1+rgb32_pixel_g]; + cur_row[pixel+rgb32_pixel_b] = cur_row[pixelm1+rgb32_pixel_b]; + } + + if (i == end) { + return; + } + else + { + stopidx = i + this.rgb_state.waitcnt; + rc = 1; + break; + } + } + } + + cur_row[pixel+rgb32_pixel_pad] = 0; + c = 0; + do + { + a = golomb_decoding_8bpc(this.channels[c].buckets_ptrs[this.channels[c].correlate_row.row[i-1]].bestcode, this.io_word); + this.channels[c].correlate_row.row[i] = a.rc; + cur_row[pixel+(2-c)] = (family_8bpc.xlatL2U[a.rc] + ((cur_row[pixelm1+(2-c)] + prev_row[pixel+(2-c)]) >> 1)) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } while (++c < n_channels); + } + + if (!rc) + { + this.rgb_state.waitcnt = stopidx - end; + return; + } + } +}; + +QuicEncoder.prototype.decode_run = function(state) +{ + var runlen = 0; + + do { + var hits; + var x = (~(this.io_word >>> 24)>>>0)&0xff; + var temp = zeroLUT[x]; + + for (hits = 1; hits <= temp; hits++) { + runlen += state.melcorder; + + if (state.melcstate < 32) { + state.melclen = J[++state.melcstate]; + state.melcorder = (1 << state.melclen); + } + } + if (temp != 8) { + this.decode_eatbits(temp + 1); + + break; + } + this.decode_eatbits(8); + } while (true); + + if (state.melclen) { + runlen += this.io_word >>> (32 - state.melclen); + this.decode_eatbits(state.melclen); + } + + if (state.melcstate) { + state.melclen = J[--state.melcstate]; + state.melcorder = (1 << state.melclen); + } + + return runlen; +}; + +QuicEncoder.prototype.quic_rgb32_uncompress_row = function (prev_row, cur_row) +{ + var bpc = 8; + var bpc_mask = 0xff; + var pos = 0; + var width = this.width; + + while ((wmimax > this.rgb_state.wmidx) && (this.rgb_state.wmileft <= width)) { + if (this.rgb_state.wmileft) { + this.quic_rgb32_uncompress_row_seg(prev_row, cur_row, pos, + pos + this.rgb_state.wmileft, bpc, bpc_mask); + pos += this.rgb_state.wmileft; + width -= this.rgb_state.wmileft; + } + + this.rgb_state.wmidx++; + this.rgb_state.set_wm_trigger(); + this.rgb_state.wmileft = wminext; + } + + if (width) { + this.quic_rgb32_uncompress_row_seg(prev_row, cur_row, pos, + pos + width, bpc, bpc_mask); + if (wmimax > this.rgb_state.wmidx) { + this.rgb_state.wmileft -= width; + } + } +}; + +QuicEncoder.prototype.quic_four_uncompress_row0_seg = function (channel, i, + correlate_row, cur_row, end, waitmask, + bpc, bpc_mask) +{ + var stopidx; + var a; + + if (i == 0) { + a = golomb_decoding_8bpc(channel.buckets_ptrs[correlate_row.zero].bestcode, this.io_word); + correlate_row.row[0] = a.rc; + cur_row[rgb32_pixel_pad] = family_8bpc.xlatL2U[a.rc]; + this.decode_eatbits(a.codewordlen); + + if (channel.state.waitcnt) { + --channel.state.waitcnt; + } else { + channel.state.waitcnt = (channel.state.tabrand() & waitmask); + channel.buckets_ptrs[correlate_row.zero].update_model_8bpc(channel.state, correlate_row.row[0], bpc); + } + stopidx = ++i + channel.state.waitcnt; + } else { + stopidx = i + channel.state.waitcnt; + } + + while (stopidx < end) { + var pbucket; + + for (; i <= stopidx; i++) { + pbucket = channel.buckets_ptrs[correlate_row.row[i - 1]]; + + a = golomb_decoding_8bpc(pbucket.bestcode, this.io_word); + correlate_row.row[i] = a.rc; + cur_row[(i*rgb32_pixel_size)+rgb32_pixel_pad] = (family_8bpc.xlatL2U[a.rc] + cur_row[((i-1)*rgb32_pixel_size)+rgb32_pixel_pad]) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } + + pbucket.update_model_8bpc(channel.state, correlate_row.row[stopidx], bpc); + + stopidx = i + (channel.state.tabrand() & waitmask); + } + + for (; i < end; i++) { + a = golomb_decoding_8bpc(channel.buckets_ptrs[correlate_row.row[i-1]].bestcode, this.io_word); + + correlate_row.row[i] = a.rc; + cur_row[(i*rgb32_pixel_size)+rgb32_pixel_pad] = (family_8bpc.xlatL2U[a.rc] + cur_row[((i-1)*rgb32_pixel_size)+rgb32_pixel_pad]) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } + channel.state.waitcnt = stopidx - end; +}; + +QuicEncoder.prototype.quic_four_uncompress_row0 = function(channel, cur_row) +{ + var bpc = 8; + var bpc_mask = 0xff; + var correlate_row = channel.correlate_row; + var pos = 0; + var width = this.width; + + while ((wmimax > channel.state.wmidx) && (channel.state.wmileft <= width)) { + if (channel.state.wmileft) { + this.quic_four_uncompress_row0_seg(channel, pos, correlate_row, cur_row, + pos + channel.state.wmileft, bppmask[channel.state.wmidx], + bpc, bpc_mask); + pos += channel.state.wmileft; + width -= channel.state.wmileft; + } + + channel.state.wmidx++; + channel.state.set_wm_trigger(); + channel.state.wmileft = wminext; + } + + if (width) { + this.quic_four_uncompress_row0_seg(channel, pos, correlate_row, cur_row, pos + width, + bppmask[channel.state.wmidx], bpc, bpc_mask); + if (wmimax > channel.state.wmidx) { + channel.state.wmileft -= width; + } + } +}; + +QuicEncoder.prototype.quic_four_uncompress_row_seg = function (channel, + correlate_row, prev_row, cur_row, i, + end, bpc, bpc_mask) +{ + var waitmask = bppmask[channel.state.wmidx]; + var stopidx; + + var run_index = 0; + var run_end; + + var a; + + if (i == 0) { + a = golomb_decoding_8bpc(channel.buckets_ptrs[correlate_row.zero].bestcode, this.io_word); + + correlate_row.row[0] = a.rc; + cur_row[rgb32_pixel_pad] = (family_8bpc.xlatL2U[a.rc] + prev_row[rgb32_pixel_pad]) & bpc_mask; + this.decode_eatbits(a.codewordlen); + + if (channel.state.waitcnt) { + --channel.state.waitcnt; + } else { + channel.state.waitcnt = (channel.state.tabrand() & waitmask); + channel.buckets_ptrs[correlate_row.zero].update_model_8bpc(channel.state, correlate_row.row[0], bpc); + } + stopidx = ++i + channel.state.waitcnt; + } else { + stopidx = i + channel.state.waitcnt; + } + for (;;) { + var rc = 0; + while (stopidx < end && !rc) { + var pbucket; + for (; i <= stopidx && !rc; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + var pixelm2 = (i-2) * rgb32_pixel_size; + + if (prev_row[pixelm1+rgb32_pixel_pad] == prev_row[pixel+rgb32_pixel_pad]) + { + if (run_index != i && i > 2 && cur_row[pixelm1+rgb32_pixel_pad] == cur_row[pixelm2+rgb32_pixel_pad]) + { + /* do run */ + channel.state.waitcnt = stopidx - i; + run_index = i; + + run_end = i + this.decode_run(channel.state); + + for (; i < run_end; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + cur_row[pixel+rgb32_pixel_pad] = cur_row[pixelm1+rgb32_pixel_pad]; + } + + if (i == end) { + return; + } + else + { + stopidx = i + channel.state.waitcnt; + rc = 1; + break; + } + } + } + + pbucket = channel.buckets_ptrs[correlate_row.row[i - 1]]; + a = golomb_decoding_8bpc(pbucket.bestcode, this.io_word); + correlate_row.row[i] = a.rc; + cur_row[pixel+rgb32_pixel_pad] = (family_8bpc.xlatL2U[a.rc] + ((cur_row[pixelm1+rgb32_pixel_pad] + prev_row[pixel+rgb32_pixel_pad]) >> 1)) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } + if (rc) + break; + + pbucket.update_model_8bpc(channel.state, correlate_row.row[stopidx], bpc); + + stopidx = i + (channel.state.tabrand() & waitmask); + } + + for (; i < end && !rc; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + var pixelm2 = (i-2) * rgb32_pixel_size; + if (prev_row[pixelm1+rgb32_pixel_pad] == prev_row[pixel+rgb32_pixel_pad]) + { + if (run_index != i && i > 2 && cur_row[pixelm1+rgb32_pixel_pad] == cur_row[pixelm2+rgb32_pixel_pad]) + { + /* do run */ + channel.state.waitcnt = stopidx - i; + run_index = i; + + run_end = i + this.decode_run(channel.state); + + for (; i < run_end; i++) { + var pixel = i * rgb32_pixel_size; + var pixelm1 = (i-1) * rgb32_pixel_size; + cur_row[pixel+rgb32_pixel_pad] = cur_row[pixelm1+rgb32_pixel_pad]; + } + + if (i == end) { + return; + } + else + { + stopidx = i + channel.state.waitcnt; + rc = 1; + break; + } + } + } + + a = golomb_decoding_8bpc(channel.buckets_ptrs[correlate_row.row[i-1]].bestcode, this.io_word); + correlate_row.row[i] = a.rc; + cur_row[pixel+rgb32_pixel_pad] = (family_8bpc.xlatL2U[a.rc] + ((cur_row[pixelm1+rgb32_pixel_pad] + prev_row[pixel+rgb32_pixel_pad]) >> 1)) & bpc_mask; + this.decode_eatbits(a.codewordlen); + } + + if (!rc) + { + channel.state.waitcnt = stopidx - end; + return; + } + } +}; + +QuicEncoder.prototype.quic_four_uncompress_row = function(channel, prev_row, + cur_row) +{ + var bpc = 8; + var bpc_mask = 0xff; + var correlate_row = channel.correlate_row; + var pos = 0; + var width = this.width; + + while ((wmimax > channel.state.wmidx) && (channel.state.wmileft <= width)) { + if (channel.state.wmileft) { + this.quic_four_uncompress_row_seg(channel, correlate_row, prev_row, cur_row, pos, + pos + channel.state.wmileft, bpc, bpc_mask); + pos += channel.state.wmileft; + width -= channel.state.wmileft; + } + + channel.state.wmidx++; + channel.state.set_wm_trigger(); + channel.state.wmileft = wminext; + } + + if (width) { + this.quic_four_uncompress_row_seg(channel, correlate_row, prev_row, cur_row, pos, + pos + width, bpc, bpc_mask); + if (wmimax > channel.state.wmidx) { + channel.state.wmileft -= width; + } + } +}; + +/* We need to be generating rgb32 or rgba */ +QuicEncoder.prototype.quic_decode = function(buf, stride) +{ + var row; + + switch (this.type) + { + case Constants.QUIC_IMAGE_TYPE_RGB32: + case Constants.QUIC_IMAGE_TYPE_RGB24: + this.channels[0].correlate_row.zero = 0; + this.channels[1].correlate_row.zero = 0; + this.channels[2].correlate_row.zero = 0; + this.quic_rgb32_uncompress_row0(buf); + + this.rows_completed++; + for (row = 1; row < this.height; row++) + { + var prev = buf; + buf = prev.subarray(stride); + this.channels[0].correlate_row.zero = this.channels[0].correlate_row.row[0]; + this.channels[1].correlate_row.zero = this.channels[1].correlate_row.row[0]; + this.channels[2].correlate_row.zero = this.channels[2].correlate_row.row[0]; + this.quic_rgb32_uncompress_row(prev, buf); + this.rows_completed++; + } + break; + case Constants.QUIC_IMAGE_TYPE_RGB16: + console.log('quic: unsupported output format\n'); + return false; + break; + case Constants.QUIC_IMAGE_TYPE_RGBA: + this.channels[0].correlate_row.zero = 0; + this.channels[1].correlate_row.zero = 0; + this.channels[2].correlate_row.zero = 0; + this.quic_rgb32_uncompress_row0(buf); + + this.channels[3].correlate_row.zero = 0; + this.quic_four_uncompress_row0(this.channels[3], buf); + + this.rows_completed++; + for (row = 1; row < this.height; row++) { + var prev = buf; + buf = prev.subarray(stride); + + this.channels[0].correlate_row.zero = this.channels[0].correlate_row.row[0]; + this.channels[1].correlate_row.zero = this.channels[1].correlate_row.row[0]; + this.channels[2].correlate_row.zero = this.channels[2].correlate_row.row[0]; + this.quic_rgb32_uncompress_row(prev, buf); + + this.channels[3].correlate_row.zero = this.channels[3].correlate_row.row[0]; + this.quic_four_uncompress_row(encoder.channels[3], prev, buf); + this.rows_completed++; + } + break; + + case Constants.QUIC_IMAGE_TYPE_GRAY: + console.log('quic: unsupported output format\n'); + return false; + break; + + case Constants.QUIC_IMAGE_TYPE_INVALID: + default: + console.log('quic: bad image type\n'); + return false; + } + return true; +}; + +QuicEncoder.prototype.simple_quic_decode = function(buf) +{ + var stride = 4; /* FIXME - proper stride calc please */ + if (!this.quic_decode_begin(buf)) + return undefined; + if (this.type != Constants.QUIC_IMAGE_TYPE_RGB32 && this.type != Constants.QUIC_IMAGE_TYPE_RGB24 + && this.type != Constants.QUIC_IMAGE_TYPE_RGBA) + return undefined; + var out = new Uint8Array(this.width*this.height*4); + out[0] = 69; + if (this.quic_decode( out, (this.width * stride))) + return out; + return undefined; +}; + +function SpiceQuic() +{ +} + +SpiceQuic.prototype = +{ + from_dv: function(dv, at, mb) + { + if (!encoder) + throw('quic: no quic encoder'); + this.data_size = dv.getUint32(at, true); + at += 4; + var buf = new Uint8Array(mb.slice(at)); + this.outptr = encoder.simple_quic_decode(buf); + if (this.outptr) + { + this.type = encoder.type; + this.width = encoder.width; + this.height = encoder.height; + } + at += buf.length; + return at; + } +}; + +function convert_spice_quic_to_web(context, spice_quic) +{ + var ret = context.createImageData(spice_quic.width, spice_quic.height); + var i; + for (i = 0; i < (ret.width * ret.height * 4); i+=4) + { + ret.data[i + 0] = spice_quic.outptr[i + 2]; + ret.data[i + 1] = spice_quic.outptr[i + 1]; + ret.data[i + 2] = spice_quic.outptr[i + 0]; + if (spice_quic.type !== Constants.QUIC_IMAGE_TYPE_RGBA) + ret.data[i + 3] = 255; + else + ret.data[i + 3] = 255 - spice_quic.outptr[i + 3]; + } + return ret; +} + +/* Module initialization */ +if (need_init) +{ + need_init = false; + + family_init(family_8bpc, 8, DEFmaxclen); + family_init(family_5bpc, 5, DEFmaxclen); + /* init_zeroLUT */ + var i, j, k, l; + + j = k = 1; + l = 8; + for (i = 0; i < 256; ++i) { + zeroLUT[i] = l; + --k; + if (k == 0) { + k = j; + --l; + j *= 2; + } + } + + encoder = new QuicEncoder(); + + if (!encoder) + throw('quic: failed to create encoder'); +} + +export { + Constants, + SpiceQuic, + convert_spice_quic_to_web +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/resize.js b/packages/itmat-ui-react/src/components/lxd/spice/src/resize.js new file mode 100644 index 000000000..eed2f9935 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/resize.js @@ -0,0 +1,94 @@ +/* eslint-disable no-unused-vars */ + +/* + Copyright (C) 2014 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** resize.js +** This bit of Javascript is a set of logic to help with window +** resizing, using the agent channel to request screen resizes. +** +** It's a bit tricky, as we want to wait for resizing to settle down +** before sending a size. Further, while horizontal resizing to use the whole +** browser width is fairly easy to arrange with css, resizing an element to use +** the whole vertical space (or to force a middle div to consume the bulk of the browser +** window size) is tricky, and the consensus seems to be that Javascript is +** the only right way to do it. +**--------------------------------------------------------------------------*/ +function resize_helper(sc) +{ + if (!sc) { + return; + } + + var width = document.getElementById(sc.screen_id).clientWidth; + var wrapper = document.getElementById('spice-area'); + + var isFullScreen = document.isFullScreen + || document.fullscreenElement + || document.webkitIsFullScreen + || document.webkitFullscreenElement + || document.mozFullScreenElement + || document.msFullscreenElement; + + var height = window.innerHeight - wrapper.getBoundingClientRect().top; + + /* leave a margin at the bottom when not in full screen */ + if (!isFullScreen) { + height = height - 20; + } + + /* minimum height constraint */ + if (height < 200) { + height = 200; + } + + /* Xorg requires height be a multiple of 8; round down */ + if (height % 8 > 0) + height -= (height % 8); + + /* Xorg requires width be a multiple of 8; round down */ + if (width % 8 > 0) + width -= (width % 8); + + sc.resize_window(0, width, height, 32, 0, 0); + sc.spice_resize_timer = undefined; +} + +function handle_resize(e) +{ + var sc = window.spice_connection; + + if (!sc) { + return; + } + + if (sc.spice_resize_timer) + { + window.clearTimeout(sc.spice_resize_timer); + sc.spice_resize_timer = undefined; + } + + sc.spice_resize_timer = window.setTimeout(resize_helper, 200, sc); +} + +export { + resize_helper, + handle_resize +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/simulatecursor.js b/packages/itmat-ui-react/src/components/lxd/spice/src/simulatecursor.js new file mode 100644 index 000000000..edfb280c4 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/simulatecursor.js @@ -0,0 +1,212 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/* + Copyright (C) 2013 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** SpiceSimulateCursor +** Internet Explorer 10 does not support data uri's in cursor assignment. +** This file provides a number of gimmicks to compensate. First, if there +** is a preloaded cursor available, we will use that. Failing that, we will +** simulate a cursor using an image that is moved around the screen. +**--------------------------------------------------------------------------*/ + +import { SpiceDataView } from './spicedataview.js'; +import { hex_sha1 } from './thirdparty/sha1.js'; + +var SpiceSimulateCursor = { + + cursors : [], + unknown_cursors : [], + warned: false, + + add_cursor: function(sha1, value) + { + SpiceSimulateCursor.cursors[sha1] = value; + }, + + unknown_cursor: function(sha1, curdata) + { + if (! SpiceSimulateCursor.warned) + { + SpiceSimulateCursor.warned = true; + alert('Internet Explorer does not support dynamic cursors. ' + + 'This page will now simulate cursors with images, ' + + 'which will be imperfect. We recommend using Chrome or Firefox instead. ' + + '\n\nIf you need to use Internet Explorer, you can create a static cursor ' + + 'file for each cursor your application uses. ' + + 'View the console log for more information on creating static cursors for your environment.'); + } + + if (! SpiceSimulateCursor.unknown_cursors[sha1]) + { + SpiceSimulateCursor.unknown_cursors[sha1] = curdata; + console.log('Unknown cursor. Simulation required. To avoid simulation for this cursor, create and include a custom javascript file, and add the following line:'); + console.log('SpiceCursorSimulator.add_cursor("' + sha1 + '"), ".cur");'); + console.log('And then run following command, redirecting output into .cur:'); + console.log('php -r "echo urldecode(\'' + curdata + '\');"'); + } + }, + + simulate_cursor: function (spicecursor, cursor, screen, pngstr) + { + var cursor_sha = hex_sha1(pngstr + ' ' + cursor.header.hot_spot_x + ' ' + cursor.header.hot_spot_y); + if (typeof SpiceSimulateCursor.cursors != 'undefined') + if (typeof SpiceSimulateCursor.cursors[cursor_sha] != 'undefined') + { + var curstr = 'url(' + SpiceSimulateCursor.cursors[cursor_sha] + '), default'; + screen.style.cursor = curstr; + } + + if (window.getComputedStyle(screen, null).cursor == 'auto') + { + SpiceSimulateCursor.unknown_cursor(cursor_sha, + SpiceSimulateCursor.create_icondir(cursor.header.width, cursor.header.height, + cursor.data.byteLength, cursor.header.hot_spot_x, cursor.header.hot_spot_y) + pngstr); + + document.getElementById(spicecursor.parent.screen_id).style.cursor = 'none'; + if (! spicecursor.spice_simulated_cursor) + { + spicecursor.spice_simulated_cursor = document.createElement('img'); + + spicecursor.spice_simulated_cursor.style.position = 'absolute'; + spicecursor.spice_simulated_cursor.style.display = 'none'; + spicecursor.spice_simulated_cursor.style.overflow = 'hidden'; + + spicecursor.spice_simulated_cursor.spice_screen = document.getElementById(spicecursor.parent.screen_id); + + spicecursor.spice_simulated_cursor.addEventListener('mousemove', SpiceSimulateCursor.handle_sim_mousemove); + + spicecursor.spice_simulated_cursor.spice_screen.appendChild(spicecursor.spice_simulated_cursor); + } + + spicecursor.spice_simulated_cursor.src = 'data:image/png,' + pngstr; + + spicecursor.spice_simulated_cursor.spice_hot_x = cursor.header.hot_spot_x; + spicecursor.spice_simulated_cursor.spice_hot_y = cursor.header.hot_spot_y; + + spicecursor.spice_simulated_cursor.style.pointerEvents = 'none'; + } + else + { + if (spicecursor.spice_simulated_cursor) + { + spicecursor.spice_simulated_cursor.spice_screen.removeChild(spicecursor.spice_simulated_cursor); + delete spicecursor.spice_simulated_cursor; + } + } + }, + + handle_sim_mousemove: function(e) + { + var retval; + var f = SpiceSimulateCursor.duplicate_mouse_event(e, this.spice_screen); + return this.spice_screen.dispatchEvent(f); + }, + + duplicate_mouse_event: function(e, target) + { + var evt = document.createEvent('mouseevent'); + evt.initMouseEvent(e.type, true, true, e.view, e.detail, + e.screenX, e.screenY, e.clientX, e.clientY, + e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget); + return evt; + }, + + ICONDIR: function () + { + }, + + ICONDIRENTRY: function(width, height, bytes, hot_x, hot_y) + { + this.width = width; + this.height = height; + this.bytes = bytes; + this.hot_x = hot_x; + this.hot_y = hot_y; + }, + + + create_icondir: function (width, height, bytes, hot_x, hot_y) + { + var i; + var header = new SpiceSimulateCursor.ICONDIR(); + var entry = new SpiceSimulateCursor.ICONDIRENTRY(width, height, bytes, hot_x, hot_y); + + var mb = new ArrayBuffer(header.buffer_size() + entry.buffer_size()); + var at = header.to_buffer(mb); + at = entry.to_buffer(mb, at); + + var u8 = new Uint8Array(mb); + var str = ''; + for (i = 0; i < at; i++) + { + str += '%'; + if (u8[i] < 16) + str += '0'; + str += u8[i].toString(16); + } + return str; + } + +}; + +SpiceSimulateCursor.ICONDIR.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint16(at, 0, true); at += 2; + dv.setUint16(at, 2, true); at += 2; + dv.setUint16(at, 1, true); at += 2; + return at; + }, + buffer_size: function() + { + return 6; + } +}; + +SpiceSimulateCursor.ICONDIRENTRY.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint8(at, this.width); at++; + dv.setUint8(at, this.height); at++; + dv.setUint8(at, 0); at++; /* color palette count, unused */ + dv.setUint8(at, 0); at++; /* reserved */ + dv.setUint16(at, this.hot_x, true); at += 2; + dv.setUint16(at, this.hot_y, true); at += 2; + dv.setUint32(at, this.bytes, true); at += 4; + dv.setUint32(at, at + 4, true); at += 4; /* Offset to bytes */ + return at; + }, + buffer_size: function() + { + return 16; + } +}; + +export { SpiceSimulateCursor }; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/spicearraybuffer.js b/packages/itmat-ui-react/src/components/lxd/spice/src/spicearraybuffer.js new file mode 100644 index 000000000..c06340fdb --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/spicearraybuffer.js @@ -0,0 +1,59 @@ +/* eslint-disable no-extend-native */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** SpiceArrayBufferSlice +** This function is a work around for IE 10, which has no slice() +** method in it's subclass. +**--------------------------------------------------------------------------*/ +function SpiceArrayBufferSlice(start, end) +{ + start = start || 0; + end = end || this.byteLength; + if (end < 0) + end = this.byteLength + end; + if (start < 0) + start = this.byteLength + start; + if (start < 0) + start = 0; + if (end < 0) + end = 0; + if (end > this.byteLength) + end = this.byteLength; + if (start > end) + start = end; + + var ret = new ArrayBuffer(end - start); + var in1 = new Uint8Array(this, start, end - start); + var out = new Uint8Array(ret); + var i; + + for (i = 0; i < end - start; i++) + out[i] = in1[i]; + + return ret; +} + +if (! ArrayBuffer.prototype.slice) +{ + ArrayBuffer.prototype.slice = SpiceArrayBufferSlice; + console.log('WARNING: ArrayBuffer.slice() is missing; we are extending ArrayBuffer to compensate'); +} diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/spiceconn.js b/packages/itmat-ui-react/src/components/lxd/spice/src/spiceconn.js new file mode 100644 index 000000000..ed655f382 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/spiceconn.js @@ -0,0 +1,476 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-debugger */ +/* eslint-disable no-undef */ +/* eslint-disable no-redeclare */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** SpiceConn +** This is the base Javascript class for establishing and +** managing a connection to a Spice Server. +** It is used to provide core functionality to the Spice main, +** display, inputs, and cursor channels. See main.js for +** usage. +**--------------------------------------------------------------------------*/ + +import { Constants } from './enums.js'; +import { SpiceWireReader } from './wire.js'; +import { + SpiceLinkHeader, + SpiceLinkMess, + SpiceLinkReply, + SpiceLinkAuthTicket, + SpiceLinkAuthReply, + SpiceMiniData, + SpiceMsgcDisplayInit, + SpiceMsgSetAck, + SpiceMsgcAckSync, + SpiceMsgNotify +} from './spicemsg.js'; +import { DEBUG } from './utils.js'; +import * as Webm from './webm.js'; +import { rsa_encrypt } from './ticket.js'; + +function SpiceConn(o) { + if (o === undefined || o.uri === undefined || !o.uri) + throw new Error('You must specify a uri'); + + this.ws = new WebSocket(o.uri); + + if (!this.ws.binaryType) + throw new Error('WebSocket doesn\'t support binaryType. Try a different browser.'); + + this.connection_id = o.connection_id !== undefined ? o.connection_id : 0; + this.type = o.type !== undefined ? o.type : Constants.SPICE_CHANNEL_MAIN; + this.chan_id = o.chan_id !== undefined ? o.chan_id : 0; + if (o.parent !== undefined) { + this.parent = o.parent; + this.message_id = o.parent.message_id; + this.password = o.parent.password; + } + if (o.screen_id !== undefined) + this.screen_id = o.screen_id; + if (o.dump_id !== undefined) + this.dump_id = o.dump_id; + if (o.message_id !== undefined) + this.message_id = o.message_id; + if (o.password !== undefined) + this.password = o.password; + if (o.onerror !== undefined) + this.onerror = o.onerror; + if (o.onsuccess !== undefined) + this.onsuccess = o.onsuccess; + if (o.onagent !== undefined) + this.onagent = o.onagent; + + this.state = 'connecting'; + this.ws.parent = this; + this.wire_reader = new SpiceWireReader(this, this.process_inbound); + this.messages_sent = 0; + this.warnings = []; + + this.ws.addEventListener('open', function (e) { + DEBUG > 0 && console.log('>> WebSockets.onopen'); + DEBUG > 0 && console.log('id ' + this.parent.connection_id + '; type ' + this.parent.type); + + /*********************************************************************** + ** WHERE IT ALL REALLY BEGINS + ***********************************************************************/ + this.parent.send_hdr(); + this.parent.wire_reader.request(SpiceLinkHeader.prototype.buffer_size()); + this.parent.state = 'start'; + }); + this.ws.addEventListener('error', function (e) { + if ('url' in e.target) { + this.parent.log_err('WebSocket error: Can\'t connect to websocket on URL: ' + e.target.url); + } + this.parent.report_error(e); + }); + this.ws.addEventListener('close', function (e) { + DEBUG > 0 && console.log('>> WebSockets.onclose'); + DEBUG > 0 && console.log('id ' + this.parent.connection_id + '; type ' + this.parent.type); + DEBUG > 0 && console.log(e); + if (this.parent.state != 'closing' && this.parent.state != 'error' && this.parent.onerror !== undefined) { + var e; + if (this.parent.state == 'connecting') + e = new Error('Connection refused.'); + else if (this.parent.state == 'start' || this.parent.state == 'link') + e = new Error('Unexpected protocol mismatch.'); + else if (this.parent.state == 'ticket') + e = new Error('Bad password.'); + else + e = new Error('Unexpected close while ' + this.parent.state); + + this.parent.onerror(e); + this.parent.log_err(e.toString()); + } + }); + + if (this.ws.readyState == 2 || this.ws.readyState == 3) + throw new Error('Unable to connect to ' + o.uri); + + this.timeout = window.setTimeout(spiceconn_timeout, Constants.SPICE_CONNECT_TIMEOUT, this); +} + +SpiceConn.prototype = +{ + send_hdr: function () { + var hdr = new SpiceLinkHeader(); + var msg = new SpiceLinkMess(); + + msg.connection_id = this.connection_id; + msg.channel_type = this.type; + msg.channel_id = this.chan_id; + + msg.common_caps.push( + (1 << Constants.SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION) | + (1 << Constants.SPICE_COMMON_CAP_MINI_HEADER) + ); + + if (msg.channel_type == Constants.SPICE_CHANNEL_PLAYBACK) { + var caps = 0; + if ('MediaSource' in window && MediaSource.isTypeSupported(Webm.Constants.SPICE_PLAYBACK_CODEC)) + caps |= (1 << Constants.SPICE_PLAYBACK_CAP_OPUS); + msg.channel_caps.push(caps); + } + else if (msg.channel_type == Constants.SPICE_CHANNEL_MAIN) { + msg.channel_caps.push( + (1 << Constants.SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS) + ); + } + else if (msg.channel_type == Constants.SPICE_CHANNEL_DISPLAY) { + var caps = (1 << Constants.SPICE_DISPLAY_CAP_SIZED_STREAM) | + (1 << Constants.SPICE_DISPLAY_CAP_STREAM_REPORT) | + (1 << Constants.SPICE_DISPLAY_CAP_MULTI_CODEC) | + (1 << Constants.SPICE_DISPLAY_CAP_CODEC_MJPEG); + if ('MediaSource' in window && MediaSource.isTypeSupported(Webm.Constants.SPICE_VP8_CODEC)) + caps |= (1 << Constants.SPICE_DISPLAY_CAP_CODEC_VP8); + msg.channel_caps.push(caps); + } + + hdr.size = msg.buffer_size(); + + var mb = new ArrayBuffer(hdr.buffer_size() + msg.buffer_size()); + hdr.to_buffer(mb); + msg.to_buffer(mb, hdr.buffer_size()); + + DEBUG > 1 && console.log('Sending header:'); + DEBUG > 2 && hexdump_buffer(mb); + this.ws.send(mb); + }, + + send_ticket: function (ticket) { + var hdr = new SpiceLinkAuthTicket(); + hdr.auth_mechanism = Constants.SPICE_COMMON_CAP_AUTH_SPICE; + // FIXME - we need to implement RSA to make this work right + hdr.encrypted_data = ticket; + var mb = new ArrayBuffer(hdr.buffer_size()); + + hdr.to_buffer(mb); + DEBUG > 1 && console.log('Sending ticket:'); + DEBUG > 2 && hexdump_buffer(mb); + this.ws.send(mb); + }, + + send_msg: function (msg) { + var mb = new ArrayBuffer(msg.buffer_size()); + msg.to_buffer(mb); + this.messages_sent++; + DEBUG > 0 && console.log('>> hdr ' + this.channel_type() + ' type ' + msg.type + ' size ' + mb.byteLength); + DEBUG > 2 && hexdump_buffer(mb); + this.ws.send(mb); + }, + + process_inbound: function (mb, saved_header) { + DEBUG > 2 && console.log(this.type + ': processing message of size ' + mb.byteLength + '; state is ' + this.state); + if (this.state == 'ready') { + if (saved_header == undefined) { + var msg = new SpiceMiniData(mb); + + if (msg.type > 500) { + if (DEBUG > 0) { + alert('Something has gone very wrong; we think we have message of type ' + msg.type); + debugger; + } + } + + if (msg.size == 0) { + this.process_message(msg); + this.wire_reader.request(SpiceMiniData.prototype.buffer_size()); + } + else { + this.wire_reader.request(msg.size); + this.wire_reader.save_header(msg); + } + } + else { + saved_header.data = mb; + this.process_message(saved_header); + this.wire_reader.request(SpiceMiniData.prototype.buffer_size()); + this.wire_reader.save_header(undefined); + } + } + + else if (this.state == 'start') { + this.reply_hdr = new SpiceLinkHeader(mb); + if (this.reply_hdr.magic != Constants.SPICE_MAGIC) { + this.state = 'error'; + var e = new Error('Error: magic mismatch: ' + this.reply_hdr.magic); + console.log('this.reply_hdr', this.reply_hdr); + this.report_error(e); + } + else { + // FIXME - Determine major/minor version requirements + this.wire_reader.request(this.reply_hdr.size); + this.state = 'link'; + } + } + + else if (this.state == 'link') { + this.reply_link = new SpiceLinkReply(mb); + // FIXME - Screen the caps - require minihdr at least, right? + if (this.reply_link.error) { + this.state = 'error'; + var e = new Error('Error: reply link error ' + this.reply_link.error); + this.report_error(e); + } + else { + this.send_ticket(rsa_encrypt(this.reply_link.pub_key, this.password + String.fromCharCode(0))); + this.state = 'ticket'; + this.wire_reader.request(SpiceLinkAuthReply.prototype.buffer_size()); + } + } + + else if (this.state == 'ticket') { + this.auth_reply = new SpiceLinkAuthReply(mb); + if (this.auth_reply.auth_code == Constants.SPICE_LINK_ERR_OK) { + DEBUG > 0 && console.log(this.type + ': Connected'); + + if (this.type == Constants.SPICE_CHANNEL_DISPLAY) { + // FIXME - pixmap and glz dictionary config info? + var dinit = new SpiceMsgcDisplayInit(); + var reply = new SpiceMiniData(); + reply.build_msg(Constants.SPICE_MSGC_DISPLAY_INIT, dinit); + DEBUG > 0 && console.log('Request display init'); + this.send_msg(reply); + } + this.state = 'ready'; + this.wire_reader.request(SpiceMiniData.prototype.buffer_size()); + if (this.timeout) { + window.clearTimeout(this.timeout); + delete this.timeout; + } + } + else { + this.state = 'error'; + if (this.auth_reply.auth_code == Constants.SPICE_LINK_ERR_PERMISSION_DENIED) { + var e = new Error('Permission denied.'); + } + else { + var e = new Error('Unexpected link error ' + this.auth_reply.auth_code); + } + this.report_error(e); + } + } + }, + + process_common_messages: function (msg) { + if (msg.type == Constants.SPICE_MSG_SET_ACK) { + var ack = new SpiceMsgSetAck(msg.data); + // FIXME - what to do with generation? + this.ack_window = ack.window; + DEBUG > 1 && console.log(this.type + ': set ack to ' + ack.window); + this.msgs_until_ack = this.ack_window; + var ackack = new SpiceMsgcAckSync(ack); + var reply = new SpiceMiniData(); + reply.build_msg(Constants.SPICE_MSGC_ACK_SYNC, ackack); + this.send_msg(reply); + return true; + } + + if (msg.type == Constants.SPICE_MSG_PING) { + DEBUG > 1 && console.log('ping!'); + var pong = new SpiceMiniData(); + pong.type = Constants.SPICE_MSGC_PONG; + if (msg.data) { + pong.data = msg.data.slice(0, 12); + } + pong.size = pong.buffer_size(); + this.send_msg(pong); + return true; + } + + if (msg.type == Constants.SPICE_MSG_NOTIFY) { + // FIXME - Visibility + what + var notify = new SpiceMsgNotify(msg.data); + if (notify.severity == Constants.SPICE_NOTIFY_SEVERITY_ERROR) + this.log_err(notify.message); + else if (notify.severity == Constants.SPICE_NOTIFY_SEVERITY_WARN) + this.log_warn(notify.message); + else + this.log_info(notify.message); + return true; + } + + return false; + + }, + + process_message: function (msg) { + var rc; + var start = Date.now(); + DEBUG > 0 && console.log('<< hdr ' + this.channel_type() + ' type ' + msg.type + ' size ' + (msg.data && msg.data.byteLength)); + rc = this.process_common_messages(msg); + if (!rc) { + if (this.process_channel_message) { + rc = this.process_channel_message(msg); + if (!rc) + this.log_warn(this.channel_type() + ': Unknown message type ' + msg.type + '!'); + } + else + this.log_err(this.channel_type() + ': No message handlers for this channel; message ' + msg.type); + } + + if (this.msgs_until_ack !== undefined && this.ack_window) { + this.msgs_until_ack--; + if (this.msgs_until_ack <= 0) { + this.msgs_until_ack = this.ack_window; + var ack = new SpiceMiniData(); + ack.type = Constants.SPICE_MSGC_ACK; + this.send_msg(ack); + DEBUG > 1 && console.log(this.type + ': sent ack'); + } + } + + var delta = Date.now() - start; + if (DEBUG > 0 || delta > Webm.Constants.GAP_DETECTION_THRESHOLD) + console.log('delta ' + this.channel_type() + ':' + msg.type + ' ' + delta); + return rc; + }, + + channel_type: function () { + if (this.type == Constants.SPICE_CHANNEL_MAIN) + return 'main'; + else if (this.type == Constants.SPICE_CHANNEL_DISPLAY) + return 'display'; + else if (this.type == Constants.SPICE_CHANNEL_INPUTS) + return 'inputs'; + else if (this.type == Constants.SPICE_CHANNEL_CURSOR) + return 'cursor'; + else if (this.type == Constants.SPICE_CHANNEL_PLAYBACK) + return 'playback'; + else if (this.type == Constants.SPICE_CHANNEL_RECORD) + return 'record'; + else if (this.type == Constants.SPICE_CHANNEL_TUNNEL) + return 'tunnel'; + else if (this.type == Constants.SPICE_CHANNEL_SMARTCARD) + return 'smartcard'; + else if (this.type == Constants.SPICE_CHANNEL_USBREDIR) + return 'Fusbredir'; + else if (this.type == Constants.SPICE_CHANNEL_PORT) + return 'port'; + else if (this.type == Constants.SPICE_CHANNEL_WEBDAV) + return 'webdav'; + return 'unknown-' + this.type; + + }, + + log_info: function () { + var msg = Array.prototype.join.call(arguments, ' '); + console.log(msg); + if (this.message_id) { + var p = document.createElement('p'); + p.appendChild(document.createTextNode(msg)); + p.className += 'spice-message-info'; + document.getElementById(this.message_id).appendChild(p); + } + }, + + log_warn: function () { + var msg = Array.prototype.join.call(arguments, ' '); + console.log('WARNING: ' + msg); + if (this.message_id) { + var p = document.createElement('p'); + p.appendChild(document.createTextNode(msg)); + p.className += 'spice-message-warning'; + document.getElementById(this.message_id).appendChild(p); + } + }, + + log_err: function () { + var msg = Array.prototype.join.call(arguments, ' '); + console.log('ERROR: ' + msg); + if (this.message_id) { + var p = document.createElement('p'); + p.appendChild(document.createTextNode(msg)); + p.className += 'spice-message-error'; + document.getElementById(this.message_id).appendChild(p); + } + }, + + known_unimplemented: function (type, msg) { + if ((!this.warnings[type]) || DEBUG > 1) { + var str = ''; + if (DEBUG <= 1) + str = ' [ further notices suppressed ]'; + this.log_warn('Unimplemented function ' + type + '(' + msg + ')' + str); + this.warnings[type] = true; + } + }, + + report_error: function (e) { + this.log_err(e.toString()); + if (this.onerror != undefined) + this.onerror(e); + else + throw (e); + }, + + report_success: function (m) { + if (this.onsuccess != undefined) + this.onsuccess(m); + }, + + cleanup: function () { + if (this.timeout) { + window.clearTimeout(this.timeout); + delete this.timeout; + } + if (this.ws) { + this.ws.close(); + this.ws = undefined; + } + }, + + handle_timeout: function () { + var e = new Error('Connection timed out.'); + this.report_error(e); + } +}; + +function spiceconn_timeout(sc) { + SpiceConn.prototype.handle_timeout.call(sc); +} + +export { + SpiceConn +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/spicedataview.js b/packages/itmat-ui-react/src/components/lxd/spice/src/spicedataview.js new file mode 100644 index 000000000..9610c9cd2 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/spicedataview.js @@ -0,0 +1,126 @@ +/* eslint-disable no-loss-of-precision */ +/* eslint-disable @typescript-eslint/no-loss-of-precision */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** SpiceDataView +** FIXME FIXME +** This is used because Firefox does not have DataView yet. +** We should use DataView if we have it, because it *has* to +** be faster than this code +**--------------------------------------------------------------------------*/ +function SpiceDataView(buffer, byteOffset, byteLength) +{ + if (byteOffset !== undefined) + { + if (byteLength !== undefined) + this.u8 = new Uint8Array(buffer, byteOffset, byteLength); + else + this.u8 = new Uint8Array(buffer, byteOffset); + } + else + this.u8 = new Uint8Array(buffer); +} + +SpiceDataView.prototype = { + getUint8: function(byteOffset) + { + return this.u8[byteOffset]; + }, + getUint16: function(byteOffset, littleEndian) + { + var low = 1, high = 0; + if (littleEndian) + { + low = 0; + high = 1; + } + + return (this.u8[byteOffset + high] << 8) | this.u8[byteOffset + low]; + }, + getUint32: function(byteOffset, littleEndian) + { + var low = 2, high = 0; + if (littleEndian) + { + low = 0; + high = 2; + } + + return (this.getUint16(byteOffset + high, littleEndian) << 16) | + this.getUint16(byteOffset + low, littleEndian); + }, + getUint64: function (byteOffset, littleEndian) + { + var low = 4, high = 0; + if (littleEndian) + { + low = 0; + high = 4; + } + + return (this.getUint32(byteOffset + high, littleEndian) << 32) | + this.getUint32(byteOffset + low, littleEndian); + }, + setUint8: function(byteOffset, b) + { + this.u8[byteOffset] = (b & 0xff); + }, + setUint16: function(byteOffset, i, littleEndian) + { + var low = 1, high = 0; + if (littleEndian) + { + low = 0; + high = 1; + } + this.u8[byteOffset + high] = (i & 0xffff) >> 8; + this.u8[byteOffset + low] = (i & 0x00ff); + }, + setUint32: function(byteOffset, w, littleEndian) + { + var low = 2, high = 0; + if (littleEndian) + { + low = 0; + high = 2; + } + + this.setUint16(byteOffset + high, (w & 0xffffffff) >> 16, littleEndian); + this.setUint16(byteOffset + low, (w & 0x0000ffff), littleEndian); + }, + setUint64: function(byteOffset, w, littleEndian) + { + var low = 4, high = 0; + if (littleEndian) + { + low = 0; + high = 4; + } + + this.setUint32(byteOffset + high, (w & 0xffffffffffffffff) >> 32, littleEndian); + this.setUint32(byteOffset + low, (w & 0x00000000ffffffff), littleEndian); + } +}; + +export { + SpiceDataView +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/spicemsg.js b/packages/itmat-ui-react/src/components/lxd/spice/src/spicemsg.js new file mode 100644 index 000000000..6dd24073f --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/spicemsg.js @@ -0,0 +1,1218 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-useless-concat */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-empty-function */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** Spice messages +** This file contains classes for passing messages to and from +** a spice server. This file should arguably be generated from +** spice.proto, but it was instead put together by hand. +**--------------------------------------------------------------------------*/ + +import { Constants } from './enums.js'; +import { SpiceDataView } from './spicedataview.js'; +import { create_rsa_from_mb } from './ticket.js'; +import { + SpiceChannelId, + SpiceRect, + SpiceClip, + SpiceCopy, + SpiceFill, + SpicePoint, + SpiceSurface, + SpicePoint16, + SpiceCursor +} from './spicetype.js'; +import { + keycode_to_start_scan, + keycode_to_end_scan +} from './utils.js'; + +function SpiceLinkHeader(a, at) { + this.magic = Constants.SPICE_MAGIC; + this.major_version = Constants.SPICE_VERSION_MAJOR; + this.minor_version = Constants.SPICE_VERSION_MINOR; + this.size = 0; + if (a !== undefined) + this.from_buffer(a, at); +} + +SpiceLinkHeader.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.magic = ''; + for (var i = 0; i < 4; i++) + this.magic += String.fromCharCode(dv.getUint8(at + i)); + at += 4; + + this.major_version = dv.getUint32(at, true); at += 4; + this.minor_version = dv.getUint32(at, true); at += 4; + this.size = dv.getUint32(at, true); at += 4; + }, + + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + for (var i = 0; i < 4; i++) + dv.setUint8(at + i, this.magic.charCodeAt(i)); + at += 4; + + dv.setUint32(at, this.major_version, true); at += 4; + dv.setUint32(at, this.minor_version, true); at += 4; + dv.setUint32(at, this.size, true); at += 4; + }, + buffer_size: function () { + return 16; + } +}; + +function SpiceLinkMess(a, at) { + this.connection_id = 0; + this.channel_type = 0; + this.channel_id = 0; + this.common_caps = []; + this.channel_caps = []; + + if (a !== undefined) + this.from_buffer(a, at); +} + +SpiceLinkMess.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var i; + var orig_at = at; + var dv = new SpiceDataView(a); + this.connection_id = dv.getUint32(at, true); at += 4; + this.channel_type = dv.getUint8(at, true); at++; + this.channel_id = dv.getUint8(at, true); at++; + var num_common_caps = dv.getUint32(at, true); at += 4; + var num_channel_caps = dv.getUint32(at, true); at += 4; + var caps_offset = dv.getUint32(at, true); at += 4; + + at = orig_at + caps_offset; + this.common_caps = []; + for (i = 0; i < num_common_caps; i++) { + this.common_caps.unshift(dv.getUint32(at, true)); at += 4; + } + + this.channel_caps = []; + for (i = 0; i < num_channel_caps; i++) { + this.channel_caps.unshift(dv.getUint32(at, true)); at += 4; + } + }, + + to_buffer: function (a, at) { + at = at || 0; + var orig_at = at; + var i; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.connection_id, true); at += 4; + dv.setUint8(at, this.channel_type, true); at++; + dv.setUint8(at, this.channel_id, true); at++; + dv.setUint32(at, this.common_caps.length, true); at += 4; + dv.setUint32(at, this.channel_caps.length, true); at += 4; + dv.setUint32(at, (at - orig_at) + 4, true); at += 4; + + for (i = 0; i < this.common_caps.length; i++) { + dv.setUint32(at, this.common_caps[i], true); at += 4; + } + + for (i = 0; i < this.channel_caps.length; i++) { + dv.setUint32(at, this.channel_caps[i], true); at += 4; + } + }, + buffer_size: function () { + return 18 + (4 * this.common_caps.length) + (4 * this.channel_caps.length); + } +}; + +function SpiceLinkReply(a, at) { + this.error = 0; + this.pub_key = undefined; + this.common_caps = []; + this.channel_caps = []; + + if (a !== undefined) + this.from_buffer(a, at); +} + +SpiceLinkReply.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var i; + var orig_at = at; + var dv = new SpiceDataView(a); + this.error = dv.getUint32(at, true); at += 4; + + this.pub_key = create_rsa_from_mb(a, at); + at += Constants.SPICE_TICKET_PUBKEY_BYTES; + + var num_common_caps = dv.getUint32(at, true); at += 4; + var num_channel_caps = dv.getUint32(at, true); at += 4; + var caps_offset = dv.getUint32(at, true); at += 4; + + at = orig_at + caps_offset; + this.common_caps = []; + for (i = 0; i < num_common_caps; i++) { + this.common_caps.unshift(dv.getUint32(at, true)); at += 4; + } + + this.channel_caps = []; + for (i = 0; i < num_channel_caps; i++) { + this.channel_caps.unshift(dv.getUint32(at, true)); at += 4; + } + } +}; + +function SpiceLinkAuthTicket(a, at) { + this.auth_mechanism = 0; + this.encrypted_data = undefined; +} + +SpiceLinkAuthTicket.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var i; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.auth_mechanism, true); at += 4; + for (i = 0; i < Constants.SPICE_TICKET_KEY_PAIR_LENGTH / 8; i++) { + if (this.encrypted_data && i < this.encrypted_data.length) + dv.setUint8(at, this.encrypted_data[i], true); + else + dv.setUint8(at, 0, true); + at++; + } + }, + buffer_size: function () { + return 4 + (Constants.SPICE_TICKET_KEY_PAIR_LENGTH / 8); + } +}; + +function SpiceLinkAuthReply(a, at) { + this.auth_code = 0; + if (a !== undefined) + this.from_buffer(a, at); +} + +SpiceLinkAuthReply.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.auth_code = dv.getUint32(at, true); at += 4; + }, + buffer_size: function () { + return 4; + } +}; + +function SpiceMiniData(a, at) { + this.type = 0; + this.size = 0; + this.data = undefined; + if (a !== undefined) + this.from_buffer(a, at); +} + +SpiceMiniData.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var i; + var dv = new SpiceDataView(a); + this.type = dv.getUint16(at, true); at += 2; + this.size = dv.getUint32(at, true); at += 4; + if (a.byteLength > at) { + this.data = a.slice(at); + at += this.data.byteLength; + } + }, + to_buffer: function (a, at) { + at = at || 0; + var i; + var dv = new SpiceDataView(a); + dv.setUint16(at, this.type, true); at += 2; + dv.setUint32(at, this.data ? this.data.byteLength : 0, true); at += 4; + if (this.data && this.data.byteLength > 0) { + var u8arr = new Uint8Array(this.data); + for (i = 0; i < u8arr.length; i++, at++) + dv.setUint8(at, u8arr[i], true); + } + }, + build_msg: function (in_type, extra) { + this.type = in_type; + this.size = extra.buffer_size(); + this.data = new ArrayBuffer(this.size); + extra.to_buffer(this.data); + }, + buffer_size: function () { + if (this.data) + return 6 + this.data.byteLength; + else + return 6; + } +}; + +function SpiceMsgChannels(a, at) { + this.num_of_channels = 0; + this.channels = []; + if (a !== undefined) + this.from_buffer(a, at); +} + +SpiceMsgChannels.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var i; + var dv = new SpiceDataView(a); + this.num_of_channels = dv.getUint32(at, true); at += 4; + for (i = 0; i < this.num_of_channels; i++) { + var chan = new SpiceChannelId(); + at = chan.from_dv(dv, at, a); + this.channels.push(chan); + } + } +}; + +function SpiceMsgMainInit(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgMainInit.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.session_id = dv.getUint32(at, true); at += 4; + this.display_channels_hint = dv.getUint32(at, true); at += 4; + this.supported_mouse_modes = dv.getUint32(at, true); at += 4; + this.current_mouse_mode = dv.getUint32(at, true); at += 4; + this.agent_connected = dv.getUint32(at, true); at += 4; + this.agent_tokens = dv.getUint32(at, true); at += 4; + this.multi_media_time = dv.getUint32(at, true); at += 4; + this.ram_hint = dv.getUint32(at, true); at += 4; + } +}; + +function SpiceMsgMainMouseMode(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgMainMouseMode.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.supported_modes = dv.getUint16(at, true); at += 2; + this.current_mode = dv.getUint16(at, true); at += 2; + } +}; + +function SpiceMsgMainAgentData(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgMainAgentData.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.protocol = dv.getUint32(at, true); at += 4; + this.type = dv.getUint32(at, true); at += 4; + this.opaque = dv.getUint64(at, true); at += 8; + this.size = dv.getUint32(at, true); at += 4; + if (a.byteLength > at) { + this.data = a.slice(at); + at += this.data.byteLength; + } + } +}; + +function SpiceMsgMainAgentTokens(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgMainAgentTokens.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.num_tokens = dv.getUint32(at, true); at += 4; + } +}; + +function SpiceMsgSetAck(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgSetAck.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.generation = dv.getUint32(at, true); at += 4; + this.window = dv.getUint32(at, true); at += 4; + } +}; + +function SpiceMsgcAckSync(ack) { + this.generation = ack.generation; +} + +SpiceMsgcAckSync.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.generation, true); at += 4; + }, + buffer_size: function () { + return 4; + } +}; + +function SpiceMsgcMainMouseModeRequest(mode) { + this.mode = mode; +} + +SpiceMsgcMainMouseModeRequest.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint16(at, this.mode, true); at += 2; + }, + buffer_size: function () { + return 2; + } +}; + +function SpiceMsgcMainAgentStart(num_tokens) { + this.num_tokens = num_tokens; +} + +SpiceMsgcMainAgentStart.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.num_tokens, true); at += 4; + }, + buffer_size: function () { + return 4; + } +}; + +function SpiceMsgcMainAgentData(type, data) { + this.protocol = Constants.VD_AGENT_PROTOCOL; + this.type = type; + this.opaque = 0; + this.size = data.buffer_size(); + this.data = data; +} + +SpiceMsgcMainAgentData.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.protocol, true); at += 4; + dv.setUint32(at, this.type, true); at += 4; + dv.setUint64(at, this.opaque, true); at += 8; + dv.setUint32(at, this.size, true); at += 4; + this.data.to_buffer(a, at); + }, + buffer_size: function () { + return 4 + 4 + 8 + 4 + this.data.buffer_size(); + } +}; + +function VDAgentAnnounceCapabilities(request, caps) { + if (caps) { + this.request = request; + this.caps = caps; + } + else + this.from_buffer(request); +} + +VDAgentAnnounceCapabilities.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.request, true); at += 4; + dv.setUint32(at, this.caps, true); at += 4; + }, + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.request = dv.getUint32(at, true); at += 4; + this.caps = dv.getUint32(at, true); at += 4; + return at; + }, + buffer_size: function () { + return 8; + } +}; + +function VDAgentMonitorsConfig(flags, width, height, depth, x, y) { + this.num_mon = 1; + this.flags = flags; + this.width = width; + this.height = height; + this.depth = depth; + this.x = x; + this.y = y; +} + +VDAgentMonitorsConfig.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.num_mon, true); at += 4; + dv.setUint32(at, this.flags, true); at += 4; + dv.setUint32(at, this.height, true); at += 4; + dv.setUint32(at, this.width, true); at += 4; + dv.setUint32(at, this.depth, true); at += 4; + dv.setUint32(at, this.x, true); at += 4; + dv.setUint32(at, this.y, true); at += 4; + }, + buffer_size: function () { + return 28; + } +}; + +function VDAgentFileXferStatusMessage(data, result) { + if (result) { + this.id = data; + this.result = result; + } + else + this.from_buffer(data); +} + +VDAgentFileXferStatusMessage.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.id, true); at += 4; + dv.setUint32(at, this.result, true); at += 4; + }, + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.id = dv.getUint32(at, true); at += 4; + this.result = dv.getUint32(at, true); at += 4; + return at; + }, + buffer_size: function () { + return 8; + } +}; + +function VDAgentFileXferStartMessage(id, name, size) { + this.id = id; + this.string = '[vdagent-file-xfer]\n' + 'name=' + name + '\nsize=' + size + '\n'; +} + +VDAgentFileXferStartMessage.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.id, true); at += 4; + for (var i = 0; i < this.string.length; i++, at++) + dv.setUint8(at, this.string.charCodeAt(i)); + }, + buffer_size: function () { + return 4 + this.string.length + 1; + } +}; + +function VDAgentFileXferDataMessage(id, size, data) { + this.id = id; + this.size = size; + this.data = data; +} + +VDAgentFileXferDataMessage.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.id, true); at += 4; + dv.setUint64(at, this.size, true); at += 8; + if (this.data && this.data.byteLength > 0) { + var u8arr = new Uint8Array(this.data); + for (var i = 0; i < u8arr.length; i++, at++) + dv.setUint8(at, u8arr[i]); + } + }, + buffer_size: function () { + return 12 + this.size; + } +}; + +function SpiceMsgNotify(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgNotify.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var i; + var dv = new SpiceDataView(a); + this.time_stamp = dv.getUint64(at, true); at += 8; + this.severity = dv.getUint32(at, true); at += 4; + this.visibility = dv.getUint32(at, true); at += 4; + this.what = dv.getUint32(at, true); at += 4; + this.message_len = dv.getUint32(at, true); at += 4; + this.message = ''; + for (i = 0; i < this.message_len; i++) { + var c = dv.getUint8(at, true); at++; + this.message += String.fromCharCode(c); + } + } +}; + +function SpiceMsgcDisplayInit() { + this.pixmap_cache_id = 1; + this.glz_dictionary_id = 0; + this.pixmap_cache_size = 10 * 1024 * 1024; + this.glz_dictionary_window_size = 0; +} + +SpiceMsgcDisplayInit.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint8(at, this.pixmap_cache_id, true); at++; + dv.setUint64(at, this.pixmap_cache_size, true); at += 8; + dv.setUint8(at, this.glz_dictionary_id, true); at++; + dv.setUint32(at, this.glz_dictionary_window_size, true); at += 4; + }, + buffer_size: function () { + return 14; + } +}; + +function SpiceMsgDisplayBase() { +} + +SpiceMsgDisplayBase.prototype = +{ + from_dv: function (dv, at, mb) { + this.surface_id = dv.getUint32(at, true); at += 4; + this.box = new SpiceRect(); + at = this.box.from_dv(dv, at, mb); + this.clip = new SpiceClip(); + return this.clip.from_dv(dv, at, mb); + } +}; + +function SpiceMsgDisplayDrawCopy(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayDrawCopy.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.base = new SpiceMsgDisplayBase(); + at = this.base.from_dv(dv, at, a); + this.data = new SpiceCopy(); + return this.data.from_dv(dv, at, a); + } +}; + +function SpiceMsgDisplayDrawFill(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayDrawFill.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.base = new SpiceMsgDisplayBase(); + at = this.base.from_dv(dv, at, a); + this.data = new SpiceFill(); + return this.data.from_dv(dv, at, a); + } +}; + +function SpiceMsgDisplayCopyBits(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayCopyBits.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.base = new SpiceMsgDisplayBase(); + at = this.base.from_dv(dv, at, a); + this.src_pos = new SpicePoint(); + return this.src_pos.from_dv(dv, at, a); + } +}; + + +function SpiceMsgSurfaceCreate(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgSurfaceCreate.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.surface = new SpiceSurface(); + return this.surface.from_dv(dv, at, a); + } +}; + +function SpiceMsgSurfaceDestroy(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgSurfaceDestroy.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.surface_id = dv.getUint32(at, true); at += 4; + } +}; + +function SpiceMsgInputsInit(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgInputsInit.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.keyboard_modifiers = dv.getUint16(at, true); at += 2; + return at; + } +}; + +function SpiceMsgInputsKeyModifiers(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgInputsKeyModifiers.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.keyboard_modifiers = dv.getUint16(at, true); at += 2; + return at; + } +}; + +function SpiceMsgCursorInit(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgCursorInit.prototype = +{ + from_buffer: function (a, at, mb) { + at = at || 0; + var dv = new SpiceDataView(a); + this.position = new SpicePoint16(); + at = this.position.from_dv(dv, at, mb); + this.trail_length = dv.getUint16(at, true); at += 2; + this.trail_frequency = dv.getUint16(at, true); at += 2; + this.visible = dv.getUint8(at, true); at++; + this.cursor = new SpiceCursor(); + return this.cursor.from_dv(dv, at, a); + } +}; + +function SpiceMsgPlaybackData(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgPlaybackData.prototype = +{ + from_buffer: function (a, at, mb) { + at = at || 0; + var dv = new SpiceDataView(a); + this.time = dv.getUint32(at, true); at += 4; + if (a.byteLength > at) { + this.data = a.slice(at); + at += this.data.byteLength; + } + return at; + } +}; + +function SpiceMsgPlaybackMode(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgPlaybackMode.prototype = +{ + from_buffer: function (a, at, mb) { + at = at || 0; + var dv = new SpiceDataView(a); + this.time = dv.getUint32(at, true); at += 4; + this.mode = dv.getUint16(at, true); at += 2; + if (a.byteLength > at) { + this.data = a.slice(at); + at += this.data.byteLength; + } + return at; + } +}; + +function SpiceMsgPlaybackStart(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgPlaybackStart.prototype = +{ + from_buffer: function (a, at, mb) { + at = at || 0; + var dv = new SpiceDataView(a); + this.channels = dv.getUint32(at, true); at += 4; + this.format = dv.getUint16(at, true); at += 2; + this.frequency = dv.getUint32(at, true); at += 4; + this.time = dv.getUint32(at, true); at += 4; + return at; + } +}; + + + +function SpiceMsgCursorSet(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgCursorSet.prototype = +{ + from_buffer: function (a, at, mb) { + at = at || 0; + var dv = new SpiceDataView(a); + this.position = new SpicePoint16(); + at = this.position.from_dv(dv, at, mb); + this.visible = dv.getUint8(at, true); at++; + this.cursor = new SpiceCursor(); + return this.cursor.from_dv(dv, at, a); + } +}; + + +function SpiceMsgcMousePosition(sc, e) { + // FIXME - figure out how to correctly compute display_id + this.display_id = 0; + this.buttons_state = sc.buttons_state; + if (e) { + this.x = e.offsetX; + this.y = e.offsetY; + + sc.mousex = e.offsetX; + sc.mousey = e.offsetY; + } + else { + this.x = this.y = this.buttons_state = 0; + } +} + +SpiceMsgcMousePosition.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.x, true); at += 4; + dv.setUint32(at, this.y, true); at += 4; + dv.setUint16(at, this.buttons_state, true); at += 2; + dv.setUint8(at, this.display_id, true); at += 1; + return at; + }, + buffer_size: function () { + return 11; + } +}; + +function SpiceMsgcMouseMotion(sc, e) { + // FIXME - figure out how to correctly compute display_id + this.display_id = 0; + this.buttons_state = sc.buttons_state; + if (e) { + this.x = e.offsetX; + this.y = e.offsetY; + + if (sc.mousex !== undefined) { + this.x -= sc.mousex; + this.y -= sc.mousey; + } + sc.mousex = e.offsetX; + sc.mousey = e.offsetY; + } + else { + this.x = this.y = this.buttons_state = 0; + } +} + +/* Use the same functions as for MousePosition */ +SpiceMsgcMouseMotion.prototype.to_buffer = SpiceMsgcMousePosition.prototype.to_buffer; +SpiceMsgcMouseMotion.prototype.buffer_size = SpiceMsgcMousePosition.prototype.buffer_size; + +function SpiceMsgcMousePress(sc, e) { + if (e) { + this.button = e.button + 1; + this.buttons_state = 1 << e.button; + sc.buttons_state = this.buttons_state; + } + else { + this.button = Constants.SPICE_MOUSE_BUTTON_LEFT; + this.buttons_state = Constants.SPICE_MOUSE_BUTTON_MASK_LEFT; + } +} + +SpiceMsgcMousePress.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint8(at, this.button, true); at++; + dv.setUint16(at, this.buttons_state, true); at += 2; + return at; + }, + buffer_size: function () { + return 3; + } +}; + +function SpiceMsgcMouseRelease(sc, e) { + if (e) { + this.button = e.button + 1; + this.buttons_state = 0; + sc.buttons_state = this.buttons_state; + } + else { + this.button = Constants.SPICE_MOUSE_BUTTON_LEFT; + this.buttons_state = 0; + } +} + +/* Use the same functions as for MousePress */ +SpiceMsgcMouseRelease.prototype.to_buffer = SpiceMsgcMousePress.prototype.to_buffer; +SpiceMsgcMouseRelease.prototype.buffer_size = SpiceMsgcMousePress.prototype.buffer_size; + + +function SpiceMsgcKeyDown(e) { + if (e) { + this.code = keycode_to_start_scan(e.keyCode, e.code); + } + else { + this.code = 0; + } +} + +SpiceMsgcKeyDown.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.code, true); at += 4; + return at; + }, + buffer_size: function () { + return 4; + } +}; + +function SpiceMsgcKeyUp(e) { + if (e) { + this.code = keycode_to_end_scan(e.keyCode, e.code); + } + else { + this.code = 0; + } +} + +/* Use the same functions as for KeyDown */ +SpiceMsgcKeyUp.prototype.to_buffer = SpiceMsgcKeyDown.prototype.to_buffer; +SpiceMsgcKeyUp.prototype.buffer_size = SpiceMsgcKeyDown.prototype.buffer_size; + +function SpiceMsgDisplayStreamCreate(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayStreamCreate.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.surface_id = dv.getUint32(at, true); at += 4; + this.id = dv.getUint32(at, true); at += 4; + this.flags = dv.getUint8(at, true); at += 1; + this.codec_type = dv.getUint8(at, true); at += 1; + this.stamp = dv.getUint64(at, true); at += 8; + this.stream_width = dv.getUint32(at, true); at += 4; + this.stream_height = dv.getUint32(at, true); at += 4; + this.src_width = dv.getUint32(at, true); at += 4; + this.src_height = dv.getUint32(at, true); at += 4; + + this.dest = new SpiceRect(); + at = this.dest.from_dv(dv, at, a); + this.clip = new SpiceClip(); + this.clip.from_dv(dv, at, a); + } +}; + +function SpiceStreamDataHeader(a, at) { +} + +SpiceStreamDataHeader.prototype = +{ + from_dv: function (dv, at, mb) { + this.id = dv.getUint32(at, true); at += 4; + this.multi_media_time = dv.getUint32(at, true); at += 4; + return at; + } +}; + +function SpiceMsgDisplayStreamData(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayStreamData.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.base = new SpiceStreamDataHeader(); + at = this.base.from_dv(dv, at, a); + this.data_size = dv.getUint32(at, true); at += 4; + this.data = dv.u8.subarray(at, at + this.data_size); + } +}; + +function SpiceMsgDisplayStreamDataSized(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayStreamDataSized.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.base = new SpiceStreamDataHeader(); + at = this.base.from_dv(dv, at, a); + this.width = dv.getUint32(at, true); at += 4; + this.height = dv.getUint32(at, true); at += 4; + this.dest = new SpiceRect(); + at = this.dest.from_dv(dv, at, a); + this.data_size = dv.getUint32(at, true); at += 4; + this.data = dv.u8.subarray(at, at + this.data_size); + } +}; + + +function SpiceMsgDisplayStreamClip(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayStreamClip.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.id = dv.getUint32(at, true); at += 4; + this.clip = new SpiceClip(); + this.clip.from_dv(dv, at, a); + } +}; + +function SpiceMsgDisplayStreamDestroy(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayStreamDestroy.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.id = dv.getUint32(at, true); at += 4; + } +}; + +function SpiceMsgDisplayStreamActivateReport(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgDisplayStreamActivateReport.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + this.stream_id = dv.getUint32(at, true); at += 4; + this.unique_id = dv.getUint32(at, true); at += 4; + this.max_window_size = dv.getUint32(at, true); at += 4; + this.timeout_ms = dv.getUint32(at, true); at += 4; + } +}; + +function SpiceMsgcDisplayStreamReport(stream_id, unique_id) { + this.stream_id = stream_id; + this.unique_id = unique_id; + this.start_frame_mm_time = 0; + this.end_frame_mm_time = 0; + this.num_frames = 0; + this.num_drops = 0; + this.last_frame_delay = 0; + + // TODO - Implement audio delay + this.audio_delay = -1; +} + +SpiceMsgcDisplayStreamReport.prototype = +{ + to_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + dv.setUint32(at, this.stream_id, true); at += 4; + dv.setUint32(at, this.unique_id, true); at += 4; + dv.setUint32(at, this.start_frame_mm_time, true); at += 4; + dv.setUint32(at, this.end_frame_mm_time, true); at += 4; + dv.setUint32(at, this.num_frames, true); at += 4; + dv.setUint32(at, this.num_drops, true); at += 4; + dv.setUint32(at, this.last_frame_delay, true); at += 4; + dv.setUint32(at, this.audio_delay, true); at += 4; + return at; + }, + buffer_size: function () { + return 8 * 4; + } +}; + +function SpiceMsgDisplayInvalList(a, at) { + this.count = 0; + this.resources = []; + this.from_buffer(a, at); +} + +SpiceMsgDisplayInvalList.prototype = +{ + from_buffer: function (a, at) { + var i; + at = at || 0; + var dv = new SpiceDataView(a); + this.count = dv.getUint16(at, true); at += 2; + for (i = 0; i < this.count; i++) { + this.resources[i] = {}; + this.resources[i].type = dv.getUint8(at, true); at++; + this.resources[i].id = dv.getUint64(at, true); at += 8; + } + } +}; + +function SpiceMsgPortInit(a, at) { + this.from_buffer(a, at); +} + +SpiceMsgPortInit.prototype = +{ + from_buffer: function (a, at) { + at = at || 0; + var dv = new SpiceDataView(a); + var namesize = dv.getUint32(at, true); at += 4; + var offset = dv.getUint32(at, true); at += 4; + this.opened = dv.getUint8(at, true); at += 1; + this.name = a.slice(offset, offset + namesize - 1); + } +}; + +export { + SpiceLinkHeader, + SpiceLinkMess, + SpiceLinkReply, + SpiceLinkAuthTicket, + SpiceLinkAuthReply, + SpiceMiniData, + SpiceMsgChannels, + SpiceMsgMainInit, + SpiceMsgMainMouseMode, + SpiceMsgMainAgentData, + SpiceMsgMainAgentTokens, + SpiceMsgSetAck, + SpiceMsgcAckSync, + SpiceMsgcMainMouseModeRequest, + SpiceMsgcMainAgentStart, + SpiceMsgcMainAgentData, + VDAgentAnnounceCapabilities, + VDAgentMonitorsConfig, + VDAgentFileXferStatusMessage, + VDAgentFileXferStartMessage, + VDAgentFileXferDataMessage, + SpiceMsgNotify, + SpiceMsgcDisplayInit, + SpiceMsgDisplayBase, + SpiceMsgDisplayDrawCopy, + SpiceMsgDisplayDrawFill, + SpiceMsgDisplayCopyBits, + SpiceMsgSurfaceCreate, + SpiceMsgSurfaceDestroy, + SpiceMsgInputsInit, + SpiceMsgInputsKeyModifiers, + SpiceMsgCursorInit, + SpiceMsgPlaybackData, + SpiceMsgPlaybackMode, + SpiceMsgPlaybackStart, + SpiceMsgCursorSet, + SpiceMsgcMousePosition, + SpiceMsgcMouseMotion, + SpiceMsgcMousePress, + SpiceMsgcMouseRelease, + SpiceMsgcKeyDown, + SpiceMsgcKeyUp, + SpiceMsgDisplayStreamCreate, + SpiceStreamDataHeader, + SpiceMsgDisplayStreamData, + SpiceMsgDisplayStreamDataSized, + SpiceMsgDisplayStreamClip, + SpiceMsgDisplayStreamDestroy, + SpiceMsgDisplayStreamActivateReport, + SpiceMsgcDisplayStreamReport, + SpiceMsgDisplayInvalList, + SpiceMsgPortInit +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/spicetype.js b/packages/itmat-ui-react/src/components/lxd/spice/src/spicetype.js new file mode 100644 index 000000000..959cbb249 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/spicetype.js @@ -0,0 +1,502 @@ +/* eslint-disable no-new-object */ +/* eslint-disable no-redeclare */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*---------------------------------------------------------------------------- +** Spice types +** This file contains classes for common spice types. +** Generally, they are used as helpers in reading and writing messages +** to and from the server. +**--------------------------------------------------------------------------*/ + +import { Constants } from './enums.js'; +import { SpiceQuic } from './quic.js'; + +function SpiceChannelId() +{ +} +SpiceChannelId.prototype = +{ + from_dv: function(dv, at, mb) + { + this.type = dv.getUint8(at, true); at ++; + this.id = dv.getUint8(at, true); at ++; + return at; + } +}; + +function SpiceRect() +{ +} + +SpiceRect.prototype = +{ + from_dv: function(dv, at, mb) + { + this.top = dv.getUint32(at, true); at += 4; + this.left = dv.getUint32(at, true); at += 4; + this.bottom = dv.getUint32(at, true); at += 4; + this.right = dv.getUint32(at, true); at += 4; + return at; + }, + is_same_size : function(r) + { + if ((this.bottom - this.top) == (r.bottom - r.top) && + (this.right - this.left) == (r.right - r.left) ) + return true; + + return false; + } +}; + +function SpiceClipRects() +{ +} + +SpiceClipRects.prototype = +{ + from_dv: function(dv, at, mb) + { + var i; + this.num_rects = dv.getUint32(at, true); at += 4; + if (this.num_rects > 0) + this.rects = []; + for (i = 0; i < this.num_rects; i++) + { + this.rects[i] = new SpiceRect(); + at = this.rects[i].from_dv(dv, at, mb); + } + return at; + } +}; + +function SpiceClip() +{ +} + +SpiceClip.prototype = +{ + from_dv: function(dv, at, mb) + { + this.type = dv.getUint8(at, true); at ++; + if (this.type == Constants.SPICE_CLIP_TYPE_RECTS) + { + this.rects = new SpiceClipRects(); + at = this.rects.from_dv(dv, at, mb); + } + return at; + } +}; + +function SpiceImageDescriptor() +{ +} + +SpiceImageDescriptor.prototype = +{ + from_dv: function(dv, at, mb) + { + this.id = dv.getUint64(at, true); at += 8; + this.type = dv.getUint8(at, true); at ++; + this.flags = dv.getUint8(at, true); at ++; + this.width = dv.getUint32(at, true); at += 4; + this.height= dv.getUint32(at, true); at += 4; + return at; + } +}; + +function SpicePalette() +{ +} + +SpicePalette.prototype = +{ + from_dv: function(dv, at, mb) + { + var i; + this.unique = dv.getUint64(at, true); at += 8; + this.num_ents = dv.getUint16(at, true); at += 2; + this.ents = []; + for (i = 0; i < this.num_ents; i++) + { + this.ents[i] = dv.getUint32(at, true); at += 4; + } + return at; + } +}; + +function SpiceBitmap() +{ +} + +SpiceBitmap.prototype = +{ + from_dv: function(dv, at, mb) + { + this.format = dv.getUint8(at, true); at++; + this.flags = dv.getUint8(at, true); at++; + this.x = dv.getUint32(at, true); at += 4; + this.y = dv.getUint32(at, true); at += 4; + this.stride = dv.getUint32(at, true); at += 4; + if (this.flags & Constants.SPICE_BITMAP_FLAGS_PAL_FROM_CACHE) + { + this.palette_id = dv.getUint64(at, true); at += 8; + } + else + { + var offset = dv.getUint32(at, true); at += 4; + if (offset == 0) + this.palette = null; + else + { + this.palette = new SpicePalette(); + this.palette.from_dv(dv, offset, mb); + } + } + // FIXME - should probably constrain this to the offset + // of palette, if non zero + this.data = mb.slice(at); + at += this.data.byteLength; + return at; + } +}; + +function SpiceImage() +{ +} + +SpiceImage.prototype = +{ + from_dv: function(dv, at, mb) + { + this.descriptor = new SpiceImageDescriptor(); + at = this.descriptor.from_dv(dv, at, mb); + + if (this.descriptor.type == Constants.SPICE_IMAGE_TYPE_LZ_RGB) + { + this.lz_rgb = new Object(); + this.lz_rgb.length = dv.getUint32(at, true); at += 4; + var initial_at = at; + this.lz_rgb.magic = ''; + for (var i = 3; i >= 0; i--) + this.lz_rgb.magic += String.fromCharCode(dv.getUint8(at + i)); + at += 4; + + // NOTE: The endian change is *correct* + this.lz_rgb.version = dv.getUint32(at); at += 4; + this.lz_rgb.type = dv.getUint32(at); at += 4; + this.lz_rgb.width = dv.getUint32(at); at += 4; + this.lz_rgb.height = dv.getUint32(at); at += 4; + this.lz_rgb.stride = dv.getUint32(at); at += 4; + this.lz_rgb.top_down = dv.getUint32(at); at += 4; + + var header_size = at - initial_at; + + this.lz_rgb.data = mb.slice(at, this.lz_rgb.length + at - header_size); + at += this.lz_rgb.data.byteLength; + + } + + if (this.descriptor.type == Constants.SPICE_IMAGE_TYPE_BITMAP) + { + this.bitmap = new SpiceBitmap(); + at = this.bitmap.from_dv(dv, at, mb); + } + + if (this.descriptor.type == Constants.SPICE_IMAGE_TYPE_SURFACE) + { + this.surface_id = dv.getUint32(at, true); at += 4; + } + + if (this.descriptor.type == Constants.SPICE_IMAGE_TYPE_JPEG) + { + this.jpeg = new Object(); + this.jpeg.data_size = dv.getUint32(at, true); at += 4; + this.jpeg.data = mb.slice(at); + at += this.jpeg.data.byteLength; + } + + if (this.descriptor.type == Constants.SPICE_IMAGE_TYPE_JPEG_ALPHA) + { + this.jpeg_alpha = new Object(); + this.jpeg_alpha.flags = dv.getUint8(at, true); at += 1; + this.jpeg_alpha.jpeg_size = dv.getUint32(at, true); at += 4; + this.jpeg_alpha.data_size = dv.getUint32(at, true); at += 4; + this.jpeg_alpha.data = mb.slice(at, this.jpeg_alpha.jpeg_size + at); + at += this.jpeg_alpha.data.byteLength; + // Alpha channel is an LZ image + this.jpeg_alpha.alpha = new Object(); + this.jpeg_alpha.alpha.length = this.jpeg_alpha.data_size - this.jpeg_alpha.jpeg_size; + var initial_at = at; + this.jpeg_alpha.alpha.magic = ''; + for (var i = 3; i >= 0; i--) + this.jpeg_alpha.alpha.magic += String.fromCharCode(dv.getUint8(at + i)); + at += 4; + + // NOTE: The endian change is *correct* + this.jpeg_alpha.alpha.version = dv.getUint32(at); at += 4; + this.jpeg_alpha.alpha.type = dv.getUint32(at); at += 4; + this.jpeg_alpha.alpha.width = dv.getUint32(at); at += 4; + this.jpeg_alpha.alpha.height = dv.getUint32(at); at += 4; + this.jpeg_alpha.alpha.stride = dv.getUint32(at); at += 4; + this.jpeg_alpha.alpha.top_down = dv.getUint32(at); at += 4; + + var header_size = at - initial_at; + + this.jpeg_alpha.alpha.data = mb.slice(at, this.jpeg_alpha.alpha.length + at - header_size); + at += this.jpeg_alpha.alpha.data.byteLength; + } + + if (this.descriptor.type == Constants.SPICE_IMAGE_TYPE_QUIC) + { + this.quic = new SpiceQuic(); + at = this.quic.from_dv(dv, at, mb); + } + return at; + } +}; + + +function SpiceQMask() +{ +} + +SpiceQMask.prototype = +{ + from_dv: function(dv, at, mb) + { + this.flags = dv.getUint8(at, true); at++; + this.pos = new SpicePoint(); + at = this.pos.from_dv(dv, at, mb); + var offset = dv.getUint32(at, true); at += 4; + if (offset == 0) + { + this.bitmap = null; + return at; + } + + this.bitmap = new SpiceImage(); + return this.bitmap.from_dv(dv, offset, mb); + } +}; + + +function SpicePattern() +{ +} + +SpicePattern.prototype = +{ + from_dv: function(dv, at, mb) + { + var offset = dv.getUint32(at, true); at += 4; + if (offset == 0) + { + this.pat = null; + } + else + { + this.pat = new SpiceImage(); + this.pat.from_dv(dv, offset, mb); + } + + this.pos = new SpicePoint(); + return this.pos.from_dv(dv, at, mb); + } +}; + +function SpiceBrush() +{ +} + +SpiceBrush.prototype = +{ + from_dv: function(dv, at, mb) + { + this.type = dv.getUint8(at, true); at ++; + if (this.type == Constants.SPICE_BRUSH_TYPE_SOLID) + { + this.color = dv.getUint32(at, true); at += 4; + } + else if (this.type == Constants.SPICE_BRUSH_TYPE_PATTERN) + { + this.pattern = new SpicePattern(); + at = this.pattern.from_dv(dv, at, mb); + } + return at; + } +}; + +function SpiceFill() +{ +} + +SpiceFill.prototype = +{ + from_dv: function(dv, at, mb) + { + this.brush = new SpiceBrush(); + at = this.brush.from_dv(dv, at, mb); + this.rop_descriptor = dv.getUint16(at, true); at += 2; + this.mask = new SpiceQMask(); + return this.mask.from_dv(dv, at, mb); + } +}; + + +function SpiceCopy() +{ +} + +SpiceCopy.prototype = +{ + from_dv: function(dv, at, mb) + { + var offset = dv.getUint32(at, true); at += 4; + if (offset == 0) + { + this.src_bitmap = null; + } + else + { + this.src_bitmap = new SpiceImage(); + this.src_bitmap.from_dv(dv, offset, mb); + } + this.src_area = new SpiceRect(); + at = this.src_area.from_dv(dv, at, mb); + this.rop_descriptor = dv.getUint16(at, true); at += 2; + this.scale_mode = dv.getUint8(at, true); at ++; + this.mask = new SpiceQMask(); + return this.mask.from_dv(dv, at, mb); + } +}; + +function SpicePoint16() +{ +} + +SpicePoint16.prototype = +{ + from_dv: function(dv, at, mb) + { + this.x = dv.getUint16(at, true); at += 2; + this.y = dv.getUint16(at, true); at += 2; + return at; + } +}; + +function SpicePoint() +{ +} + +SpicePoint.prototype = +{ + from_dv: function(dv, at, mb) + { + this.x = dv.getUint32(at, true); at += 4; + this.y = dv.getUint32(at, true); at += 4; + return at; + } +}; + +function SpiceCursorHeader() +{ +} + +SpiceCursorHeader.prototype = +{ + from_dv: function(dv, at, mb) + { + this.unique = dv.getUint64(at, true); at += 8; + this.type = dv.getUint8(at, true); at ++; + this.width = dv.getUint16(at, true); at += 2; + this.height = dv.getUint16(at, true); at += 2; + this.hot_spot_x = dv.getUint16(at, true); at += 2; + this.hot_spot_y = dv.getUint16(at, true); at += 2; + return at; + } +}; + +function SpiceCursor() +{ +} + +SpiceCursor.prototype = +{ + from_dv: function(dv, at, mb) + { + this.flags = dv.getUint16(at, true); at += 2; + if (this.flags & Constants.SPICE_CURSOR_FLAGS_NONE) + this.header = null; + else + { + this.header = new SpiceCursorHeader(); + at = this.header.from_dv(dv, at, mb); + this.data = mb.slice(at); + at += this.data.byteLength; + } + return at; + } +}; + +function SpiceSurface() +{ +} + +SpiceSurface.prototype = +{ + from_dv: function(dv, at, mb) + { + this.surface_id = dv.getUint32(at, true); at += 4; + this.width = dv.getUint32(at, true); at += 4; + this.height = dv.getUint32(at, true); at += 4; + this.format = dv.getUint32(at, true); at += 4; + this.flags = dv.getUint32(at, true); at += 4; + return at; + } +}; + +/* FIXME - SpiceImage types lz_plt, jpeg, zlib_glz, and jpeg_alpha are + completely unimplemented */ + +export { + SpiceChannelId, + SpiceRect, + SpiceClipRects, + SpiceClip, + SpiceImageDescriptor, + SpicePalette, + SpiceBitmap, + SpiceImage, + SpiceQMask, + SpicePattern, + SpiceBrush, + SpiceFill, + SpiceCopy, + SpicePoint16, + SpicePoint, + SpiceCursorHeader, + SpiceCursor, + SpiceSurface +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/jsbn.js b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/jsbn.js new file mode 100644 index 000000000..8e2934dae --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/jsbn.js @@ -0,0 +1,595 @@ +/* eslint-disable no-undef */ +/* eslint-disable eqeqeq */ +// Downloaded from http://www-cs-students.stanford.edu/~tjw/jsbn/ by Jeremy White on 6/1/2012 + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + + +// Basic JavaScript BN library - subset useful for RSA encryption. + +// Bits per digit +var dbits; + +// JavaScript engine analysis +var canary = 0xdeadbeefcafe; +var j_lm = ((canary&0xffffff)==0xefcafe); + +// (public) Constructor +function BigInteger(a,b,c) { + if(a != null) + if('number' == typeof a) this.fromNumber(a,b,c); + else if(b == null && 'string' != typeof a) this.fromString(a,256); + else this.fromString(a,b); +} + +// return new, unset BigInteger +function nbi() { return new BigInteger(null); } + +// am: Compute w_j += (x*this_i), propagate carries, +// c is initial carry, returns final carry. +// c < 3*dvalue, x < 2*dvalue, this_i < dvalue +// We need to select the fastest one that works in this environment. + +// am1: use a single mult and divide to get the high bits, +// max digit bits should be 26 because +// max internal value = 2*dvalue^2-2*dvalue (< 2^53) +function am1(i,x,w,j,c,n) { + while(--n >= 0) { + var v = x*this[i++]+w[j]+c; + c = Math.floor(v/0x4000000); + w[j++] = v&0x3ffffff; + } + return c; +} +// am2 avoids a big mult-and-extract completely. +// Max digit bits should be <= 30 because we do bitwise ops +// on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) +function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this[i]&0x7fff; + var h = this[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w[j++] = l&0x3fffffff; + } + return c; +} +// Alternately, set max digit bits to 28 since some +// browsers slow down when dealing with 32-bit numbers. +function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this[i]&0x3fff; + var h = this[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w[j++] = l&0xfffffff; + } + return c; +} +if(j_lm && (navigator.appName == 'Microsoft Internet Explorer')) { + BigInteger.prototype.am = am2; + dbits = 30; +} +else if(j_lm && (navigator.appName != 'Netscape')) { + BigInteger.prototype.am = am1; + dbits = 26; +} +else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; +} + +BigInteger.prototype.DB = dbits; +BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; +} + +// (protected) set from integer value x, -DV <= x < DV +function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this[0] = x; + else if(x < -1) this[0] = x+DV; + else this.t = 0; +} + +// return bigint initialized to value +function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + +// (protected) set from string and radix +function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == '-') mi = true; + continue; + } + mi = false; + if(sh == 0) + this[this.t++] = x; + else if(sh+k > this.DB) { + this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; +} + +// (public) return string representation in given radix +function bnToString(b) { + if(this.s < 0) return '-'+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this[i]&((1<>(p+=this.DB-k); + } + else { + d = (this[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:'0'; +} + +// (public) -this +function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + +// (public) |this| +function bnAbs() { return (this.s<0)?this.negate():this; } + +// (public) return + if this > a, - if this < a, 0 if equal +function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return r; + while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; + return 0; +} + +// returns bit length of the integer x +function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; +} + +// (public) return the number of bits in "this" +function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); +} + +// (protected) r = this << n*DB +function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; + for(i = n-1; i >= 0; --i) r[i] = 0; + r.t = this.t+n; + r.s = this.s; +} + +// (protected) r = this >> n*DB +function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r[i-n] = this[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; +} + +// (protected) r = this << n +function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r[i+ds+1] = (this[i]>>cbs)|c; + c = (this[i]&bm)<= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); +} + +// (protected) r = this >> n +function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r[i-ds-1] |= (this[i]&bm)<>bs; + } + if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r[i++] = this.DV+c; + else if(c > 0) r[i++] = c; + r.t = i; + r.clamp(); +} + +// (protected) r = this * a, r != this,a (HAC 14.12) +// "this" should be the larger one if appropriate. +function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); +} + +// (protected) r = this^2, r != this (HAC 14.16) +function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x[i],r,2*i,0,1); + if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r[i+x.t] -= x.DV; + r[i+x.t+1] = 1; + } + } + if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); + r.s = 0; + r.clamp(); +} + +// (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) +// r != q, this != m. q or r may be null. +function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); + if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); +} + +// (public) this mod a +function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; +} + +// Modular reduction using "classic" algorithm +function Classic(m) { this.m = m; } +function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; +} +function cRevert(x) { return x; } +function cReduce(x) { x.divRemTo(this.m,null,x); } +function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } +function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +Classic.prototype.convert = cConvert; +Classic.prototype.revert = cRevert; +Classic.prototype.reduce = cReduce; +Classic.prototype.mulTo = cMulTo; +Classic.prototype.sqrTo = cSqrTo; + +// (protected) return "-1/this % 2^DB"; useful for Mont. reduction +// justification: +// xy == 1 (mod m) +// xy = 1+km +// xy(2-xy) = (1+km)(1-km) +// x[y(2-xy)] = 1-k^2m^2 +// x[y(2-xy)] == 1 (mod m^2) +// if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 +// should reduce x and y(2-xy) by m^2 at each step to keep size bounded. +// JS multiply "overflows" differently from C/C++, so care is needed here. +function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; +} + +// Montgomery reduction +function Montgomery(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp&0x7fff; + this.mph = this.mp>>15; + this.um = (1<<(m.DB-15))-1; + this.mt2 = 2*m.t; +} + +// xR mod m +function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; +} + +// x/R mod m +function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; +} + +// x = x/R mod m (HAC 14.32) +function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = "x^2/R mod m"; x != r +function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = "xy/R mod m"; x,y != r +function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Montgomery.prototype.convert = montConvert; +Montgomery.prototype.revert = montRevert; +Montgomery.prototype.reduce = montReduce; +Montgomery.prototype.mulTo = montMulTo; +Montgomery.prototype.sqrTo = montSqrTo; + +// (protected) true iff this is even +function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } + +// (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) +function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); +} + +// (public) this^e % m, 0 <= e < 2^32 +function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); +} + +// protected +BigInteger.prototype.copyTo = bnpCopyTo; +BigInteger.prototype.fromInt = bnpFromInt; +BigInteger.prototype.fromString = bnpFromString; +BigInteger.prototype.clamp = bnpClamp; +BigInteger.prototype.dlShiftTo = bnpDLShiftTo; +BigInteger.prototype.drShiftTo = bnpDRShiftTo; +BigInteger.prototype.lShiftTo = bnpLShiftTo; +BigInteger.prototype.rShiftTo = bnpRShiftTo; +BigInteger.prototype.subTo = bnpSubTo; +BigInteger.prototype.multiplyTo = bnpMultiplyTo; +BigInteger.prototype.squareTo = bnpSquareTo; +BigInteger.prototype.divRemTo = bnpDivRemTo; +BigInteger.prototype.invDigit = bnpInvDigit; +BigInteger.prototype.isEven = bnpIsEven; +BigInteger.prototype.exp = bnpExp; + +// public +BigInteger.prototype.toString = bnToString; +BigInteger.prototype.negate = bnNegate; +BigInteger.prototype.abs = bnAbs; +BigInteger.prototype.compareTo = bnCompareTo; +BigInteger.prototype.bitLength = bnBitLength; +BigInteger.prototype.mod = bnMod; +BigInteger.prototype.modPowInt = bnModPowInt; + +// "constants" +BigInteger.ZERO = nbv(0); +BigInteger.ONE = nbv(1); + +export { + BigInteger +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/prng4.js b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/prng4.js new file mode 100644 index 000000000..75aafff81 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/prng4.js @@ -0,0 +1,84 @@ +// Downloaded from http://www-cs-students.stanford.edu/~tjw/jsbn/ by Jeremy White on 6/1/2012 + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + + +// prng4.js - uses Arcfour as a PRNG + +function Arcfour() { + this.i = 0; + this.j = 0; + this.S = []; +} + +// Initialize arcfour context from key, an array of ints, each from [0..255] +function ARC4init(key) { + var i, j, t; + for(i = 0; i < 256; ++i) + this.S[i] = i; + j = 0; + for(i = 0; i < 256; ++i) { + j = (j + this.S[i] + key[i % key.length]) & 255; + t = this.S[i]; + this.S[i] = this.S[j]; + this.S[j] = t; + } + this.i = 0; + this.j = 0; +} + +function ARC4next() { + var t; + this.i = (this.i + 1) & 255; + this.j = (this.j + this.S[this.i]) & 255; + t = this.S[this.i]; + this.S[this.i] = this.S[this.j]; + this.S[this.j] = t; + return this.S[(t + this.S[this.i]) & 255]; +} + +Arcfour.prototype.init = ARC4init; +Arcfour.prototype.next = ARC4next; + +// Plug in your RNG constructor here +function prng_newstate() { + return new Arcfour(); +} + +// Pool size must be a multiple of 4 and greater than 32. +// An array of bytes the size of the pool will be passed to init() +var rng_psize = 256; + +export { + prng_newstate, + rng_psize +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rng.js b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rng.js new file mode 100644 index 000000000..536a30311 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rng.js @@ -0,0 +1,109 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable @typescript-eslint/no-empty-function */ +// Downloaded from http://www-cs-students.stanford.edu/~tjw/jsbn/ by Jeremy White on 6/1/2012 + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + + +// Random number generator - requires a PRNG backend, e.g. prng4.js +import { prng_newstate, rng_psize } from './prng4.js'; + +// For best results, put code like +// +// in your main HTML document. + +var rng_state; +var rng_pool; +var rng_pptr; + +// Mix in a 32-bit integer into the pool +function rng_seed_int(x) { + rng_pool[rng_pptr++] ^= x & 255; + rng_pool[rng_pptr++] ^= (x >> 8) & 255; + rng_pool[rng_pptr++] ^= (x >> 16) & 255; + rng_pool[rng_pptr++] ^= (x >> 24) & 255; + if(rng_pptr >= rng_psize) rng_pptr -= rng_psize; +} + +// Mix in the current time (w/milliseconds) into the pool +function rng_seed_time() { + rng_seed_int(new Date().getTime()); +} + +// Initialize the pool with junk if needed. +if(rng_pool == null) { + rng_pool = []; + rng_pptr = 0; + var t; + if(navigator.appName == 'Netscape' && navigator.appVersion < '5' && window.crypto) { + // Extract entropy (256 bits) from NS4 RNG if available + var z = window.crypto.random(32); + for(t = 0; t < z.length; ++t) + rng_pool[rng_pptr++] = z.charCodeAt(t) & 255; + } + while(rng_pptr < rng_psize) { // extract some randomness from Math.random() + t = Math.floor(65536 * Math.random()); + rng_pool[rng_pptr++] = t >>> 8; + rng_pool[rng_pptr++] = t & 255; + } + rng_pptr = 0; + rng_seed_time(); + //rng_seed_int(window.screenX); + //rng_seed_int(window.screenY); +} + +function rng_get_byte() { + if(rng_state == null) { + rng_seed_time(); + rng_state = prng_newstate(); + rng_state.init(rng_pool); + for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) + rng_pool[rng_pptr] = 0; + rng_pptr = 0; + //rng_pool = null; + } + // TODO: allow reseeding after first request + return rng_state.next(); +} + +function rng_get_bytes(ba) { + var i; + for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); +} + +function SecureRandom() {} + +SecureRandom.prototype.nextBytes = rng_get_bytes; + +export { + SecureRandom +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rsa.js b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rsa.js new file mode 100644 index 000000000..0f36fbb50 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/rsa.js @@ -0,0 +1,156 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +// Downloaded from http://www-cs-students.stanford.edu/~tjw/jsbn/ by Jeremy White on 6/1/2012 +// Converted into an ES6 module + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + + +// Depends on jsbn.js and rng.js +import { BigInteger } from './jsbn.js'; +import { SecureRandom } from './rng.js'; + +// Version 1.1: support utf-8 encoding in pkcs1pad2 + +// convert a (hex) string to a bignum object +function parseBigInt(str,r) { + return new BigInteger(str,r); +} + +function linebrk(s,n) { + var ret = ''; + var i = 0; + while(i + n < s.length) { + ret += s.substring(i,i+n) + '\n'; + i += n; + } + return ret + s.substring(i,s.length); +} + +function byte2Hex(b) { + if(b < 0x10) + return '0' + b.toString(16); + else + return b.toString(16); +} + +// PKCS#1 (type 2, random) pad input string s to n bytes, and return a bigint +function pkcs1pad2(s,n) { + if(n < s.length + 11) { // TODO: fix for utf-8 + alert('Message too long for RSA'); + return null; + } + var ba = []; + var i = s.length - 1; + while(i >= 0 && n > 0) { + var c = s.charCodeAt(i--); + if(c < 128) { // encode using utf-8 + ba[--n] = c; + } + else if((c > 127) && (c < 2048)) { + ba[--n] = (c & 63) | 128; + ba[--n] = (c >> 6) | 192; + } + else { + ba[--n] = (c & 63) | 128; + ba[--n] = ((c >> 6) & 63) | 128; + ba[--n] = (c >> 12) | 224; + } + } + ba[--n] = 0; + var rng = new SecureRandom(); + var x = []; + while(n > 2) { // random non-zero pad + x[0] = 0; + while(x[0] == 0) rng.nextBytes(x); + ba[--n] = x[0]; + } + ba[--n] = 2; + ba[--n] = 0; + return new BigInteger(ba); +} + +// "empty" RSA key constructor +function RSAKey() { + this.n = null; + this.e = 0; + this.d = null; + this.p = null; + this.q = null; + this.dmp1 = null; + this.dmq1 = null; + this.coeff = null; +} + +// Set the public key fields N and e from hex strings +function RSASetPublic(N,E) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + } + else + alert('Invalid RSA public key'); +} + +// Perform raw public operation on "x": return x^e (mod n) +function RSADoPublic(x) { + return x.modPowInt(this.e, this.n); +} + +// Return the PKCS#1 RSA encryption of "text" as an even-length hex string +function RSAEncrypt(text) { + var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3); + if(m == null) return null; + var c = this.doPublic(m); + if(c == null) return null; + var h = c.toString(16); + if((h.length & 1) == 0) return h; else return '0' + h; +} + +// Return the PKCS#1 RSA encryption of "text" as a Base64-encoded string +//function RSAEncryptB64(text) { +// var h = this.encrypt(text); +// if(h) return hex2b64(h); else return null; +//} + +// protected +RSAKey.prototype.doPublic = RSADoPublic; + +// public +RSAKey.prototype.setPublic = RSASetPublic; +RSAKey.prototype.encrypt = RSAEncrypt; +//RSAKey.prototype.encrypt_b64 = RSAEncryptB64; + +export { + RSAKey +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/sha1.js b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/sha1.js new file mode 100644 index 000000000..4ed85d579 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/thirdparty/sha1.js @@ -0,0 +1,356 @@ +/* eslint-disable no-redeclare */ +/* eslint-disable eqeqeq */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined + * in FIPS 180-1 + * Version 2.2 Copyright Paul Johnston 2000 - 2009. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + */ + +/* Downloaded 6/1/2012 from the above address by Jeremy White. + License reproduce here for completeness: + +Copyright (c) 1998 - 2009, Paul Johnston & Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ''; /* base-64 pad character. "=" for strict RFC compliance */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_sha1(s) { return rstr2hex(rstr_sha1(str2rstr_utf8(s))); } +function b64_sha1(s) { return rstr2b64(rstr_sha1(str2rstr_utf8(s))); } +function any_sha1(s, e) { return rstr2any(rstr_sha1(str2rstr_utf8(s)), e); } +function hex_hmac_sha1(k, d) +{ return rstr2hex(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); } +function b64_hmac_sha1(k, d) +{ return rstr2b64(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); } +function any_hmac_sha1(k, d, e) +{ return rstr2any(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d)), e); } + +/* + * Perform a simple self-test to see if the VM is working + */ +function sha1_vm_test() +{ + return hex_sha1('abc').toLowerCase() == 'a9993e364706816aba3e25717850c26c9cd0d89d'; +} + +/* + * Calculate the SHA1 of a raw string + */ +function rstr_sha1(s) +{ + return binb2rstr(binb_sha1(rstr2binb(s), s.length * 8)); +} + +/* + * Calculate the HMAC-SHA1 of a key and some data (raw strings) + */ +function rstr_hmac_sha1(key, data) +{ + var bkey = rstr2binb(key); + if(bkey.length > 16) bkey = binb_sha1(bkey, key.length * 8); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = binb_sha1(ipad.concat(rstr2binb(data)), 512 + data.length * 8); + return binb2rstr(binb_sha1(opad.concat(hash), 512 + 160)); +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) +{ + try { hexcase; } catch(e) { hexcase=0; } + var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef'; + var output = ''; + var x; + for(var i = 0; i < input.length; i++) + { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0F) + + hex_tab.charAt( x & 0x0F); + } + return output; +} + +/* + * Convert a raw string to a base-64 string + */ +function rstr2b64(input) +{ + try { b64pad; } catch(e) { b64pad=''; } + var tab = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + var output = ''; + var len = input.length; + for(var i = 0; i < len; i += 3) + { + var triplet = (input.charCodeAt(i) << 16) + | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) + | (i + 2 < len ? input.charCodeAt(i+2) : 0); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > input.length * 8) output += b64pad; + else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); + } + } + return output; +} + +/* + * Convert a raw string to an arbitrary string encoding + */ +function rstr2any(input, encoding) +{ + var divisor = encoding.length; + var remainders = []; + var i, q, x, quotient; + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + var dividend = Array(Math.ceil(input.length / 2)); + for(i = 0; i < dividend.length; i++) + { + dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. We stop when the dividend is zero. + * All remainders are stored for later use. + */ + while(dividend.length > 0) + { + quotient = []; + x = 0; + for(i = 0; i < dividend.length; i++) + { + x = (x << 16) + dividend[i]; + q = Math.floor(x / divisor); + x -= q * divisor; + if(quotient.length > 0 || q > 0) + quotient[quotient.length] = q; + } + remainders[remainders.length] = x; + dividend = quotient; + } + + /* Convert the remainders to the output string */ + var output = ''; + for(i = remainders.length - 1; i >= 0; i--) + output += encoding.charAt(remainders[i]); + + /* Append leading zero equivalents */ + var full_length = Math.ceil(input.length * 8 / + (Math.log(encoding.length) / Math.log(2))); + for(i = output.length; i < full_length; i++) + output = encoding[0] + output; + + return output; +} + +/* + * Encode a string as utf-8. + * For efficiency, this assumes the input is valid utf-16. + */ +function str2rstr_utf8(input) +{ + var output = ''; + var i = -1; + var x, y; + + while(++i < input.length) + { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i); + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; + if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) + { + x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); + i++; + } + + /* Encode output as utf-8 */ + if(x <= 0x7F) + output += String.fromCharCode(x); + else if(x <= 0x7FF) + output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), + 0x80 | ( x & 0x3F)); + else if(x <= 0xFFFF) + output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + else if(x <= 0x1FFFFF) + output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + } + return output; +} + +/* + * Encode a string as utf-16 + */ +function str2rstr_utf16le(input) +{ + var output = ''; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode( input.charCodeAt(i) & 0xFF, + (input.charCodeAt(i) >>> 8) & 0xFF); + return output; +} + +function str2rstr_utf16be(input) +{ + var output = ''; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, + input.charCodeAt(i) & 0xFF); + return output; +} + +/* + * Convert a raw string to an array of big-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binb(input) +{ + var output = Array(input.length >> 2); + for(var i = 0; i < output.length; i++) + output[i] = 0; + for(var i = 0; i < input.length * 8; i += 8) + output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32); + return output; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2rstr(input) +{ + var output = ''; + for(var i = 0; i < input.length * 32; i += 8) + output += String.fromCharCode((input[i>>5] >>> (24 - i % 32)) & 0xFF); + return output; +} + +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function binb_sha1(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + var olde = e; + + for(var j = 0; j < 80; j++) + { + if(j < 16) w[j] = x[i + j]; + else w[j] = bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); + var t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = bit_rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return [a, b, c, d, e]; + +} + +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ +function sha1_ft(t, b, c, d) +{ + if(t < 20) return (b & c) | ((~b) & d); + if(t < 40) return b ^ c ^ d; + if(t < 60) return (b & c) | (b & d) | (c & d); + return b ^ c ^ d; +} + +/* + * Determine the appropriate additive constant for the current iteration + */ +function sha1_kt(t) +{ + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +export { + hex_sha1, + rstr_sha1 +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/ticket.js b/packages/itmat-ui-react/src/components/lxd/spice/src/ticket.js new file mode 100644 index 000000000..9dea5ab04 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/ticket.js @@ -0,0 +1,262 @@ +/* eslint-disable no-new-wrappers */ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import { RSAKey } from './thirdparty/rsa.js'; +import { BigInteger } from './thirdparty/jsbn.js'; +import { SecureRandom } from './thirdparty/rng.js'; +import { rstr_sha1 } from './thirdparty/sha1.js'; + +var SHA_DIGEST_LENGTH = 20; + +/*---------------------------------------------------------------------------- +** General ticket RSA encryption functions - just good enough to +** support what we need to send back an encrypted ticket. +**--------------------------------------------------------------------------*/ + + +/*---------------------------------------------------------------------------- +** OAEP padding functions. Inspired by the OpenSSL implementation. +**--------------------------------------------------------------------------*/ +function MGF1(mask, seed) +{ + var i, j, outlen; + for (i = 0, outlen = 0; outlen < mask.length; i++) + { + var combo_buf = new String(); + + for (j = 0; j < seed.length; j++) + combo_buf += String.fromCharCode(seed[j]); + combo_buf += String.fromCharCode((i >> 24) & 255); + combo_buf += String.fromCharCode((i >> 16) & 255); + combo_buf += String.fromCharCode((i >> 8) & 255); + combo_buf += String.fromCharCode((i) & 255); + + var combo_hash = rstr_sha1(combo_buf); + for (j = 0; j < combo_hash.length && outlen < mask.length; j++, outlen++) + { + mask[outlen] = combo_hash.charCodeAt(j); + } + } +} + + +function RSA_padding_add_PKCS1_OAEP(tolen, from, param) +{ + var seed = new Array(SHA_DIGEST_LENGTH); + var rand = new SecureRandom(); + rand.nextBytes(seed); + + var dblen = tolen - 1 - seed.length; + var db = new Array(dblen); + var padlen = dblen - from.length - 1; + var i; + + if (param === undefined) + param = ''; + + if (padlen < SHA_DIGEST_LENGTH) + { + console.log('Error - data too large for key size.'); + return null; + } + + for (i = 0; i < padlen; i++) + db[i] = 0; + + var param_hash = rstr_sha1(param); + for (i = 0; i < param_hash.length; i++) + db[i] = param_hash.charCodeAt(i); + + db[padlen] = 1; + for (i = 0; i < from.length; i++) + db[i + padlen + 1] = from.charCodeAt(i); + + var dbmask = new Array(dblen); + if (MGF1(dbmask, seed) < 0) + return null; + + for (i = 0; i < dbmask.length; i++) + db[i] ^= dbmask[i]; + + + var seedmask = Array(SHA_DIGEST_LENGTH); + if (MGF1(seedmask, db) < 0) + return null; + + for (i = 0; i < seedmask.length; i++) + seed[i] ^= seedmask[i]; + + var ret = new String(); + ret += String.fromCharCode(0); + for (i = 0; i < seed.length; i++) + ret += String.fromCharCode(seed[i]); + for (i = 0; i < db.length; i++) + ret += String.fromCharCode(db[i]); + return ret; +} + + +function asn_get_length(u8, at) +{ + var len = u8[at++]; + if (len > 0x80) + { + if (len != 0x81) + { + console.log('Error: we lazily don\'t support keys bigger than 255 bytes. It\'d be easy to fix.'); + return null; + } + len = u8[at++]; + } + + return [ at, len]; +} + +function find_sequence(u8, at) +{ + var lenblock; + at = at || 0; + if (u8[at++] != 0x30) + { + console.log('Error: public key should start with a sequence flag.'); + return null; + } + + lenblock = asn_get_length(u8, at); + if (! lenblock) + return null; + return lenblock; +} + +/*---------------------------------------------------------------------------- +** Extract an RSA key from a memory buffer +**--------------------------------------------------------------------------*/ +function create_rsa_from_mb(mb, at) +{ + var u8 = new Uint8Array(mb); + var lenblock; + var seq; + var ba; + var i; + var ret; + + /* We have a sequence which contains a sequence followed by a bit string */ + seq = find_sequence(u8, at); + if (! seq) + return null; + + at = seq[0]; + seq = find_sequence(u8, at); + if (! seq) + return null; + + /* Skip over the contained sequence */ + at = seq[0] + seq[1]; + if (u8[at++] != 0x3) + { + console.log('Error: expecting bit string next.'); + return null; + } + + /* Get the bit string, which is *itself* a sequence. Having fun yet? */ + lenblock = asn_get_length(u8, at); + if (! lenblock) + return null; + + at = lenblock[0]; + if (u8[at] != 0 && u8[at + 1] != 0x30) + { + console.log('Error: unexpected values in bit string.'); + return null; + } + + /* Okay, now we have a sequence of two binary values, we hope. */ + seq = find_sequence(u8, at + 1); + if (! seq) + return null; + + at = seq[0]; + if (u8[at++] != 0x02) + { + console.log('Error: expecting integer n next.'); + return null; + } + lenblock = asn_get_length(u8, at); + if (! lenblock) + return null; + at = lenblock[0]; + + ba = new Array(lenblock[1]); + for (i = 0; i < lenblock[1]; i++) + ba[i] = u8[at + i]; + + ret = new RSAKey(); + ret.n = new BigInteger(ba); + + at += lenblock[1]; + + if (u8[at++] != 0x02) + { + console.log('Error: expecting integer e next.'); + return null; + } + lenblock = asn_get_length(u8, at); + if (! lenblock) + return null; + at = lenblock[0]; + + ret.e = u8[at++]; + for (i = 1; i < lenblock[1]; i++) + { + ret.e <<= 8; + ret.e |= u8[at++]; + } + + return ret; +} + +function rsa_encrypt(rsa, str) +{ + var i; + var ret = []; + var oaep = RSA_padding_add_PKCS1_OAEP((rsa.n.bitLength()+7)>>3, str); + if (! oaep) + return null; + + var ba = new Array(oaep.length); + + for (i = 0; i < oaep.length; i++) + ba[i] = oaep.charCodeAt(i); + var bigint = new BigInteger(ba); + var enc = rsa.doPublic(bigint); + var h = enc.toString(16); + if ((h.length & 1) != 0) + h = '0' + h; + for (i = 0; i < h.length; i += 2) + ret[i / 2] = parseInt(h.substring(i, i + 2), 16); + return ret; +} + +export { + create_rsa_from_mb, + rsa_encrypt +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/utils.js b/packages/itmat-ui-react/src/components/lxd/spice/src/utils.js new file mode 100644 index 000000000..aee3f53a5 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/utils.js @@ -0,0 +1,349 @@ +/* eslint-disable eqeqeq */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +import { KeyNames } from './atKeynames.js'; +import { code_to_scancode } from './code_to_scancode.js'; + +/*---------------------------------------------------------------------------- +** Utility settings and functions for Spice +**--------------------------------------------------------------------------*/ +var DEBUG = 0; +var PLAYBACK_DEBUG = 0; +var STREAM_DEBUG = 0; +var DUMP_DRAWS = false; +var DUMP_CANVASES = false; + +/*---------------------------------------------------------------------------- +** We use an Image temporarily, and the image/src does not get garbage +** collected as quickly as we might like. This blank image helps with that. +**--------------------------------------------------------------------------*/ +var EMPTY_GIF_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + +/*---------------------------------------------------------------------------- +** combine_array_buffers +** Combine two array buffers. +** FIXME - this can't be optimal. See wire.js about eliminating the need. +**--------------------------------------------------------------------------*/ +function combine_array_buffers(a1, a2) { + var in1 = new Uint8Array(a1); + var in2 = new Uint8Array(a2); + var ret = new ArrayBuffer(a1.byteLength + a2.byteLength); + var out = new Uint8Array(ret); + var o = 0; + var i; + for (i = 0; i < in1.length; i++) + out[o++] = in1[i]; + for (i = 0; i < in2.length; i++) + out[o++] = in2[i]; + + return ret; +} + +/*---------------------------------------------------------------------------- +** hexdump_buffer +**--------------------------------------------------------------------------*/ +function hexdump_buffer(a) { + var mg = new Uint8Array(a); + var hex = ''; + var str = ''; + var last_zeros = 0; + for (var i = 0; i < mg.length; i++) { + var h = Number(mg[i]).toString(16); + if (h.length == 1) + hex += '0'; + hex += h + ' '; + + if (mg[i] == 10 || mg[i] == 13 || mg[i] == 8) + str += '.'; + else + str += String.fromCharCode(mg[i]); + + if ((i % 16 == 15) || (i == (mg.length - 1))) { + while (i % 16 != 15) { + hex += ' '; + i++; + } + + if (last_zeros == 0) + console.log(hex + ' | ' + str); + + if (hex == '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ') { + if (last_zeros == 1) { + console.log('.'); + last_zeros++; + } + else if (last_zeros == 0) + last_zeros++; + } + else + last_zeros = 0; + + hex = str = ''; + } + } +} + +/*---------------------------------------------------------------------------- +** Convert arraybuffer to string +**--------------------------------------------------------------------------*/ +function arraybuffer_to_str(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); +} + +/*---------------------------------------------------------------------------- +** Converting browser keycodes to AT scancodes is very hard. +** Spice transmits keys using the original AT scan codes, often +** described as 'Scan Code Set 1'. +** There is a confusion of other scan codes; Xorg synthesizes it's +** own in the same atKeynames.c file that has the XT codes. +** Scan code set 2 and 3 are more common, and use different values. +** Further, there is no formal specification for keycodes +** returned by browsers, so we have done our mapping largely with +** empirical testing. +** There has been little rigorous testing with International keyboards, +** and this would be an easy area for non English speakers to contribute. +**--------------------------------------------------------------------------*/ +var common_scanmap = []; + +/* The following appear to be keycodes that work in most browsers */ +common_scanmap['1'.charCodeAt(0)] = KeyNames.KEY_1; +common_scanmap['2'.charCodeAt(0)] = KeyNames.KEY_2; +common_scanmap['3'.charCodeAt(0)] = KeyNames.KEY_3; +common_scanmap['4'.charCodeAt(0)] = KeyNames.KEY_4; +common_scanmap['5'.charCodeAt(0)] = KeyNames.KEY_5; +common_scanmap['6'.charCodeAt(0)] = KeyNames.KEY_6; +common_scanmap['7'.charCodeAt(0)] = KeyNames.KEY_7; +common_scanmap['8'.charCodeAt(0)] = KeyNames.KEY_8; +common_scanmap['9'.charCodeAt(0)] = KeyNames.KEY_9; +common_scanmap['0'.charCodeAt(0)] = KeyNames.KEY_0; +common_scanmap[145] = KeyNames.KEY_ScrollLock; +common_scanmap[103] = KeyNames.KEY_KP_7; +common_scanmap[104] = KeyNames.KEY_KP_8; +common_scanmap[105] = KeyNames.KEY_KP_9; +common_scanmap[100] = KeyNames.KEY_KP_4; +common_scanmap[101] = KeyNames.KEY_KP_5; +common_scanmap[102] = KeyNames.KEY_KP_6; +common_scanmap[107] = KeyNames.KEY_KP_Plus; +common_scanmap[97] = KeyNames.KEY_KP_1; +common_scanmap[98] = KeyNames.KEY_KP_2; +common_scanmap[99] = KeyNames.KEY_KP_3; +common_scanmap[96] = KeyNames.KEY_KP_0; +common_scanmap[109] = KeyNames.KEY_Minus; +common_scanmap[110] = KeyNames.KEY_KP_Decimal; +common_scanmap[191] = KeyNames.KEY_Slash; +common_scanmap[190] = KeyNames.KEY_Period; +common_scanmap[188] = KeyNames.KEY_Comma; +common_scanmap[220] = KeyNames.KEY_BSlash; +common_scanmap[192] = KeyNames.KEY_Tilde; +common_scanmap[222] = KeyNames.KEY_Quote; +common_scanmap[219] = KeyNames.KEY_LBrace; +common_scanmap[221] = KeyNames.KEY_RBrace; + +common_scanmap['Q'.charCodeAt(0)] = KeyNames.KEY_Q; +common_scanmap['W'.charCodeAt(0)] = KeyNames.KEY_W; +common_scanmap['E'.charCodeAt(0)] = KeyNames.KEY_E; +common_scanmap['R'.charCodeAt(0)] = KeyNames.KEY_R; +common_scanmap['T'.charCodeAt(0)] = KeyNames.KEY_T; +common_scanmap['Y'.charCodeAt(0)] = KeyNames.KEY_Y; +common_scanmap['U'.charCodeAt(0)] = KeyNames.KEY_U; +common_scanmap['I'.charCodeAt(0)] = KeyNames.KEY_I; +common_scanmap['O'.charCodeAt(0)] = KeyNames.KEY_O; +common_scanmap['P'.charCodeAt(0)] = KeyNames.KEY_P; +common_scanmap['A'.charCodeAt(0)] = KeyNames.KEY_A; +common_scanmap['S'.charCodeAt(0)] = KeyNames.KEY_S; +common_scanmap['D'.charCodeAt(0)] = KeyNames.KEY_D; +common_scanmap['F'.charCodeAt(0)] = KeyNames.KEY_F; +common_scanmap['G'.charCodeAt(0)] = KeyNames.KEY_G; +common_scanmap['H'.charCodeAt(0)] = KeyNames.KEY_H; +common_scanmap['J'.charCodeAt(0)] = KeyNames.KEY_J; +common_scanmap['K'.charCodeAt(0)] = KeyNames.KEY_K; +common_scanmap['L'.charCodeAt(0)] = KeyNames.KEY_L; +common_scanmap['Z'.charCodeAt(0)] = KeyNames.KEY_Z; +common_scanmap['X'.charCodeAt(0)] = KeyNames.KEY_X; +common_scanmap['C'.charCodeAt(0)] = KeyNames.KEY_C; +common_scanmap['V'.charCodeAt(0)] = KeyNames.KEY_V; +common_scanmap['B'.charCodeAt(0)] = KeyNames.KEY_B; +common_scanmap['N'.charCodeAt(0)] = KeyNames.KEY_N; +common_scanmap['M'.charCodeAt(0)] = KeyNames.KEY_M; +common_scanmap[' '.charCodeAt(0)] = KeyNames.KEY_Space; +common_scanmap[13] = KeyNames.KEY_Enter; +common_scanmap[27] = KeyNames.KEY_Escape; +common_scanmap[8] = KeyNames.KEY_BackSpace; +common_scanmap[9] = KeyNames.KEY_Tab; +common_scanmap[16] = KeyNames.KEY_ShiftL; +common_scanmap[17] = KeyNames.KEY_LCtrl; +common_scanmap[18] = KeyNames.KEY_Alt; +common_scanmap[20] = KeyNames.KEY_CapsLock; +common_scanmap[44] = KeyNames.KEY_SysReqest; +common_scanmap[144] = KeyNames.KEY_NumLock; +common_scanmap[112] = KeyNames.KEY_F1; +common_scanmap[113] = KeyNames.KEY_F2; +common_scanmap[114] = KeyNames.KEY_F3; +common_scanmap[115] = KeyNames.KEY_F4; +common_scanmap[116] = KeyNames.KEY_F5; +common_scanmap[117] = KeyNames.KEY_F6; +common_scanmap[118] = KeyNames.KEY_F7; +common_scanmap[119] = KeyNames.KEY_F8; +common_scanmap[120] = KeyNames.KEY_F9; +common_scanmap[121] = KeyNames.KEY_F10; +common_scanmap[122] = KeyNames.KEY_F11; +common_scanmap[123] = KeyNames.KEY_F12; + +/* TODO: Break and Print are complex scan codes. XSpice cheats and + uses Xorg synthesized codes to simplify them. Fixing this will + require XSpice to handle the scan codes correctly, and then + fix spice-html5 to send the complex scan codes. */ +common_scanmap[42] = 99; // Print, XSpice only +common_scanmap[19] = 101;// Break, XSpice only + +/* Handle the so called 'GREY' keys, for the extended keys that + were grey on the original AT keyboard. These are + prefixed, as they were on the PS/2 controller, with an + 0xE0 byte to indicate that they are extended */ +common_scanmap[111] = 0xE0 | (KeyNames.KEY_Slash << 8);// KP_Divide +common_scanmap[106] = 0xE0 | (KeyNames.KEY_KP_Multiply << 8); // KP_Multiply +common_scanmap[36] = 0xE0 | (KeyNames.KEY_KP_7 << 8); // Home +common_scanmap[38] = 0xE0 | (KeyNames.KEY_KP_8 << 8); // Up +common_scanmap[33] = 0xE0 | (KeyNames.KEY_KP_9 << 8); // PgUp +common_scanmap[37] = 0xE0 | (KeyNames.KEY_KP_4 << 8); // Left +common_scanmap[39] = 0xE0 | (KeyNames.KEY_KP_6 << 8); // Right +common_scanmap[35] = 0xE0 | (KeyNames.KEY_KP_1 << 8); // End +common_scanmap[40] = 0xE0 | (KeyNames.KEY_KP_2 << 8); // Down +common_scanmap[34] = 0xE0 | (KeyNames.KEY_KP_3 << 8); // PgDown +common_scanmap[45] = 0xE0 | (KeyNames.KEY_KP_0 << 8); // Insert +common_scanmap[46] = 0xE0 | (KeyNames.KEY_KP_Decimal << 8); // Delete +common_scanmap[91] = 0xE0 | (0x5B << 8); //KeyNames.KEY_LMeta +common_scanmap[92] = 0xE0 | (0x5C << 8); //KeyNames.KEY_RMeta +common_scanmap[93] = 0xE0 | (0x5D << 8); //KeyNames.KEY_Menu + +/* Firefox/Mozilla codes */ +var firefox_scanmap = []; +firefox_scanmap[173] = KeyNames.KEY_Minus; +firefox_scanmap[61] = KeyNames.KEY_Equal; +firefox_scanmap[59] = KeyNames.KEY_SemiColon; + +/* DOM3 codes */ +var DOM_scanmap = []; +DOM_scanmap[189] = KeyNames.KEY_Minus; +DOM_scanmap[187] = KeyNames.KEY_Equal; +DOM_scanmap[186] = KeyNames.KEY_SemiColon; + +function get_scancode(keyCode, code) { + if (code_to_scancode[code] !== undefined) { + return code_to_scancode[code]; + } + + if (common_scanmap[keyCode] === undefined) { + if (navigator.userAgent.indexOf('Firefox') != -1) + return firefox_scanmap[keyCode]; + else + return DOM_scanmap[keyCode]; + } + else + return common_scanmap[keyCode]; +} + +function keycode_to_start_scan(keyCode, code) { + var scancode = get_scancode(keyCode, code); + if (scancode === undefined) { + alert('no map for ' + keyCode); + return 0; + } + + return scancode; +} + +function keycode_to_end_scan(keyCode, code) { + var scancode = get_scancode(keyCode, code); + if (scancode === undefined) + return 0; + + if (scancode < 0x100) { + return scancode | 0x80; + } else { + return scancode | 0x8000; + } +} + +function dump_media_element(m) { + var ret = + '[networkState ' + m.networkState + + '|readyState ' + m.readyState + + '|error ' + m.error + + '|seeking ' + m.seeking + + '|duration ' + m.duration + + '|paused ' + m.paused + + '|ended ' + m.error + + '|buffered ' + dump_timerange(m.buffered) + + ']'; + return ret; +} + +function dump_media_source(ms) { + var ret = + '[duration ' + ms.duration + + '|readyState ' + ms.readyState + ']'; + return ret; +} + +function dump_source_buffer(sb) { + var ret = + '[appendWindowStart ' + sb.appendWindowStart + + '|appendWindowEnd ' + sb.appendWindowEnd + + '|buffered ' + dump_timerange(sb.buffered) + + '|timeStampOffset ' + sb.timeStampOffset + + '|updating ' + sb.updating + + ']'; + return ret; +} + +function dump_timerange(tr) { + var ret; + + if (tr) { + var i = tr.length; + ret = '{len ' + i; + if (i > 0) + ret += '; start ' + tr.start(0) + '; end ' + tr.end(i - 1); + ret += '}'; + } + else + ret = 'N/A'; + + return ret; +} + +export { + DEBUG, + PLAYBACK_DEBUG, + STREAM_DEBUG, + DUMP_DRAWS, + DUMP_CANVASES, + EMPTY_GIF_IMAGE, + combine_array_buffers, + hexdump_buffer, + arraybuffer_to_str, + keycode_to_start_scan, + keycode_to_end_scan, + dump_media_element, + dump_media_source, + dump_source_buffer +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/webm.js b/packages/itmat-ui-react/src/components/lxd/spice/src/webm.js new file mode 100644 index 000000000..dce0a6045 --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/webm.js @@ -0,0 +1,673 @@ + +/* + Copyright (C) 2014 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + + +/*---------------------------------------------------------------------------- +** EBML identifiers +**--------------------------------------------------------------------------*/ +var EBML_HEADER = [ 0x1a, 0x45, 0xdf, 0xa3 ]; +var EBML_HEADER_VERSION = [ 0x42, 0x86 ]; +var EBML_HEADER_READ_VERSION = [ 0x42, 0xf7 ]; +var EBML_HEADER_MAX_ID_LENGTH = [ 0x42, 0xf2 ]; +var EBML_HEADER_MAX_SIZE_LENGTH = [ 0x42, 0xf3 ]; +var EBML_HEADER_DOC_TYPE = [ 0x42, 0x82 ]; +var EBML_HEADER_DOC_TYPE_VERSION = [ 0x42, 0x87 ]; +var EBML_HEADER_DOC_TYPE_READ_VERSION = [ 0x42, 0x85 ]; + +var WEBM_SEGMENT_HEADER = [ 0x18, 0x53, 0x80, 0x67 ]; +var WEBM_SEGMENT_INFORMATION = [ 0x15, 0x49, 0xA9, 0x66 ]; + +var WEBM_TIMECODE_SCALE = [ 0x2A, 0xD7, 0xB1 ]; +var WEBM_MUXING_APP = [ 0x4D, 0x80 ]; +var WEBM_WRITING_APP = [ 0x57, 0x41 ]; + +var WEBM_SEEK_HEAD = [ 0x11, 0x4D, 0x9B, 0x74 ]; +var WEBM_SEEK = [ 0x4D, 0xBB ]; +var WEBM_SEEK_ID = [ 0x53, 0xAB ]; +var WEBM_SEEK_POSITION = [ 0x53, 0xAC ]; + +var WEBM_TRACKS = [ 0x16, 0x54, 0xAE, 0x6B ]; +var WEBM_TRACK_ENTRY = [ 0xAE ]; +var WEBM_TRACK_NUMBER = [ 0xD7 ]; +var WEBM_TRACK_UID = [ 0x73, 0xC5 ]; +var WEBM_TRACK_TYPE = [ 0x83 ]; +var WEBM_FLAG_ENABLED = [ 0xB9 ]; +var WEBM_FLAG_DEFAULT = [ 0x88 ]; +var WEBM_FLAG_FORCED = [ 0x55, 0xAA ]; +var WEBM_FLAG_LACING = [ 0x9C ]; +var WEBM_MIN_CACHE = [ 0x6D, 0xE7 ]; + +var WEBM_MAX_BLOCK_ADDITION_ID = [ 0x55, 0xEE ]; +var WEBM_CODEC_DECODE_ALL = [ 0xAA ]; +var WEBM_SEEK_PRE_ROLL = [ 0x56, 0xBB ]; +var WEBM_CODEC_DELAY = [ 0x56, 0xAA ]; +var WEBM_CODEC_PRIVATE = [ 0x63, 0xA2 ]; +var WEBM_CODEC_ID = [ 0x86 ]; + +var WEBM_VIDEO = [ 0xE0 ] ; +var WEBM_FLAG_INTERLACED = [ 0x9A ] ; +var WEBM_PIXEL_WIDTH = [ 0xB0 ] ; +var WEBM_PIXEL_HEIGHT = [ 0xBA ] ; + +var WEBM_AUDIO = [ 0xE1 ] ; +var WEBM_SAMPLING_FREQUENCY = [ 0xB5 ] ; +var WEBM_CHANNELS = [ 0x9F ] ; + +var WEBM_CLUSTER = [ 0x1F, 0x43, 0xB6, 0x75 ]; +var WEBM_TIME_CODE = [ 0xE7 ] ; +var WEBM_SIMPLE_BLOCK = [ 0xA3 ] ; + +/*---------------------------------------------------------------------------- +** Various OPUS / Webm constants +**--------------------------------------------------------------------------*/ +var Constants = { + CLUSTER_SIMPLEBLOCK_FLAG_KEYFRAME : 1 << 7, + + OPUS_FREQUENCY : 48000, + OPUS_CHANNELS : 2, + + SPICE_PLAYBACK_CODEC : 'audio/webm; codecs="opus"', + MAX_CLUSTER_TIME : 1000, + + EXPECTED_PACKET_DURATION : 10, + GAP_DETECTION_THRESHOLD : 50, + + SPICE_VP8_CODEC : 'video/webm; codecs="vp8"' +}; + +/*---------------------------------------------------------------------------- +** EBML utility functions +** These classes can create the binary representation of a webm file +**--------------------------------------------------------------------------*/ +function EBML_write_u1_data_len(len, dv, at) +{ + var b = 0x80 | len; + dv.setUint8(at, b); + return at + 1; +} + +function EBML_write_u8_value(id, val, dv, at) +{ + at = EBML_write_array(id, dv, at); + at = EBML_write_u1_data_len(1, dv, at); + dv.setUint8(at, val); + return at + 1; +} + +function EBML_write_u32_value(id, val, dv, at) +{ + at = EBML_write_array(id, dv, at); + at = EBML_write_u1_data_len(4, dv, at); + dv.setUint32(at, val); + return at + 4; +} + +function EBML_write_u16_value(id, val, dv, at) +{ + at = EBML_write_array(id, dv, at); + at = EBML_write_u1_data_len(2, dv, at); + dv.setUint16(at, val); + return at + 2; +} + +function EBML_write_float_value(id, val, dv, at) +{ + at = EBML_write_array(id, dv, at); + at = EBML_write_u1_data_len(4, dv, at); + dv.setFloat32(at, val); + return at + 4; +} + + + +function EBML_write_u64_data_len(len, dv, at) +{ + /* Javascript doesn't do 64 bit ints, so this cheats and + just has a max of 32 bits. Fine for our purposes */ + dv.setUint8(at++, 0x01); + dv.setUint8(at++, 0x00); + dv.setUint8(at++, 0x00); + dv.setUint8(at++, 0x00); + var val = len & 0xFFFFFFFF; + for (var shift = 24; shift >= 0; shift -= 8) + dv.setUint8(at++, val >> shift); + return at; +} + +function EBML_write_array(arr, dv, at) +{ + for (var i = 0; i < arr.length; i++) + dv.setUint8(at + i, arr[i]); + return at + arr.length; +} + +function EBML_write_string(str, dv, at) +{ + for (var i = 0; i < str.length; i++) + dv.setUint8(at + i, str.charCodeAt(i)); + return at + str.length; +} + +function EBML_write_data(id, data, dv, at) +{ + at = EBML_write_array(id, dv, at); + if (data.length < 127) + at = EBML_write_u1_data_len(data.length, dv, at); + else + at = EBML_write_u64_data_len(data.length, dv, at); + if ((typeof data) == 'string') + at = EBML_write_string(data, dv, at); + else + at = EBML_write_array(data, dv, at); + return at; +} + +/*---------------------------------------------------------------------------- +** Webm objects +** These classes can create the binary representation of a webm file +**--------------------------------------------------------------------------*/ +function EBMLHeader() +{ + this.id = EBML_HEADER; + this.Version = 1; + this.ReadVersion = 1; + this.MaxIDLength = 4; + this.MaxSizeLength = 8; + this.DocType = 'webm'; + this.DocTypeVersion = 2; /* Not well specified by the WebM people, but functionally required for Firefox */ + this.DocTypeReadVersion = 2; +} + +EBMLHeader.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(0x1f, dv, at); + at = EBML_write_u8_value(EBML_HEADER_VERSION, this.Version, dv, at); + at = EBML_write_u8_value(EBML_HEADER_READ_VERSION, this.ReadVersion, dv, at); + at = EBML_write_u8_value(EBML_HEADER_MAX_ID_LENGTH, this.MaxIDLength, dv, at); + at = EBML_write_u8_value(EBML_HEADER_MAX_SIZE_LENGTH, this.MaxSizeLength, dv, at); + at = EBML_write_data(EBML_HEADER_DOC_TYPE, this.DocType, dv, at); + at = EBML_write_u8_value(EBML_HEADER_DOC_TYPE_VERSION, this.DocTypeVersion, dv, at); + at = EBML_write_u8_value(EBML_HEADER_DOC_TYPE_READ_VERSION, this.DocTypeReadVersion, dv, at); + + return at; + }, + buffer_size: function() + { + return 0x1f + 8 + this.id.length; + } +}; + +function webm_Segment() +{ + this.id = WEBM_SEGMENT_HEADER; +} + +webm_Segment.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + + at = EBML_write_array(this.id, dv, at); + dv.setUint8(at++, 0xff); + return at; + }, + buffer_size: function() + { + return this.id.length + 1; + } +}; + +function webm_SegmentInformation() +{ + this.id = WEBM_SEGMENT_INFORMATION; + this.timecode_scale = 1000000; /* 1 ms */ + this.muxing_app = 'spice'; + this.writing_app = 'spice-html5'; + +} + +webm_SegmentInformation.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at); + at = EBML_write_u32_value(WEBM_TIMECODE_SCALE, this.timecode_scale, dv, at); + at = EBML_write_data(WEBM_MUXING_APP, this.muxing_app, dv, at); + at = EBML_write_data(WEBM_WRITING_APP, this.writing_app, dv, at); + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + WEBM_TIMECODE_SCALE.length + 1 + 4 + + WEBM_MUXING_APP.length + 1 + this.muxing_app.length + + WEBM_WRITING_APP.length + 1 + this.writing_app.length; + } +}; + +function webm_Audio(frequency) +{ + this.id = WEBM_AUDIO; + this.sampling_frequency = frequency; + this.channels = Constants.OPUS_CHANNELS; +} + +webm_Audio.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at); + at = EBML_write_u8_value(WEBM_CHANNELS, this.channels, dv, at); + at = EBML_write_float_value(WEBM_SAMPLING_FREQUENCY, this.sampling_frequency, dv, at); + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + WEBM_SAMPLING_FREQUENCY.length + 1 + 4 + + WEBM_CHANNELS.length + 1 + 1; + } +}; + +function webm_Video(width, height) +{ + this.id = WEBM_VIDEO; + this.flag_interlaced = 0; + this.width = width; + this.height = height; +} + +webm_Video.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_INTERLACED, this.flag_interlaced, dv, at); + at = EBML_write_u16_value(WEBM_PIXEL_WIDTH, this.width, dv, at); + at = EBML_write_u16_value(WEBM_PIXEL_HEIGHT, this.height, dv, at); + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + WEBM_FLAG_INTERLACED.length + 1 + 1 + + WEBM_PIXEL_WIDTH.length + 1 + 2 + + WEBM_PIXEL_HEIGHT.length + 1 + 2; + } +}; + + + +/* --------------------------- + SeekHead not currently used. Hopefully not needed. +*/ +function webm_Seek(seekid, pos) +{ + this.id = WEBM_SEEK; + this.pos = pos; + this.seekid = seekid; +} + +webm_Seek.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u1_data_len(this.buffer_size() - 1 - this.id.length, dv, at); + + at = EBML_write_data(WEBM_SEEK_ID, this.seekid, dv, at); + at = EBML_write_u16_value(WEBM_SEEK_POSITION, this.pos, dv, at); + + return at; + }, + buffer_size: function() + { + return this.id.length + 1 + + WEBM_SEEK_ID.length + 1 + this.seekid.length + + WEBM_SEEK_POSITION.length + 1 + 2; + } +}; +function webm_SeekHead(info_pos, track_pos) +{ + this.id = WEBM_SEEK_HEAD; + this.info = new webm_Seek(WEBM_SEGMENT_INFORMATION, info_pos); + this.track = new webm_Seek(WEBM_TRACKS, track_pos); +} + +webm_SeekHead.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at); + + at = this.info.to_buffer(a, at); + at = this.track.to_buffer(a, at); + + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + this.info.buffer_size() + + this.track.buffer_size(); + } +}; + +/* ------------------------------- + End of Seek Head +*/ + +function webm_AudioTrackEntry() +{ + this.id = WEBM_TRACK_ENTRY; + this.number = 1; + this.uid = 2; // Arbitrary id; most likely makes no difference + this.type = 2; // Audio + this.flag_enabled = 1; + this.flag_default = 1; + this.flag_forced = 1; + this.flag_lacing = 0; + this.min_cache = 0; // fixme - check + this.max_block_addition_id = 0; + this.codec_decode_all = 0; // fixme - check + this.seek_pre_roll = 0; // 80000000; // fixme - check + this.codec_delay = 80000000; // Must match codec_private.preskip + this.codec_id = 'A_OPUS'; + this.audio = new webm_Audio(Constants.OPUS_FREQUENCY); + + // See: http://tools.ietf.org/html/draft-terriberry-oggopus-01 + this.codec_private = [ 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, // OpusHead + 0x01, // Version + Constants.OPUS_CHANNELS, + 0x00, 0x0F, // Preskip - 3840 samples - should be 8ms at 48kHz + 0x80, 0xbb, 0x00, 0x00, // 48000 + 0x00, 0x00, // Output gain + 0x00 // Channel mapping family + ]; +} + +webm_AudioTrackEntry.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at); + at = EBML_write_u8_value(WEBM_TRACK_NUMBER, this.number, dv, at); + at = EBML_write_u8_value(WEBM_TRACK_UID, this.uid, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_ENABLED, this.flag_enabled, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_DEFAULT, this.flag_default, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_FORCED, this.flag_forced, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_LACING, this.flag_lacing, dv, at); + at = EBML_write_data(WEBM_CODEC_ID, this.codec_id, dv, at); + at = EBML_write_u8_value(WEBM_MIN_CACHE, this.min_cache, dv, at); + at = EBML_write_u8_value(WEBM_MAX_BLOCK_ADDITION_ID, this.max_block_addition_id, dv, at); + at = EBML_write_u8_value(WEBM_CODEC_DECODE_ALL, this.codec_decode_all, dv, at); + at = EBML_write_u32_value(WEBM_CODEC_DELAY, this.codec_delay, dv, at); + at = EBML_write_u32_value(WEBM_SEEK_PRE_ROLL, this.seek_pre_roll, dv, at); + at = EBML_write_u8_value(WEBM_TRACK_TYPE, this.type, dv, at); + at = EBML_write_data(WEBM_CODEC_PRIVATE, this.codec_private, dv, at); + + at = this.audio.to_buffer(a, at); + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + WEBM_TRACK_NUMBER.length + 1 + 1 + + WEBM_TRACK_UID.length + 1 + 1 + + WEBM_TRACK_TYPE.length + 1 + 1 + + WEBM_FLAG_ENABLED.length + 1 + 1 + + WEBM_FLAG_DEFAULT.length + 1 + 1 + + WEBM_FLAG_FORCED.length + 1 + 1 + + WEBM_FLAG_LACING.length + 1 + 1 + + WEBM_MIN_CACHE.length + 1 + 1 + + WEBM_MAX_BLOCK_ADDITION_ID.length + 1 + 1 + + WEBM_CODEC_DECODE_ALL.length + 1 + 1 + + WEBM_SEEK_PRE_ROLL.length + 1 + 4 + + WEBM_CODEC_DELAY.length + 1 + 4 + + WEBM_CODEC_ID.length + this.codec_id.length + 1 + + WEBM_CODEC_PRIVATE.length + 1 + this.codec_private.length + + this.audio.buffer_size(); + } +}; + +function webm_VideoTrackEntry(width, height) +{ + /* + ** In general, we follow specifications found by looking here: + ** https://www.webmproject.org/docs/container/ + ** which points here: + ** https://www.matroska.org/technical/specs/index.html + ** and here: + ** https://datatracker.ietf.org/doc/draft-ietf-cellar-matroska/ + ** Our goal is to supply mandatory values, and note where we differ + ** from the default. + */ + this.id = WEBM_TRACK_ENTRY; + this.number = 1; + this.uid = 1; + this.type = 1; // Video + this.flag_enabled = 1; + this.flag_default = 1; + this.flag_forced = 1; // Different than default; we wish to force + this.flag_lacing = 1; + this.min_cache = 0; + this.max_block_addition_id = 0; + this.codec_id = 'V_VP8'; + this.codec_decode_all = 1; + this.seek_pre_roll = 0; + + this.video = new webm_Video(width, height); +} + +webm_VideoTrackEntry.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at); + at = EBML_write_u8_value(WEBM_TRACK_NUMBER, this.number, dv, at); + at = EBML_write_u8_value(WEBM_TRACK_UID, this.uid, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_ENABLED, this.flag_enabled, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_DEFAULT, this.flag_default, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_FORCED, this.flag_forced, dv, at); + at = EBML_write_u8_value(WEBM_FLAG_LACING, this.flag_lacing, dv, at); + at = EBML_write_data(WEBM_CODEC_ID, this.codec_id, dv, at); + at = EBML_write_u8_value(WEBM_MIN_CACHE, this.min_cache, dv, at); + at = EBML_write_u8_value(WEBM_MAX_BLOCK_ADDITION_ID, this.max_block_addition_id, dv, at); + at = EBML_write_u8_value(WEBM_CODEC_DECODE_ALL, this.codec_decode_all, dv, at); + at = EBML_write_u32_value(WEBM_SEEK_PRE_ROLL, this.seek_pre_roll, dv, at); + at = EBML_write_u8_value(WEBM_TRACK_TYPE, this.type, dv, at); + at = this.video.to_buffer(a, at); + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + WEBM_TRACK_NUMBER.length + 1 + 1 + + WEBM_TRACK_UID.length + 1 + 1 + + WEBM_FLAG_ENABLED.length + 1 + 1 + + WEBM_FLAG_DEFAULT.length + 1 + 1 + + WEBM_FLAG_FORCED.length + 1 + 1 + + WEBM_FLAG_LACING.length + 1 + 1 + + WEBM_CODEC_ID.length + this.codec_id.length + 1 + + WEBM_MIN_CACHE.length + 1 + 1 + + WEBM_MAX_BLOCK_ADDITION_ID.length + 1 + 1 + + WEBM_CODEC_DECODE_ALL.length + 1 + 1 + + WEBM_SEEK_PRE_ROLL.length + 1 + 4 + + WEBM_TRACK_TYPE.length + 1 + 1 + + this.video.buffer_size(); + } +}; + +function webm_Tracks(entry) +{ + this.id = WEBM_TRACKS; + this.track_entry = entry; +} + +webm_Tracks.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at); + at = this.track_entry.to_buffer(a, at); + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + this.track_entry.buffer_size(); + } +}; + +function webm_Cluster(timecode, data) +{ + this.id = WEBM_CLUSTER; + this.timecode = timecode; + this.data = data; +} + +webm_Cluster.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + dv.setUint8(at++, 0xff); + at = EBML_write_u32_value(WEBM_TIME_CODE, this.timecode, dv, at); + return at; + }, + buffer_size: function() + { + return this.id.length + 1 + + WEBM_TIME_CODE.length + 1 + 4; + } +}; + +function webm_SimpleBlock(timecode, data, keyframe) +{ + this.id = WEBM_SIMPLE_BLOCK; + this.timecode = timecode; + this.data = data; + this.keyframe = keyframe; +} + +webm_SimpleBlock.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + var dv = new DataView(a); + at = EBML_write_array(this.id, dv, at); + at = EBML_write_u64_data_len(this.data.byteLength + 4, dv, at); + at = EBML_write_u1_data_len(1, dv, at); // Track # + dv.setUint16(at, this.timecode); at += 2; // timecode - relative to cluster + dv.setUint8(at, this.keyframe ? Constants.CLUSTER_SIMPLEBLOCK_FLAG_KEYFRAME : 0); at += 1; // flags + + // FIXME - There should be a better way to copy + var u8 = new Uint8Array(this.data); + for (var i = 0; i < this.data.byteLength; i++) + dv.setUint8(at++, u8[i]); + + return at; + }, + buffer_size: function() + { + return this.id.length + 8 + + 1 + 2 + 1 + + this.data.byteLength; + } +}; + +function webm_Header() +{ + this.ebml = new EBMLHeader(); + this.segment = new webm_Segment(); + this.seek_head = new webm_SeekHead(0, 0); + + this.seek_head.info.pos = this.segment.buffer_size() + this.seek_head.buffer_size(); + + this.info = new webm_SegmentInformation(); + + this.seek_head.track.pos = this.seek_head.info.pos + this.info.buffer_size(); +} + +webm_Header.prototype = +{ + to_buffer: function(a, at) + { + at = at || 0; + at = this.ebml.to_buffer(a, at); + at = this.segment.to_buffer(a, at); + at = this.info.to_buffer(a, at); + + return at; + }, + buffer_size: function() + { + return this.ebml.buffer_size() + + this.segment.buffer_size() + + this.info.buffer_size(); + } +}; + +export { + Constants, + webm_Audio as Audio, + webm_Video as Video, + webm_AudioTrackEntry as AudioTrackEntry, + webm_VideoTrackEntry as VideoTrackEntry, + webm_Tracks as Tracks, + webm_Cluster as Cluster, + webm_SimpleBlock as SimpleBlock, + webm_Header as Header +}; diff --git a/packages/itmat-ui-react/src/components/lxd/spice/src/wire.js b/packages/itmat-ui-react/src/components/lxd/spice/src/wire.js new file mode 100644 index 000000000..c57a9d07e --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/spice/src/wire.js @@ -0,0 +1,134 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/* + Copyright (C) 2012 by Jeremy P. White + + This file is part of spice-html5. + + spice-html5 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + spice-html5 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with spice-html5. If not, see . +*/ + +/*-------------------------------------------------------------------------------------- +** SpiceWireReader +** This class will receive messages from a WebSocket and relay it to a given +** callback. It will optionally save and pass along a header, useful in processing +** the mini message format. +**--------------------------------------------------------------------------------------*/ + +import { DEBUG } from './utils.js'; +import { combine_array_buffers } from './utils.js'; + +function SpiceWireReader(sc, callback) +{ + this.sc = sc; + this.callback = callback; + this.needed = 0; + + this.buffers = []; + + this.sc.ws.wire_reader = this; + this.sc.ws.binaryType = 'arraybuffer'; + this.sc.ws.addEventListener('message', wire_blob_catcher); +} + +SpiceWireReader.prototype = +{ + + /*------------------------------------------------------------------------ + ** Process messages coming in from our WebSocket + **----------------------------------------------------------------------*/ + inbound: function (mb) + { + var at; + + /* Just buffer if we don't need anything yet */ + if (this.needed == 0) + { + this.buffers.push(mb); + return; + } + + /* Optimization - if we have just one inbound block, and it's + suitable for our needs, just use it. */ + if (this.buffers.length == 0 && mb.byteLength >= this.needed) + { + if (mb.byteLength > this.needed) + { + this.buffers.push(mb.slice(this.needed)); + mb = mb.slice(0, this.needed); + } + this.callback.call(this.sc, mb, + this.saved_msg_header || undefined); + } + else + { + this.buffers.push(mb); + } + + + /* If we have fragments that add up to what we need, combine them */ + /* FIXME - it would be faster to revise the processing code to handle + ** multiple fragments directly. Essentially, we should be + ** able to do this without any slice() or combine_array_buffers() calls */ + while (this.buffers.length > 1 && this.buffers[0].byteLength < this.needed) + { + var mb1 = this.buffers.shift(); + var mb2 = this.buffers.shift(); + + this.buffers.unshift(combine_array_buffers(mb1, mb2)); + } + + + while (this.buffers.length > 0 && this.buffers[0].byteLength >= this.needed) + { + mb = this.buffers.shift(); + if (mb.byteLength > this.needed) + { + this.buffers.unshift(mb.slice(this.needed)); + mb = mb.slice(0, this.needed); + } + this.callback.call(this.sc, mb, + this.saved_msg_header || undefined); + } + + }, + + request: function(n) + { + this.needed = n; + }, + + save_header: function(h) + { + this.saved_msg_header = h; + }, + + clear_header: function() + { + this.saved_msg_header = undefined; + } +}; + +function wire_blob_catcher(e) +{ + DEBUG > 1 && console.log('>> WebSockets.onmessage'); + DEBUG > 1 && console.log('id ' + this.wire_reader.sc.connection_id +'; type ' + this.wire_reader.sc.type); + SpiceWireReader.prototype.inbound.call(this.wire_reader, e.data); +} + +export { + SpiceWireReader +}; diff --git a/packages/itmat-ui-react/src/components/lxd/util/formatUtils.ts b/packages/itmat-ui-react/src/components/lxd/util/formatUtils.ts new file mode 100644 index 000000000..d5349305b --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/util/formatUtils.ts @@ -0,0 +1,36 @@ +// src/utils/formatUtils.ts +import { Cpu, Memory, Storage, Gpu} from '@itmat-broker/itmat-types'; + +// Format CPU information +export const formatCPUInfo = (cpu: Cpu) => { + if (!cpu.sockets) return 'CPU information not available'; + const coreCount = cpu.sockets.reduce((acc, socket) => acc + (socket.cores ? socket.cores.length : 0), 0); + return `CPU: ${cpu.total} Socket(s), ${coreCount} Core(s) Total, Architecture: ${cpu.architecture}`; +}; + +// Format Memory information +export const formatMemoryInfo = (memory: Memory) => { + return `Total Memory: ${formatBytes(memory.total)}, Used: ${formatBytes(memory.used)}, Free: ${formatBytes(memory.total - memory.used)}`; +}; + +// Format Storage information +export const formatStorageInfo = (storage: Storage) => { + if (!storage.disks) return 'Storage information not available'; + const totalSize = storage.disks.reduce((acc, disk) => acc + disk.size, 0); + return `Storage: ${storage.total} Disk(s), Total Size: ${formatBytes(totalSize)}`; +}; + +export const formatGPUInfo = (gpu: Gpu) => { + return `GPU: ${gpu.total} Card(s)`; +}; + + +// Helper function to format bytes into a more readable format +export function formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} diff --git a/packages/itmat-ui-react/src/components/lxd/util/updateMaxHeight.tsx b/packages/itmat-ui-react/src/components/lxd/util/updateMaxHeight.tsx new file mode 100644 index 000000000..1c6f64a8b --- /dev/null +++ b/packages/itmat-ui-react/src/components/lxd/util/updateMaxHeight.tsx @@ -0,0 +1,23 @@ +type HeightProperty = 'height' | 'max-height' | 'min-height'; + +export const updateMaxHeight = ( + targetClass: string, + bottomClass?: string, + additionalOffset = 0, + targetProperty: HeightProperty = 'height' +) => { + const elements = document.getElementsByClassName(targetClass); + const belowElements = bottomClass + ? document.getElementsByClassName(bottomClass) + : null; + if (elements.length !== 1 || (belowElements && belowElements.length !== 1)) { + return; + } + const above = elements[0].getBoundingClientRect().top + 1; + const below = belowElements + ? belowElements[0].getBoundingClientRect().height + 1 + : 0; + const offset = Math.ceil(above + below + additionalOffset); + const style = `${targetProperty}: calc(100vh - ${offset}px)`; + elements[0].setAttribute('style', style); +}; diff --git a/packages/itmat-ui-react/src/components/scaffold/mainMenuBar.tsx b/packages/itmat-ui-react/src/components/scaffold/mainMenuBar.tsx index eb0aa2822..537fb0516 100644 --- a/packages/itmat-ui-react/src/components/scaffold/mainMenuBar.tsx +++ b/packages/itmat-ui-react/src/components/scaffold/mainMenuBar.tsx @@ -69,10 +69,17 @@ export const MainMenuBar: FunctionComponent = () => {
My Drive
-
+ {/*
isActive ? css.clickedButton : undefined}>
Analytical Environment
+
*/} +
+
+ isActive ? css.clickedButton : undefined}> +
Analytical Environment
+
+
{ (whoAmI.data.type === enumUserTypes.ADMIN) ? @@ -102,6 +109,11 @@ export const MainMenuBar: FunctionComponent = () => {
Organisations
+
+ isActive ? css.clickedButton : undefined}> +
LXD
+
+
}]} /> diff --git a/packages/itmat-ui-react/src/components/scaffold/mainPanel.tsx b/packages/itmat-ui-react/src/components/scaffold/mainPanel.tsx index 53c5892e7..9a4cfb3fc 100644 --- a/packages/itmat-ui-react/src/components/scaffold/mainPanel.tsx +++ b/packages/itmat-ui-react/src/components/scaffold/mainPanel.tsx @@ -9,6 +9,8 @@ import css from './scaffold.module.css'; import { DrivePage } from '../drive'; import { DomainPage } from '../domain'; import { OrganisationPage } from '../organisation'; +import { InstancePage } from '../instance'; +import { LXDPage } from '../lxd'; export const MainPanel: FunctionComponent = () => { return ( @@ -24,6 +26,8 @@ export const MainPanel: FunctionComponent = () => { } /> } /> + } /> + } /> } /> diff --git a/yarn.lock b/yarn.lock index e535039d0..8555cbe84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4317,6 +4317,16 @@ dependencies: tslib "^2.3.0" +"@xterm/addon-fit@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@xterm/addon-fit/-/addon-fit-0.10.0.tgz#bebf87fadd74e3af30fdcdeef47030e2592c6f55" + integrity sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ== + +"@xterm/xterm@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0.tgz#275fb8f6e14afa6e8a0c05d4ebc94523ff775396" + integrity sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"