From 9c1bcfba8ae82fd0e8919ae483a3bab4c8fa2679 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simon=20D=C3=B6ring?= <simon.doering@stud.hs-bochum.de>
Date: Mon, 4 Jan 2021 21:00:21 +0100
Subject: [PATCH] Migrate camera server to Typescript

---
 .gitignore                                    |   1 +
 camera-server/models/validation-error.js      |   7 -
 camera-server/package-lock.json               |  29 ++
 camera-server/package.json                    |   3 +
 camera-server/server.js                       | 342 ---------------
 camera-server/src/models/camera-slot-state.ts |  22 +
 camera-server/src/models/sender-socket.ts     |   6 +
 camera-server/src/models/validation-error.ts  |   5 +
 camera-server/src/server.ts                   | 411 ++++++++++++++++++
 camera-server/src/util/cleanup.ts             |  35 ++
 camera-server/test-server.js                  |  28 --
 camera-server/tsconfig.json                   |  36 ++
 12 files changed, 548 insertions(+), 377 deletions(-)
 delete mode 100644 camera-server/models/validation-error.js
 delete mode 100644 camera-server/server.js
 create mode 100644 camera-server/src/models/camera-slot-state.ts
 create mode 100644 camera-server/src/models/sender-socket.ts
 create mode 100644 camera-server/src/models/validation-error.ts
 create mode 100644 camera-server/src/server.ts
 create mode 100644 camera-server/src/util/cleanup.ts
 delete mode 100644 camera-server/test-server.js
 create mode 100644 camera-server/tsconfig.json

diff --git a/.gitignore b/.gitignore
index c2658d7..b947077 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 node_modules/
+dist/
diff --git a/camera-server/models/validation-error.js b/camera-server/models/validation-error.js
deleted file mode 100644
index 1a1d681..0000000
--- a/camera-server/models/validation-error.js
+++ /dev/null
@@ -1,7 +0,0 @@
-class ValidationError extends Error {
-    constructor(message) {
-        super(message);
-    }
-}
-
-module.exports = ValidationError;
diff --git a/camera-server/package-lock.json b/camera-server/package-lock.json
index 205ee3b..252583e 100644
--- a/camera-server/package-lock.json
+++ b/camera-server/package-lock.json
@@ -19,11 +19,40 @@
       "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz",
       "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg=="
     },
+    "@types/engine.io": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.4.tgz",
+      "integrity": "sha512-98rXVukLD6/ozrQ2O80NAlWDGA4INg+tqsEReWJldqyi2fulC9V7Use/n28SWgROXKm6003ycWV4gZHoF8GA6w==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/node": {
       "version": "14.14.12",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.12.tgz",
       "integrity": "sha512-ASH8OPHMNlkdjrEdmoILmzFfsJICvhBsFfAum4aKZ/9U4B6M6tTmTPh+f3ttWdD74CEGV5XvXWkbyfSdXaTd7g=="
     },
+    "@types/socket.io": {
+      "version": "2.1.12",
+      "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.12.tgz",
+      "integrity": "sha512-oStc5VFkpb0AsjOxQUj9ztX5Iziatyla/rjZTYbFGoVrrKwd+JU2mtxk7iSl5RGYx9WunLo6UXW1fBzQok/ZyA==",
+      "dev": true,
+      "requires": {
+        "@types/engine.io": "*",
+        "@types/node": "*",
+        "@types/socket.io-parser": "*"
+      }
+    },
+    "@types/socket.io-parser": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/@types/socket.io-parser/-/socket.io-parser-2.2.1.tgz",
+      "integrity": "sha512-+JNb+7N7tSINyXPxAJb62+NcpC1x/fPn7z818W4xeNCdPTp6VsO/X8fCsg6+ug4a56m1v9sEiTIIUKVupcHOFQ==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "accepts": {
       "version": "1.3.7",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
