Skip to content

Commit

Permalink
refactor: updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjozork committed Jan 4, 2025
1 parent e6d613e commit 0dc5d6e
Show file tree
Hide file tree
Showing 10 changed files with 1,341 additions and 267 deletions.
34 changes: 21 additions & 13 deletions apps/remote/src/MainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const MainView: React.FC<MainViewProps> = ({ client }) => {
await new Promise((resolve) => setTimeout(resolve, 100));
};

const runCodeInIframe = (code: string, css: string, width: number, height: number) => {
const runCodeInIframe = (jsVfsUrl: string, cssVfsUrl: string, width: number, height: number) => {
if (!iframeRef.current || !iframeRef.current.contentDocument) {
return;
}
Expand All @@ -135,6 +135,11 @@ const MainView: React.FC<MainViewProps> = ({ client }) => {
iframeDocument.head.innerHTML = '';
iframeDocument.body.innerHTML = '';

const baseTag = iframeDocument.createElement('base');
baseTag.setAttribute('href', `http://localhost:8380/api/v1/remote-app/vfs-proxy/`);

iframeDocument.head.appendChild(baseTag);

const baseStyleTag = iframeDocument.createElement('style');
baseStyleTag.textContent = `
html, body {
Expand Down Expand Up @@ -179,7 +184,7 @@ const MainView: React.FC<MainViewProps> = ({ client }) => {
);

const scriptTag = iframeDocument.createElement('script');
scriptTag.textContent = code;
scriptTag.setAttribute('src', `.${jsVfsUrl}`);

iframeDocument.head.appendChild(scriptTag);

Expand All @@ -193,12 +198,16 @@ const MainView: React.FC<MainViewProps> = ({ client }) => {
iframeWindow.lastUpdate = now;
});

const cssTag = iframeDocument.createElement('style');
cssTag.textContent = css;
setTimeout(() => {
const cssTag = iframeDocument.createElement('link');
cssTag.setAttribute('href', `.${cssVfsUrl}`);
cssTag.setAttribute('rel', 'stylesheet');
cssTag.setAttribute('type', 'text/css');

iframeDocument.head.appendChild(cssTag);
iframeDocument.head.appendChild(cssTag);

iframeDocument.body.style.backgroundColor = 'black';
iframeDocument.body.style.backgroundColor = 'black';
}, 1_000);
};

useEffect(() => {
Expand Down Expand Up @@ -238,16 +247,15 @@ const MainView: React.FC<MainViewProps> = ({ client }) => {
}

if (instrument) {
const jsData = await client.downloadFile(instrument.gauges[0].bundles.js);
const cssData = await client.downloadFile(instrument.gauges[0].bundles.css);

const jsText = new TextDecoder('utf8').decode(jsData);
const cssText = new TextDecoder('utf8').decode(cssData);

dispatch(setLoadedInstrument(instrument));
dispatch(setCurrentSubscriptionGroupID(v4()));

runCodeInIframe(jsText, cssText, instrument.dimensions.width, instrument.dimensions.height);
runCodeInIframe(
instrument.gauges[0].bundles.js,
instrument.gauges[0].bundles.css,
instrument.dimensions.width,
instrument.dimensions.height,
);
} else {
dispatch(setLoadedInstrument(null));
dispatch(setCurrentSubscriptionGroupID(null));
Expand Down
1 change: 0 additions & 1 deletion apps/remote/src/RemoteClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,6 @@ export class RemoteClient {
if (this.awaitedMessagesPromiseFns[msg.type]) {
while (this.awaitedMessagesPromiseFns[msg.type].length > 0) {
const [resolve] = this.awaitedMessagesPromiseFns[msg.type][0];
console.log(`calling handler`, resolve, 'for message type', msg.type);

messageHandled = true;
resolve(msg);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UtilitiesModule } from './utilities/utilities.module';
import printerConfig from './config/printer.config';
import serverConfig from './config/server.config';
import { HealthModule } from './health/health.module';
import { VfsModule } from './utilities/vfs.module';

@Module({
imports: [
Expand All @@ -27,6 +28,7 @@ import { HealthModule } from './health/health.module';
CoRouteModule,
TerrainModule,
UtilitiesModule,
VfsModule,
InterfacesModule,
HealthModule,
],
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/interfaces/interfaces.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { UtilitiesModule } from '../utilities/utilities.module';
import { McduGateway } from './mcdu.gateway';
import { RemoteAppGateway } from './remote-app.gateway';
import { VfsModule } from '../utilities/vfs.module';

@Module({
imports: [UtilitiesModule],
imports: [UtilitiesModule, VfsModule],
providers: [McduGateway, RemoteAppGateway],
})
export class InterfacesModule {}
134 changes: 134 additions & 0 deletions apps/server/src/interfaces/remote-app.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Server, WebSocket } from 'ws';
import serverConfig from '../config/server.config';
import { NetworkService } from '../utilities/network.service';
import { protocolV0 } from '@flybywiresim/remote-bridge-types';
import { VfsService } from '../utilities/vfs.service';
import { v4 } from 'uuid';

type ClientType = 'aircraft' | 'remote';

Expand All @@ -22,22 +24,139 @@ export class RemoteAppGateway implements OnGatewayInit, OnGatewayConnection {
constructor(
@Inject(serverConfig.KEY) private serverConf: ConfigType<typeof serverConfig>,
private networkService: NetworkService,
private readonly vfsService: VfsService,
) {}

private readonly logger = new Logger(RemoteAppGateway.name);

@WebSocketServer() server: Server;

private aircraftClient: WebSocket | null = null;

async afterInit(server: Server) {
this.server = server;
this.logger.log('Remote app gateway websocket initialised');
this.logger.log(
`Initialised on http://${await this.networkService.getLocalIp(true)}:${this.serverConf.port}${server.path}`,
);

this.vfsService.requestFile = async (path) => {
const data = await this.downloadFile(`/${path}`);

return Buffer.from(data);
};
}

private readonly sessions = new Map<WebSocket, RemoteBridgeConnection>();

private awaitedMessagesTypesToKeys = new Map<string, string>();

private queuedUnhandledMessages: protocolV0.Messages[] = [];

private awaitedMessagesPromiseFns: Record<string, [(arg: any) => void, (arg: any) => void][]> = {};

public async downloadFile(fileVfsPath: string): Promise<Uint8Array> {
if (!this.aircraftClient) {
throw new Error('Cannot download a file without an aircraft client');
}

const requestID = v4();

try {
this.sendMessage(this.aircraftClient, {
type: 'remoteDownloadFile',
requestID,
fileVfsPath,
fromClientID: 'gateway',
});

const chunks: Uint8Array[] = [];

let doneDownloading = false;
while (!doneDownloading) {
const nextChunk = await this.awaitMessageOfType<protocolV0.AircraftSendFileChunkMessage>(
'aircraftSendFileChunk',
requestID,
);

if (nextChunk.requestID !== requestID) {
continue;
}

console.log(
`[RemoteClient](downloadFile) Received chunk #${nextChunk.chunkIndex + 1} / ${
nextChunk.chunkCount
} for request ${requestID}`,
);

const chunk = await (await fetch(`data:application/octet-stream;base64,${nextChunk.data}`))
.arrayBuffer()
.then((it) => new Uint8Array(it));

chunks.push(chunk);

if (nextChunk.chunkIndex === nextChunk.chunkCount - 1) {
console.log(`[RemoteClient](downloadFile) Done downloading for request ${requestID}`);
this.stopAwaitingMessages('aircraftSendFileChunk', requestID);
doneDownloading = true;
}
}

const totalLength = chunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
const mergedArray = new Uint8Array(totalLength);

let offset = 0;
for (const item of chunks) {
mergedArray.set(item, offset);
offset += item.length;
}

return mergedArray;
} catch (e) {
this.stopAwaitingMessages('aircraftSendFileChunk', requestID);
throw e;
}
}

private async awaitMessageOfType<M extends protocolV0.Messages>(
type: M['type'] & string,
uniqueKey: string,
): Promise<M> {
if (this.awaitedMessagesTypesToKeys.has(type) && this.awaitedMessagesTypesToKeys.get(type) !== uniqueKey) {
throw new Error(
'[RemoteClient](awaitMessageOfType) Messages can only be awaited by one consumer at a time. Make sure to call stopAwaitingMessages after you are done handling messages',
);
}

this.awaitedMessagesTypesToKeys.set(type, uniqueKey);

const firstUnhandledMessagesIndex = this.queuedUnhandledMessages.findIndex((it) => it.type === type);

if (firstUnhandledMessagesIndex !== -1) {
const msg = this.queuedUnhandledMessages[firstUnhandledMessagesIndex];

this.queuedUnhandledMessages.splice(firstUnhandledMessagesIndex, 1);

return msg as M;
}

return new Promise((resolve, reject) => {
let array = this.awaitedMessagesPromiseFns[type];
if (!array) {
array = this.awaitedMessagesPromiseFns[type] = [];
}

array.push([resolve, reject]);
});
}

private stopAwaitingMessages(type: string, uniqueKey: string): void {
if (this.awaitedMessagesTypesToKeys.get(type) === uniqueKey) {
this.awaitedMessagesTypesToKeys.delete(type);
return;
}
}

handleConnection(client: WebSocket) {
this.logger.log('Client connected');

Expand Down Expand Up @@ -80,6 +199,9 @@ export class RemoteAppGateway implements OnGatewayInit, OnGatewayConnection {
this.sessions.set(client, { type: 'aircraft', clientName: msg.clientName, clientID: msg.fromClientID });

this.broadcastMessage(msg, msg.fromClientID, 'remote');

this.aircraftClient = client;

break;
}
case 'remoteSignin': {
Expand All @@ -99,6 +221,16 @@ export class RemoteAppGateway implements OnGatewayInit, OnGatewayConnection {
this.broadcastMessage(msg, session.clientID, session.type === 'remote' ? 'aircraft' : 'remote');
}
}

if (this.awaitedMessagesPromiseFns[msg.type]) {
while (this.awaitedMessagesPromiseFns[msg.type].length > 0) {
const [resolve] = this.awaitedMessagesPromiseFns[msg.type][0];

resolve(msg);

this.awaitedMessagesPromiseFns[msg.type].shift();
}
}
});