diff --git a/camera-server/package.json b/camera-server/package.json
index 698779d..04faac0 100644
--- a/camera-server/package.json
+++ b/camera-server/package.json
@@ -7,5 +7,8 @@
   },
   "dependencies": {
     "socket.io": "^3.0.4"
+  },
+  "devDependencies": {
+    "@types/socket.io": "^2.1.12"
   }
 }
diff --git a/camera-server/server.js b/camera-server/server.js
deleted file mode 100644
index 08bd485..0000000
--- a/camera-server/server.js
+++ /dev/null
@@ -1,342 +0,0 @@
-const ValidationError = require('./models/validation-error');
-
-const readline = require('readline');
-const rl = readline.createInterface({
-    input: process.stdin,
-    output: process.stdout,
-    terminal: false
-});
-
-let port = 5000;
-if (process.env.PORT) {
-    port = +process.env.PORT;
-    console.log('Using port ' + port + ' from PORT environment variable');
-} else {
-    console.log('Got no PORT environment variable - using default port ' + port);
-}
-
-let cameraSlots = 4;
-if (process.env.CAMERA_SLOTS) {
-    cameraSlots = +process.env.CAMERA_SLOTS;
-    console.log('Using camera count ' + cameraSlots + ' from CAMERA_SLOTS environment variable');
-} else {
-    console.log('Got no CAMERA_SLOTS environment variable - using default count of ' + cameraSlots);
-}
-
-const io = require('socket.io')(port);
-
-const visibilityCommands = ['hide', 'show'];
-const geometryCommands = [
-    'set_geometry_relative_to_window',
-    'set_geometry_relative_to_canvas'
-];
-const internalCommands = [
-    'activate_slot',
-    'deactivate_slot',
-    'refresh_token'
-];
-
-let cameraStates = [];
-for (let i = 0; i < cameraSlots; i++) {
-    cameraStates.push({
-        slotActive: false,
-        token: null,
-        feedActive: false,
-        feedId: null,
-        senderSocketId: null,
-        visibility: {
-            command: 'show',
-            params: []
-        },
-        geometry: {
-            command: 'set_geometry_relative_to_canvas',
-            params: ['rb', '0', '0', '200', '200']
-        }
-    });
-}
-
-const emitNewFeed = (slot) => {
-    const cameraState = cameraStates[slot];
-    io.emit('new_feed', {
-        slot,
-        feedId: cameraState.feedId,
-        visibility: cameraState.visibility,
-        geometry: cameraState.geometry
-    });
-};
-
-const emitRemoveFeed = (slot) => {
-    io.emit('remove_feed', { slot });
-};
-
-const handleSetFeedId = (socket, data, fn) => {
-    let success = true;
-    let message = '';
-
-    try {
-        const slot = socket.cameraSlot;
-        const currentCameraState = cameraStates[slot];
-
-        if (currentCameraState.token !== socket.cameraSlotToken) {
-            console.log('Error: Got set_feed_id event for slot ' + slot + ' with an old token');
-            throw new ValidationError('The provided token is not valid anymore - the feed is not transmitted');
-        }
-
-        if (currentCameraState.feedActive) {
-            console.log('Error: Got set_feed_id event for slot ' + slot + ' which already has an active feed');
-            throw new ValidationError('There is already somebody using this slot');
-        }
-
-        if (data == null) {
-            console.log('Error: Got set_feed_id event for slot ' + slot + ' without data');
-            throw new ValidationError('Could not get feed id because no data was provided');
-        }
-
-        const feedId = data.feedId;
-        if (feedId == null) {
-            console.log('Error: Got set_feed_id event without a feed id on slot ' + slot);
-            throw new ValidationError('No feed id was provided');
-        }
-
-        console.log('Setting feed id of slot ' + slot + ' to ' + feedId);
-        message = 'Successfully set feed id - you are now using this slot';
-
-        currentCameraState.feedActive = true;
-        currentCameraState.feedId = feedId;
-        currentCameraState.senderSocketId = socket.id;
-
-        emitNewFeed(slot);
-    } catch (e) {
-        if (e instanceof ValidationError) {
-            success = false;
-            message = e.message;
-        } else {
-            throw e;
-        }
-    }
-
-    fn({ success, message });
-};
-
-const handleSenderDisconnect = (socket, reason) => {
-    const slot = socket.cameraSlot;
-    if (slot != null) {
-        const currentCameraState = cameraStates[slot];
-        if (currentCameraState.feedActive && socket.id === currentCameraState.senderSocketId) {
-            console.log('Sender on slot ' + slot + ' disconnected - Clearing slot');
-            currentCameraState.feedActive = false;
-            currentCameraState.feedId = null;
-            currentCameraState.senderSocketId = null;
-
-            emitRemoveFeed(slot);
-        }
-    }
-};
-
-const registerSenderHandlers = (socket) => {
-    socket.on('set_feed_id', handleSetFeedId.bind(null, socket));
-    socket.on('disconnect', handleSenderDisconnect.bind(null, socket));
-};
-
-const handleSenderInit = (socket, data, fn) => {
-    let success = true;
-    let message = '';
-    try {
-        const slotStr = data.slot;
-        if (isNaN(slotStr)) {
-            console.log('Error: Got socket connection with slot ' + slotStr + ' that cannot be parsed to a number');
-            throw new ValidationError('Slot ' + slotStr + ' cannot be parsed to number');
-        }
-
-        const slot = parseInt(slotStr);
-        if (slot < 0 || slot > cameraStates.length - 1) {
-            console.log('Error: Got socket connection with slot ' + slot + ' which is not in the list of slots');
-            throw new ValidationError('Slot ' + slot + ' is not in the list of slots');
-        }
-
-        const currentCameraState = cameraStates[slot];
-        if (!currentCameraState.slotActive) {
-            console.log('Error: Got socket connection for inactive slot ' + slot);
-            throw new ValidationError('Slot ' + slot + ' is not active');
-        }
-
-        const token = data.token;
-        if (currentCameraState.token !== token) {
-            console.log('Error: Got socket connecion with wrong token ' + token + ' for slot ' + slot);
-            throw new ValidationError('Invalid token');
-        }
-
-        console.log('Got sender socket connection on slot ' + slot);
-        
-        message = 'Socket authenticated';
-        socket.cameraSlot = slot;
-        socket.cameraSlotToken = data.token;
-
-        registerSenderHandlers(socket);
-    } catch (e) {
-        if (e instanceof ValidationError) {
-            success = false;
-            message = e.message;
-        } else {
-            throw e;
-        }
-    }
-
-    fn({ success, message });
-};
-
-const handleQueryState = (fn) => {
-    console.log('Got state query from socket');
-    let response = {};
-    for (let i = 0; i < cameraStates.length; i++) {
-        const cameraState = cameraStates[i];
-        if (cameraState.feedActive) {
-            response[i] = {
-                feedId: cameraState.feedId,
-                visibility: cameraState.visibility,
-                geometry: cameraState.geometry
-            };
-        }
-    }
-    fn(response);
-}
-
-io.on('connection', (socket) => {
-    socket.on('query_state', handleQueryState);
-
-    socket.on('sender_init', handleSenderInit.bind(null, socket));
-});
-
-const handleInternalCommand = (command, slot, params) => {
-    const currentCameraState = cameraStates[slot];
-    switch (command) {
-        case 'activate_slot':
-            if (currentCameraState.slotActive) {
-                console.log('Error: Tried to activate active slot ' + slot);
-                return;
-            }
-            if (params.length === 0) {
-                console.log('Error while activating slot ' + slot + ' - Got no token parameter');
-                return;
-            }
-            currentCameraState.token = params[0];
-            currentCameraState.slotActive = true;
-            break;
-        case 'deactivate_slot':
-            if (!currentCameraState.slotActive) {
-                console.log('Error: Tried to deactivate inactive slot ' + slot );
-                return;
-            }
-            console.log('Deactivating slot ' + slot);
-            emitRemoveFeed(slot);
-
-            currentCameraState.slotActive = false;
-            currentCameraState.token = null;
-            currentCameraState.feedActive = false;
-            currentCameraState.feedId = null;
-            currentCameraState.senderSocketId = null;
-            break;
-        case 'refresh_token':
-            if (!currentCameraState.slotActive) {
-                console.log('Error: Tried to refresh token for inactive slot ' + slot);
-                return;
-            }
-            if (params.length === 0) {
-                console.log('Error while refreshing token for slot ' + slot + ' - Got no token parameter');
-                console.log('Keeping old token');
-                return;
-            }
-            console.log('Refreshing token for slot ' + slot);
-            currentCameraState.token = params[0];
-            break;
-        default:
-            console.log('Error: handleInternalCommand got unknown command ' + command);
-            break;
-    }
-};
-
-const handleCommand = (line) => {
-    let emitCommand = false;
-
-    console.log('Got command from stdin:', line);
-    const params = line.split(' ');
-    const command = params.shift();
-    if (params.length === 0) {
-        console.log('Error: Got no slot to apply the command on');
-        return;
-    }
-    const slotStr = params.shift();
-    if (isNaN(slotStr)) {
-        console.log('Error: Could not parse slot ' + slotStr + ' to an integer');
-        return;
-    }
-    const slot = parseInt(slotStr);
-    console.log('command:', command);
-    console.log('slot:', slot);
-    console.log('params:', params);
-
-    if (slot < 0 || slot > cameraStates.length - 1) {
-        console.log(`Error: Got invalid slot number ${slot}. There are ${cameraStates.length} camera slots.`);
-        return;
-    }
-
-    const currentCameraState = cameraStates[slot];
-
-    if (visibilityCommands.includes(command)) {
-        currentCameraState.visibility = {
-            command,
-            params
-        };
-        emitCommand = true;
-    } else if (geometryCommands.includes(command)) {
-        currentCameraState.geometry = {
-            command,
-            params
-        };
-        emitCommand = true;
-    } else if (internalCommands.includes(command)) {
-        handleInternalCommand(command, slot, params);
-    } else {
-        console.log('Command "' + command + '" is not a valid command');
-        return;
-    }
-
-    console.log('new cameraState:', currentCameraState);
-    
-    if (currentCameraState.feedActive && emitCommand) {
-        io.emit('command', {
-            slot,
-            command,
-            params
-        });
-    }
-}
-
-rl.on('line', handleCommand);
-
-const cleanup = () => {
-    console.log('cleanup');
-    io.emit('remove_all_feeds');
-};
-
-const exitHandler = (options, exitCode) => {
-        if (options.cleanup) cleanup();
-        if (exitCode || exitCode === 0) console.log(exitCode);
-        if (options.exit) process.exit();
-}
-
-// do something when app is closing
-process.on('exit', exitHandler.bind(null, { cleanup:true }));
-
-// catches ctrl+c event
-process.on('SIGINT', exitHandler.bind(null, { exit:true }));
-
-// catches "kill pid" (for example: nodemon restart)
-process.on('SIGUSR1', exitHandler.bind(null, { exit:true }));
-process.on('SIGUSR2', exitHandler.bind(null, { exit:true }));
-
-// catches uncaught exceptions
-process.on('uncaughtException', exitHandler.bind(null, { exit:true }));
-
-// catches termination
-process.on('SIGTERM', exitHandler.bind(null, { exit:true }));
diff --git a/camera-server/src/models/camera-slot-state.ts b/camera-server/src/models/camera-slot-state.ts
new file mode 100644
index 0000000..9795178
--- /dev/null
+++ b/camera-server/src/models/camera-slot-state.ts
@@ -0,0 +1,22 @@
+export interface CommandDescriptor {
+    command: string;
+    params: string[];
+}
+
+type NullableString = string | null;
+
+export class CameraSlotState {
+    slotActive = false;
+    token: NullableString = null;
+    feedActive = false;
+    feedId: NullableString = null;
+    senderSocketId: NullableString = null;
+    visibility: CommandDescriptor = {
+        command: 'show',
+        params: []
+    };
+    geometry: CommandDescriptor = {
+        command: 'set_geometry_relative_to_canvas',
+        params: ['rb', '0', '0', '200', '200']
+    };
+}
\ No newline at end of file
diff --git a/camera-server/src/models/sender-socket.ts b/camera-server/src/models/sender-socket.ts
new file mode 100644
index 0000000..f468674
--- /dev/null
+++ b/camera-server/src/models/sender-socket.ts
@@ -0,0 +1,6 @@
+import { Socket } from 'socket.io';
+
+export interface SenderSocket extends Socket {
+    cameraSlot: number;
+    cameraSlotToken: string;
+};
diff --git a/camera-server/src/models/validation-error.ts b/camera-server/src/models/validation-error.ts
new file mode 100644
index 0000000..f822453
--- /dev/null
+++ b/camera-server/src/models/validation-error.ts
@@ -0,0 +1,5 @@
+export class ValidationError extends Error {
+    constructor(message: string) {
+        super(message);
+    }
+}
diff --git a/camera-server/src/server.ts b/camera-server/src/server.ts
new file mode 100644
index 0000000..0dae587
--- /dev/null
+++ b/camera-server/src/server.ts
@@ -0,0 +1,411 @@
+import { Server as SocketIOServer, Socket } from 'socket.io';
+
+import { mountCleanupLogic } from './util/cleanup';
+import { ValidationError } from './models/validation-error';
+import { CameraSlotState, CommandDescriptor } from './models/camera-slot-state';
+import { SenderSocket } from './models/sender-socket';
+
+const readline = require('readline');
+const rl = readline.createInterface({
+    input: process.stdin,
+    output: process.stdout,
+    terminal: false
+});
+
+let port = 5000;
+if (process.env.PORT) {
+    port = +process.env.PORT;
+    console.log('Using port ' + port + ' from PORT environment variable');
+} else {
+    console.log(
+        'Got no PORT environment variable - using default port ' + port
+    );
+}
+
+let cameraSlots = 4;
+if (process.env.CAMERA_SLOTS) {
+    cameraSlots = +process.env.CAMERA_SLOTS;
+    console.log(
+        'Using camera count ' +
+            cameraSlots +
+            ' from CAMERA_SLOTS environment variable'
+    );
+} else {
+    console.log(
+        'Got no CAMERA_SLOTS environment variable - using default count of ' +
+            cameraSlots
+    );
+}
+
+const io = new SocketIOServer(port);
+
+const visibilityCommands = ['hide', 'show'];
+const geometryCommands = [
+    'set_geometry_relative_to_window',
+    'set_geometry_relative_to_canvas'
+];
+const internalCommands = ['activate_slot', 'deactivate_slot', 'refresh_token'];
+
+let cameraSlotState: CameraSlotState[] = [];
+for (let i = 0; i < cameraSlots; i++) {
+    cameraSlotState.push(new CameraSlotState());
+}
+
+const emitNewFeed = (slot: number) => {
+    const cameraState = cameraSlotState[slot];
+    io.emit('new_feed', {
+        slot,
+        feedId: cameraState.feedId,
+        visibility: cameraState.visibility,
+        geometry: cameraState.geometry
+    });
+};
+
+const emitRemoveFeed = (slot: number) => {
+    io.emit('remove_feed', { slot });
+};
+
+const handleSetFeedId = (
+    socket: SenderSocket,
+    data: null | { feedId?: string },
+    fn: Function
+) => {
+    let success = true;
+    let message = '';
+
+    try {
+        const slot = socket.cameraSlot;
+        const currentCameraState = cameraSlotState[slot];
+
+        if (currentCameraState.token !== socket.cameraSlotToken) {
+            console.log(
+                'Error: Got set_feed_id event for slot ' +
+                    slot +
+                    ' with an old token'
+            );
+            throw new ValidationError(
+                'The provided token is not valid anymore - the feed is not transmitted'
+            );
+        }
+
+        if (currentCameraState.feedActive) {
+            console.log(
+                'Error: Got set_feed_id event for slot ' +
+                    slot +
+                    ' which already has an active feed'
+            );
+            throw new ValidationError(
+                'There is already somebody using this slot'
+            );
+        }
+
+        if (data == null) {
+            console.log(
+                'Error: Got set_feed_id event for slot ' +
+                    slot +
+                    ' without data'
+            );
+            throw new ValidationError(
+                'Could not get feed id because no data was provided'
+            );
+        }
+
+        const feedId = data.feedId;
+        if (feedId == null) {
+            console.log(
+                'Error: Got set_feed_id event without a feed id on slot ' + slot
+            );
+            throw new ValidationError('No feed id was provided');
+        }
+
+        console.log('Setting feed id of slot ' + slot + ' to ' + feedId);
+        message = 'Successfully set feed id - you are now using this slot';
+
+        currentCameraState.feedActive = true;
+        currentCameraState.feedId = feedId;
+        currentCameraState.senderSocketId = socket.id;
+
+        emitNewFeed(slot);
+    } catch (e) {
+        if (e instanceof ValidationError) {
+            success = false;
+            message = e.message;
+        } else {
+            throw e;
+        }
+    }
+
+    fn({ success, message });
+};
+
+const handleSenderDisconnect = (socket: SenderSocket, _: string) => {
+    const slot = socket.cameraSlot;
+    if (slot != null) {
+        const currentCameraState = cameraSlotState[slot];
+        if (
+            currentCameraState.feedActive &&
+            socket.id === currentCameraState.senderSocketId
+        ) {
+            console.log(
+                'Sender on slot ' + slot + ' disconnected - Clearing slot'
+            );
+            currentCameraState.feedActive = false;
+            currentCameraState.feedId = null;
+            currentCameraState.senderSocketId = null;
+
+            emitRemoveFeed(slot);
+        }
+    }
+};
+
+const registerSenderHandlers = (socket: SenderSocket) => {
+    socket.on('set_feed_id', handleSetFeedId.bind(null, socket));
+    socket.on('disconnect', handleSenderDisconnect.bind(null, socket));
+};
+
+const handleSenderInit = (
+    socket: SenderSocket,
+    data: null | { slot?: string; token?: string },
+    fn: Function
+) => {
+    let success = true;
+    let message = '';
+    try {
+        if (data == null) {
+            console.log('Error: Got socket connection without data');
+            throw new ValidationError('No data provided');
+        }
+
+        const slotStr = data.slot;
+        if (slotStr == null) {
+            console.log('Error: Got socket connection without a slot');
+            throw new ValidationError('No slot provided');
+        }
+
+        const slot = parseInt(slotStr);
+        if (isNaN(slot)) {
+            console.log(
+                'Error: Got socket connection with slot ' +
+                    slotStr +
+                    ' that cannot be parsed to a number'
+            );
+            throw new ValidationError(
+                'Slot ' + slotStr + ' cannot be parsed to number'
+            );
+        }
+        if (slot < 0 || slot > cameraSlotState.length - 1) {
+            console.log(
+                'Error: Got socket connection with slot ' +
+                    slot +
+                    ' which is not in the list of slots'
+            );
+            throw new ValidationError(
+                'Slot ' + slot + ' is not in the list of slots'
+            );
+        }
+
+        const slotState = cameraSlotState[slot];
+        if (!slotState.slotActive) {
+            console.log(
+                'Error: Got socket connection for inactive slot ' + slot
+            );
+            throw new ValidationError('Slot ' + slot + ' is not active');
+        }
+
+        const token = data.token;
+        if (token == null) {
+            console.log('Error: Got socket connection without token');
+            throw new ValidationError('No token provided');
+        }
+        if (slotState.token !== token) {
+            console.log(
+                'Error: Got socket connecion with wrong token ' +
+                    token +
+                    ' for slot ' +
+                    slot
+            );
+            throw new ValidationError('Invalid token');
+        }
+
+        console.log('Got sender socket connection on slot ' + slot);
+
+        message = 'Socket authenticated';
+        socket.cameraSlot = slot;
+        socket.cameraSlotToken = token;
+
+        registerSenderHandlers(socket);
+    } catch (e) {
+        if (e instanceof ValidationError) {
+            success = false;
+            message = e.message;
+        } else {
+            throw e;
+        }
+    }
+
+    fn({ success, message });
+};
+
+const handleQueryState = (fn: Function) => {
+    console.log('Got state query from socket');
+    let response: {
+        [index: number]: {
+            feedId: string | null;
+            visibility: CommandDescriptor;
+            geometry: CommandDescriptor;
+        };
+    } = {};
+    for (let i = 0; i < cameraSlotState.length; i++) {
+        const cameraState = cameraSlotState[i];
+        if (cameraState.feedActive) {
+            response[i] = {
+                feedId: cameraState.feedId,
+                visibility: cameraState.visibility,
+                geometry: cameraState.geometry
+            };
+        }
+    }
+    fn(response);
+};
+
+io.on('connection', (socket: Socket) => {
+    socket.on('query_state', handleQueryState);
+
+    socket.on('sender_init', handleSenderInit.bind(null, socket));
+});
+
+const handleInternalCommand = (
+    command: string,
+    slot: number,
+    params: string[]
+) => {
+    const currentCameraState = cameraSlotState[slot];
+    switch (command) {
+        case 'activate_slot':
+            if (currentCameraState.slotActive) {
+                console.log('Error: Tried to activate active slot ' + slot);
+                return;
+            }
+            if (params.length === 0) {
+                console.log(
+                    'Error while activating slot ' +
+                        slot +
+                        ' - Got no token parameter'
+                );
+                return;
+            }
+            currentCameraState.token = params[0];
+            currentCameraState.slotActive = true;
+            break;
+        case 'deactivate_slot':
+            if (!currentCameraState.slotActive) {
+                console.log('Error: Tried to deactivate inactive slot ' + slot);
+                return;
+            }
+            console.log('Deactivating slot ' + slot);
+            emitRemoveFeed(slot);
+
+            currentCameraState.slotActive = false;
+            currentCameraState.token = null;
+            currentCameraState.feedActive = false;
+            currentCameraState.feedId = null;
+            currentCameraState.senderSocketId = null;
+            break;
+        case 'refresh_token':
+            if (!currentCameraState.slotActive) {
+                console.log(
+                    'Error: Tried to refresh token for inactive slot ' + slot
+                );
+                return;
+            }
+            if (params.length === 0) {
+                console.log(
+                    'Error while refreshing token for slot ' +
+                        slot +
+                        ' - Got no token parameter'
+                );
+                console.log('Keeping old token');
+                return;
+            }
+            console.log('Refreshing token for slot ' + slot);
+            currentCameraState.token = params[0];
+            break;
+        default:
+            console.log(
+                'Error: handleInternalCommand got unknown command ' + command
+            );
+            break;
+    }
+};
+
+const handleCommand = (line: string) => {
+    let emitCommand = false;
+
+    console.log('Got command from stdin:', line);
+    const params = line.split(' ');
+
+    const command = params.shift();
+    if (command == null) {
+        console.log('Error: Got malformed line with no command');
+        return;
+    }
+
+    const slotStr = params.shift();
+    if (slotStr == null) {
+        console.log('Error: Got no slot to apply the command on');
+        return;
+    }
+
+    const slot = parseInt(slotStr);
+    if (isNaN(slot)) {
+        console.log(
+            'Error: Could not parse slot ' + slotStr + ' to an integer'
+        );
+        return;
+    }
+    if (slot < 0 || slot > cameraSlotState.length - 1) {
+        console.log(
+            `Error: Got invalid slot number ${slot}. There are ${cameraSlotState.length} camera slots.`
+        );
+        return;
+    }
+
+    console.log('command:', command);
+    console.log('slot:', slot);
+    console.log('params:', params);
+
+    const currentCameraState = cameraSlotState[slot];
+
+    if (visibilityCommands.includes(command)) {
+        currentCameraState.visibility = {
+            command,
+            params
+        };
+        emitCommand = true;
+    } else if (geometryCommands.includes(command)) {
+        currentCameraState.geometry = {
+            command,
+            params
+        };
+        emitCommand = true;
+    } else if (internalCommands.includes(command)) {
+        handleInternalCommand(command, slot, params);
+    } else {
+        console.log('Command "' + command + '" is not a valid command');
+        return;
+    }
+
+    console.log('new cameraState:', currentCameraState);
+
+    if (currentCameraState.feedActive && emitCommand) {
+        io.emit('command', {
+            slot,
+            command,
+            params
+        });
+    }
+};
+
+rl.on('line', handleCommand);
+
+mountCleanupLogic(io);
diff --git a/camera-server/src/util/cleanup.ts b/camera-server/src/util/cleanup.ts
new file mode 100644
index 0000000..7154e5c
--- /dev/null
+++ b/camera-server/src/util/cleanup.ts
@@ -0,0 +1,35 @@
+import { Server as SocketIOServer } from 'socket.io';
+
+interface ExitHandlerOptions {
+    cleanup?: boolean;
+    exit?: boolean;
+}
+
+export const mountCleanupLogic = (io: SocketIOServer) => {
+    const cleanup = () => {
+        console.log('cleanup');
+        io.emit('remove_all_feeds');
+    };
+
+    const exitHandler = (options: ExitHandlerOptions, exitCode: number) => {
+            if (options.cleanup) cleanup();
+            if (exitCode || exitCode === 0) console.log(exitCode);
+            if (options.exit) process.exit();
+    }
+
+    // do something when app is closing
+    process.on('exit', exitHandler.bind(null, { cleanup:true }));
+
+    // catches ctrl+c event
+    process.on('SIGINT', exitHandler.bind(null, { exit:true }));
+
+    // catches "kill pid" (for example: nodemon restart)
+    process.on('SIGUSR1', exitHandler.bind(null, { exit:true }));
+    process.on('SIGUSR2', exitHandler.bind(null, { exit:true }));
+
+    // catches uncaught exceptions
+    process.on('uncaughtException', exitHandler.bind(null, { exit:true }));
+
+    // catches termination
+    process.on('SIGTERM', exitHandler.bind(null, { exit:true }));
+}
\ No newline at end of file
diff --git a/camera-server/test-server.js b/camera-server/test-server.js
deleted file mode 100644
index 7424f6b..0000000
--- a/camera-server/test-server.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const readline = require('readline');
-const rl = readline.createInterface({
-    input: process.stdin,
-    output: process.stdout,
-    terminal: false
-});
-
-const io = require('socket.io')(5005);
-
-io.on('connection', (socket) => {
-    socket.emit('command', { message: 'Hello, world!' });
-
-    socket.on('query', (param1) => {
-        console.log('query:', param1);
-    });
-
-    rl.on('line', function(line) {
-        console.log('Got command from stdin:', line);
-        const params = line.split(' ');
-        const command = params.shift();
-        console.log('command:', command);
-        console.log('params:', params);
-        socket.emit('command', {
-            command,
-            params
-        });
-    });
-});
diff --git a/camera-server/tsconfig.json b/camera-server/tsconfig.json
new file mode 100644
index 0000000..bcced0b
--- /dev/null
+++ b/camera-server/tsconfig.json
@@ -0,0 +1,36 @@
+{
+  "compilerOptions": {
+    "target": "es6",
+    "module": "commonjs",
+    "lib": [
+      "dom",
+      "es6",
+      "es2017",
+      "esnext.asynciterable"
+    ],
+    "sourceMap": true,
+    "outDir": "./dist",
+    "moduleResolution": "node",
+    "removeComments": true,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "noImplicitThis": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "resolveJsonModule": true,
+    "baseUrl": "."
+  },
+  "exclude": [
+    "node_modules"
+  ],
+  "include": [
+    "./src/**/*.ts"
+  ]
+}
\ No newline at end of file
-- 
GitLab