client.on('close', () => {
Expand All @@ -113,6 +245,8 @@ export class RemoteAppGateway implements OnGatewayInit, OnGatewayConnection {
session.clientID,
'remote',
);

this.aircraftClient = null;
} else if (session) {
this.broadcastMessage(
{ type: 'remoteClientDisconnect', clientID: session.clientID, fromClientID: 'gateway' },
Expand Down
44 changes: 44 additions & 0 deletions apps/server/src/utilities/vfs.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ApiTags } from '@nestjs/swagger';
import { Controller, Get, HttpException, HttpStatus, Param, Res, Response, StreamableFile } from '@nestjs/common';
import { contentType } from 'mime-types';
import { VfsService } from './vfs.service';
import * as path from 'node:path';
import { ExpressAdapter } from '@nestjs/platform-express';
import { HttpAdapterHost } from '@nestjs/core';

@ApiTags('VFS')
@Controller('api/v1/remote-app/vfs-proxy')
export class VfsController {
constructor(
private readonly vfsService: VfsService,
private readonly httpAdapterHost: HttpAdapterHost<ExpressAdapter>,
) {}

@Get('/:filePath(*)')
async getFile(@Param('filePath') filePath: string, @Res({ passthrough: true }) res: Response) {
if (this.vfsService.requestFile === null) {
throw new HttpException(
'The VFS service is not ready to serve files at this moment. Is an airplane client connected to the remote bridge?',
HttpStatus.SERVICE_UNAVAILABLE,
);
}

const ext = path.extname(filePath);

if (ext === '') {
throw new HttpException('Malformed request: Could not find a file extension', HttpStatus.BAD_REQUEST);
}

const contentTypeHeader = contentType(ext);

if (contentTypeHeader === false) {
throw new HttpException('Malformed request: Could not find a valid file extension', HttpStatus.BAD_REQUEST);
}

const data = await this.vfsService.requestFile(filePath);

this.httpAdapterHost.httpAdapter.setHeader(res, 'Content-Type', contentTypeHeader);

return new StreamableFile(data);
}
}
10 changes: 10 additions & 0 deletions apps/server/src/utilities/vfs.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { VfsController } from './vfs.controller';
import { VfsService } from './vfs.service';

@Module({
controllers: [VfsController],
providers: [VfsService],
exports: [VfsService],
})
export class VfsModule {}
8 changes: 8 additions & 0 deletions apps/server/src/utilities/vfs.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';

export type RequestFileFunction = (filePath: string) => Promise<Buffer>;

@Injectable()
export class VfsService {
public requestFile: RequestFileFunction | null = null;
}
Loading

0 comments on commit 0dc5d6e

Please sign in to comment.