From c11ecfb40021d108ee1d035637fac08976948095 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 15 Oct 2019 18:08:57 -0400 Subject: Room notifications added --- src/server/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/server/index.ts') diff --git a/src/server/index.ts b/src/server/index.ts index 690836fff..6b884d099 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -776,6 +776,9 @@ server.on("connection", function (socket: Socket) { Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids)); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); + Utils.AddServerHandler(socket, MessageStore.NotifyRoommates, message => HandleRoommateNotification(socket, message)); + + }); async function deleteFields() { @@ -837,6 +840,11 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any } } +function HandleRoommateNotification(socket: Socket, message: String) { + socket.broadcast.emit('message', message); + +} + const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json"); const EndpointHandlerMap = new Map([ -- cgit v1.2.3-70-g09d2 From f5fc9d12c2843d6d9241045150983d30e4eaa4d1 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Sun, 20 Oct 2019 17:00:06 -0400 Subject: Server side pretty much done for 2 person call --- src/Utils.ts | 8 +++++++- src/server/Message.ts | 10 +++++++++- src/server/index.ts | 46 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 5 deletions(-) (limited to 'src/server/index.ts') diff --git a/src/Utils.ts b/src/Utils.ts index 4b892aa70..b481b3b47 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,6 +1,6 @@ import v4 = require('uuid/v4'); import v5 = require("uuid/v5"); -import { Socket } from 'socket.io'; +import { Socket, Room } from 'socket.io'; import { Message } from './server/Message'; import { RouteStore } from './server/RouteStore'; @@ -211,6 +211,12 @@ export namespace Utils { handler([arg, loggingCallback('S sending', fn, message.Name)]); }); } + export type RoomHandler = (socket: Socket, room: string) => any; + export type UsedSockets = Socket | SocketIOClient.Socket; + export type RoomMessage = "create or join" | "created" | "joined"; + export function AddRoomHandler(socket: Socket, message: RoomMessage, handler: RoomHandler) { + socket.on(message, room => handler(socket, room)); + } } export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } { diff --git a/src/server/Message.ts b/src/server/Message.ts index 7b72b693e..60ffa215f 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -42,6 +42,11 @@ export interface Diff extends Reference { readonly diff: any; } +export interface RoomMessage { + readonly message: string; + readonly room: string; +} + export namespace MessageStore { export const Foo = new Message("Foo"); export const Bar = new Message("Bar"); @@ -58,5 +63,8 @@ export namespace MessageStore { export const YoutubeApiQuery = new Message("Youtube Api Query"); export const DeleteField = new Message("Delete field"); export const DeleteFields = new Message("Delete fields"); - export const NotifyRoommates = new Message("Notify Roommates"); + export const NotifyRoommates = new Message("Notify Roommates"); + export const HangUpCall = new Message("bye"); + + } diff --git a/src/server/index.ts b/src/server/index.ts index 6b884d099..b37822eec 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -24,7 +24,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo import { DashUserModel } from './authentication/models/user_model'; import { Client } from './Client'; import { Database } from './database'; -import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput } from "./Message"; +import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput, RoomMessage } from "./Message"; import { RouteStore } from './RouteStore'; import v4 = require('uuid/v4'); const app = express(); @@ -777,6 +777,10 @@ server.on("connection", function (socket: Socket) { Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); Utils.AddServerHandler(socket, MessageStore.NotifyRoommates, message => HandleRoommateNotification(socket, message)); + Utils.AddServerHandler(socket, MessageStore.HangUpCall, message => HandleHangUp(socket, message)); + Utils.AddRoomHandler(socket, "create or join", HandleCreateOrJoin); + + }); @@ -840,9 +844,45 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any } } -function HandleRoommateNotification(socket: Socket, message: String) { - socket.broadcast.emit('message', message); +function HandleRoommateNotification(socket: Socket, message: RoomMessage) { + //socket.broadcast.emit('message', message); + server.sockets.in(message.room).emit('message', message.message); + +} + +function HandleCreateOrJoin(socket: io.Socket, room: string) { + console.log("Received request to create or join room " + room); + + + let clientsInRoom = server.sockets.adapter.rooms[room]; + let numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; + console.log('Room ' + room + ' now has ' + numClients + ' client(s)'); + + + if (numClients === 0) { + socket.join(room); + console.log('Client ID ' + socket.id + ' created room ' + room); + socket.emit('created', room, socket.id); + + } else if (numClients === 1) { + console.log('Client ID ' + socket.id + ' joined room ' + room); + server.sockets.in(room).emit('join', room); + socket.join(room); + socket.emit('joined', room, socket.id); + server.sockets.in(room).emit('ready'); + + } else { + socket.emit('full', room); + } + + + + + +} +function HandleHangUp(socket: io.Socket, message: string) { + console.log("Receive bye from someone"); } const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json"); -- cgit v1.2.3-70-g09d2 From 2cf3ed4cd22726fdbe7f587605457271482c3a28 Mon Sep 17 00:00:00 2001 From: andrewdkim Date: Tue, 5 Nov 2019 18:31:52 -0500 Subject: add a prompt and figure out message why not sending --- src/client/views/webcam/DashWebRTC.ts | 44 ++++++++--------------------- src/client/views/webcam/DashWebRTCVideo.tsx | 16 ++++++++--- src/server/index.ts | 4 +-- 3 files changed, 25 insertions(+), 39 deletions(-) (limited to 'src/server/index.ts') diff --git a/src/client/views/webcam/DashWebRTC.ts b/src/client/views/webcam/DashWebRTC.ts index 801fd782d..bd0bd7f9a 100644 --- a/src/client/views/webcam/DashWebRTC.ts +++ b/src/client/views/webcam/DashWebRTC.ts @@ -1,7 +1,9 @@ import { DocServer } from '../../DocServer'; - +/** + * This namespace will have the code required to have functionality code for the usage of webRTC. + */ export namespace DashWebRTC { @@ -130,6 +132,14 @@ export namespace DashWebRTC { remoteVideo = remoteVideo; } + export function setLocalVideoObject(localVideoRef: HTMLVideoElement) { + localVideo = localVideoRef; + } + + export function setRemoteVideoObject(remoteVideoRef: HTMLVideoElement) { + remoteVideo = remoteVideoRef; + } + @@ -286,36 +296,4 @@ export namespace DashWebRTC { } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } \ No newline at end of file diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 62efd9730..503b71569 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -21,7 +21,9 @@ const offerOptions = { offerToReceiveVideo: 1, }; - +/** + * This models the component that will be rendered, that can be used as a doc that will reflect the video cams. + */ @observer export class DashWebRTCVideo extends React.Component { @@ -37,7 +39,7 @@ export class DashWebRTCVideo extends React.Component - - + + {/* */} diff --git a/src/server/index.ts b/src/server/index.ts index e33fd4b71..77f048a1e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -983,8 +983,8 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any } function HandleRoommateNotification(socket: Socket, message: RoomMessage) { - //socket.broadcast.emit('message', message); - server.sockets.in(message.room).emit('message', message.message); + socket.broadcast.emit('message', message); + //server.sockets.in(message.room).emit('message', message.message); } -- cgit v1.2.3-70-g09d2 From 40c40d377ec169b44e1a9691129e5c7a6b0b10e4 Mon Sep 17 00:00:00 2001 From: andrewdkim Date: Sun, 10 Nov 2019 16:23:00 -0500 Subject: messages changed to only room instead of server broadcast! --- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/webcam/DashWebRTC.ts | 8 +++++--- src/server/index.ts | 5 +++-- 4 files changed, 10 insertions(+), 7 deletions(-) (limited to 'src/server/index.ts') diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a035bdc3d..c2c402819 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -84,7 +84,7 @@ export class CollectionFreeFormDocumentView extends DocComponent this.dataProvider ? this.dataProvider.height : this.panelHeight(); render() { - trace(); + //trace(); return
(Docu render() { if (!this.props.Document) return (null); - trace(); + // trace(); const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined; const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; diff --git a/src/client/views/webcam/DashWebRTC.ts b/src/client/views/webcam/DashWebRTC.ts index 846de4115..c61469cb8 100644 --- a/src/client/views/webcam/DashWebRTC.ts +++ b/src/client/views/webcam/DashWebRTC.ts @@ -19,6 +19,7 @@ export namespace DashWebRTC { let turnReady; let localVideo: HTMLVideoElement; let remoteVideo: HTMLVideoElement; + let curRoom: string = ""; let pcConfig = { @@ -35,6 +36,8 @@ export namespace DashWebRTC { export function init(room: string) { + curRoom = room; + if (room !== '') { DocServer._socket.emit('create or join', room); console.log('Attempted to create or join room', room); @@ -73,7 +76,6 @@ export namespace DashWebRTC { if (message.message === 'got user media') { maybeStart(); } else if (message.message.type === 'offer') { - console.log("I have entered here bro!!!"); if (!isInitiator && !isStarted) { maybeStart(); } @@ -82,7 +84,7 @@ export namespace DashWebRTC { } else if (message.message.type === 'answer' && isStarted) { pc.setRemoteDescription(new RTCSessionDescription(message.message)); } else if (message.message.type === 'candidate' && isStarted) { - var candidate = new RTCIceCandidate({ + let candidate = new RTCIceCandidate({ sdpMLineIndex: message.message.label, candidate: message.message.candidate }); @@ -123,7 +125,7 @@ export namespace DashWebRTC { function sendMessage(message: any) { console.log('Client sending message: ', message); - Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: message, room: "" }); + Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: message, room: curRoom }); //DocServer._socket.emit('message', message); } diff --git a/src/server/index.ts b/src/server/index.ts index 77f048a1e..93d257721 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -983,8 +983,9 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any } function HandleRoommateNotification(socket: Socket, message: RoomMessage) { - socket.broadcast.emit('message', message); - //server.sockets.in(message.room).emit('message', message.message); + //socket.broadcast.emit('message', message); + console.log("The room that sent this: ", message.room, " and message is : ", message.message); + server.sockets.in(message.room).emit('message', message); } -- cgit v1.2.3-70-g09d2 From 8e8533a7bab97fe9be8c8fb862a7e809da5f137d Mon Sep 17 00:00:00 2001 From: Mohammad Amoush <47069173+mamoush34@users.noreply.github.com> Date: Tue, 21 Jan 2020 18:46:32 -0500 Subject: Moved everything into component Need help with server side --- src/client/views/webcam/DashWebRTC.ts | 596 ++++++++++++++-------------- src/client/views/webcam/DashWebRTCVideo.tsx | 475 +++++++++++----------- src/server/index.ts | 90 +++-- 3 files changed, 596 insertions(+), 565 deletions(-) (limited to 'src/server/index.ts') diff --git a/src/client/views/webcam/DashWebRTC.ts b/src/client/views/webcam/DashWebRTC.ts index 7d798754f..ef5ecf0fc 100644 --- a/src/client/views/webcam/DashWebRTC.ts +++ b/src/client/views/webcam/DashWebRTC.ts @@ -1,314 +1,314 @@ -import { DocServer } from '../../DocServer'; -import { Utils } from '../../../Utils'; -import { MessageStore } from '../../../server/Message'; - - - -/** - * This namespace will have the code required to have functionality code for the usage of webRTC. - */ -export class DashWebRTC { - - - private isChannelReady = false; - private isInitiator = false; - private isStarted = false; - localStream: MediaStream | undefined; - private pc: any; - remoteStream: MediaStream | undefined; - private turnReady: boolean | undefined; - localVideo: HTMLVideoElement | undefined; - remoteVideo: HTMLVideoElement | undefined; - curRoom: string = ""; - - - private pcConfig: any; - private sdpConstraints: any; - - constructor() { - this.pcConfig = { - 'iceServers': [{ - 'urls': 'stun:stun.l.google.com:19302' - }] - }; +// import { DocServer } from '../../DocServer'; +// import { Utils } from '../../../Utils'; +// import { MessageStore } from '../../../server/Message'; + + + +// /** +// * This namespace will have the code required to have functionality code for the usage of webRTC. +// */ +// export class DashWebRTC { + + +// private isChannelReady = false; +// private isInitiator = false; +// private isStarted = false; +// localStream: MediaStream | undefined; +// private pc: any; +// remoteStream: MediaStream | undefined; +// private turnReady: boolean | undefined; +// localVideo: HTMLVideoElement | undefined; +// remoteVideo: HTMLVideoElement | undefined; +// curRoom: string = ""; + + +// private pcConfig: any; +// private sdpConstraints: any; + +// constructor() { +// this.pcConfig = { +// 'iceServers': [{ +// 'urls': 'stun:stun.l.google.com:19302' +// }] +// }; - // Set up audio and video regardless of what devices are present. - this.sdpConstraints = { - offerToReceiveAudio: true, - offerToReceiveVideo: true - }; - } +// // Set up audio and video regardless of what devices are present. +// this.sdpConstraints = { +// offerToReceiveAudio: true, +// offerToReceiveVideo: true +// }; +// } - init(room: string) { +// init(room: string) { - this.curRoom = room; - let self = this; +// this.curRoom = room; +// let self = this; - if (room !== '') { - DocServer._socket.emit('create or join', room); - console.log('Attempted to create or join room', room); +// if (room !== '') { +// DocServer._socket.emit('create or join', room); +// console.log('Attempted to create or join room', room); - } +// } - DocServer._socket.on('created', function (room: string) { - console.log('Created room ' + room); - self.isInitiator = true; - }); +// DocServer._socket.on('created', function (room: string) { +// console.log('Created room ' + room); +// self.isInitiator = true; +// }); - DocServer._socket.on('full', function (room: string) { - console.log('Room ' + room + ' is full'); - }); +// DocServer._socket.on('full', function (room: string) { +// console.log('Room ' + room + ' is full'); +// }); - DocServer._socket.on('join', function (room: string) { - console.log('Another peer made a request to join room ' + room); - console.log('This peer is the initiator of room ' + room + '!'); - self.isChannelReady = true; - }); +// DocServer._socket.on('join', function (room: string) { +// console.log('Another peer made a request to join room ' + room); +// console.log('This peer is the initiator of room ' + room + '!'); +// self.isChannelReady = true; +// }); - DocServer._socket.on('joined', function (room: string) { - console.log('joined: ' + room); - self.isChannelReady = true; - }); +// DocServer._socket.on('joined', function (room: string) { +// console.log('joined: ' + room); +// self.isChannelReady = true; +// }); - DocServer._socket.on('log', function (array: any) { - console.log.apply(console, array); - }); +// DocServer._socket.on('log', function (array: any) { +// console.log.apply(console, array); +// }); - // This client receives a message - DocServer._socket.on('message', function (message: any) { - console.log('Client received message:', message); - if (message.message === 'got user media') { - self.maybeStart(); - } else if (message.message.type === 'offer') { - if (!self.isInitiator && !self.isStarted) { - self.maybeStart(); - } - self.pc.setRemoteDescription(new RTCSessionDescription(message.message)); - self.doAnswer(); - } else if (message.message.type === 'answer' && self.isStarted) { - self.pc.setRemoteDescription(new RTCSessionDescription(message.message)); - } else if (message.message.type === 'candidate' && self.isStarted) { - let candidate = new RTCIceCandidate({ - sdpMLineIndex: message.message.label, - candidate: message.message.candidate - }); - self.pc.addIceCandidate(candidate); - } else if (message === 'bye' && self.isStarted) { - self.handleRemoteHangup(); - } - }); +// // This client receives a message +// DocServer._socket.on('message', function (message: any) { +// console.log('Client received message:', message); +// if (message.message === 'got user media') { +// self.maybeStart(); +// } else if (message.message.type === 'offer') { +// if (!self.isInitiator && !self.isStarted) { +// self.maybeStart(); +// } +// self.pc.setRemoteDescription(new RTCSessionDescription(message.message)); +// self.doAnswer(); +// } else if (message.message.type === 'answer' && self.isStarted) { +// self.pc.setRemoteDescription(new RTCSessionDescription(message.message)); +// } else if (message.message.type === 'candidate' && self.isStarted) { +// let candidate = new RTCIceCandidate({ +// sdpMLineIndex: message.message.label, +// candidate: message.message.candidate +// }); +// self.pc.addIceCandidate(candidate); +// } else if (message === 'bye' && self.isStarted) { +// self.handleRemoteHangup(); +// } +// }); - navigator.mediaDevices.getUserMedia({ - audio: false, - video: true - }) - .then(this.gotStream) - .catch(function (e) { - alert('getUserMedia() error: ' + e.name); - }); - - //Trying this one out!!! - console.log('Getting user media with constraints', this.constraints); - - if (location.hostname !== 'localhost') { - this.requestTurn( - 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913' - ); - } - - - } - - - sendMessage(message: any) { - console.log('Client sending message: ', message); - Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: message, room: this.curRoom }); - //DocServer._socket.emit('message', message); - } - - - - - - setVideoObjects(localVideo: HTMLVideoElement, remoteVideo: HTMLVideoElement) { - localVideo = localVideo; - remoteVideo = remoteVideo; - } - - setLocalVideoObject(localVideoRef: HTMLVideoElement) { - this.localVideo = localVideoRef; - } - - setRemoteVideoObject(remoteVideoRef: HTMLVideoElement) { - this.remoteVideo = remoteVideoRef; - } - - - - - gotStream(stream: any) { - console.log('Adding local stream.'); - this.localStream = stream; - this.localVideo!.srcObject = stream; - this.sendMessage('got user media'); - if (this.isInitiator) { - this.maybeStart(); - } - } - - constraints = { - video: true, - audio: true - }; - - - - - - maybeStart() { - console.log('>>>>>>> maybeStart() ', this.isStarted, this.localStream, this.isChannelReady); - if (!this.isStarted && typeof this.localStream !== 'undefined' && this.isChannelReady) { - console.log('>>>>>> creating peer connection'); - this.createPeerConnection(); - this.pc.addStream(this.localStream); - this.isStarted = true; - console.log('isInitiator', this.isInitiator); - if (this.isInitiator) { - this.doCall(); - } - } - } - - - // //this will need to be changed to our version of hangUp - // window.onbeforeunload = function () { - // sendMessage('bye'); - // }; - - createPeerConnection() { - try { - this.pc = new RTCPeerConnection(undefined); - this.pc.onicecandidate = this.handleIceCandidate; - this.pc.onaddstream = this.handleRemoteStreamAdded; - this.pc.onremovestream = this.handleRemoteStreamRemoved; - console.log('Created RTCPeerConnnection'); - } catch (e) { - console.log('Failed to create PeerConnection, exception: ' + e.message); - alert('Cannot create RTCPeerConnection object.'); - return; - } - } - - handleIceCandidate(event: any) { - console.log('icecandidate event: ', event); - if (event.candidate) { - this.sendMessage({ - type: 'candidate', - label: event.candidate.sdpMLineIndex, - id: event.candidate.sdpMid, - candidate: event.candidate.candidate - }); - } else { - console.log('End of candidates.'); - } - } - - handleCreateOfferError(event: any) { - console.log('createOffer() error: ', event); - } - - doCall() { - console.log('Sending offer to peer'); - this.pc.createOffer(this.setLocalAndSendMessage, this.handleCreateOfferError); - } - - doAnswer() { - console.log('Sending answer to peer.'); - this.pc.createAnswer().then( - this.setLocalAndSendMessage, - this.onCreateSessionDescriptionError - ); - } - - setLocalAndSendMessage(sessionDescription: any) { - this.pc.setLocalDescription(sessionDescription); - console.log('setLocalAndSendMessage sending message', sessionDescription); - this.sendMessage(sessionDescription); - } - - onCreateSessionDescriptionError(error: any) { - console.log('Failed to create session description: ' + error.toString()); - } - - - requestTurn(turnURL: any) { - var turnExists = false; - let self = this; - for (var i in this.pcConfig.iceServers) { - if (this.pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') { - turnExists = true; - this.turnReady = true; - break; - } - } - if (!turnExists) { - console.log('Getting TURN server from ', turnURL); - // No TURN server. Get one from computeengineondemand.appspot.com: - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4 && xhr.status === 200) { - var turnServer = JSON.parse(xhr.responseText); - console.log('Got TURN server: ', turnServer); - self.pcConfig.iceServers.push({ - 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn, - //'credential': turnServer.password - }); - self.turnReady = true; - } - }; - xhr.open('GET', turnURL, true); - xhr.send(); - } - } - - handleRemoteStreamAdded(event: MediaStreamEvent) { - console.log('Remote stream added.'); - this.remoteStream = event.stream!; - this.remoteVideo!.srcObject = this.remoteStream; - } - - handleRemoteStreamRemoved(event: MediaStreamEvent) { - console.log('Remote stream removed. Event: ', event); - } - - hangup() { - console.log('Hanging up.'); - if (this.pc) { - stop(); - this.sendMessage('bye'); - } - - if (this.localStream) { - this.localStream.getTracks().forEach(track => track.stop()); - } - - } - - handleRemoteHangup() { - console.log('Session terminated.'); - stop(); - this.isInitiator = false; - } - - stop() { - this.isStarted = false; - this.pc.close(); - this.pc = null; - } - - -} \ No newline at end of file +// navigator.mediaDevices.getUserMedia({ +// audio: false, +// video: true +// }) +// .then(this.gotStream) +// .catch(function (e) { +// alert('getUserMedia() error: ' + e.name); +// }); + +// //Trying this one out!!! +// console.log('Getting user media with constraints', this.constraints); + +// if (location.hostname !== 'localhost') { +// this.requestTurn( +// 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913' +// ); +// } + + +// } + + +// sendMessage(message: any) { +// console.log('Client sending message: ', message); +// Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: message, room: this.curRoom }); +// //DocServer._socket.emit('message', message); +// } + + + + + +// setVideoObjects(localVideo: HTMLVideoElement, remoteVideo: HTMLVideoElement) { +// this.localVideo = localVideo; +// this.remoteVideo = remoteVideo; +// } + +// setLocalVideoObject(localVideoRef: HTMLVideoElement) { +// this.localVideo = localVideoRef; +// } + +// setRemoteVideoObject(remoteVideoRef: HTMLVideoElement) { +// this.remoteVideo = remoteVideoRef; +// } + + + + +// gotStream(stream: any) { +// console.log('Adding local stream.'); +// this.localStream = stream; +// this.localVideo!.srcObject = stream; +// this.sendMessage('got user media'); +// if (this.isInitiator) { +// this.maybeStart(); +// } +// } + +// constraints = { +// video: true, +// audio: true +// }; + + + + + +// maybeStart() { +// console.log('>>>>>>> maybeStart() ', this.isStarted, this.localStream, this.isChannelReady); +// if (!this.isStarted && typeof this.localStream !== 'undefined' && this.isChannelReady) { +// console.log('>>>>>> creating peer connection'); +// this.createPeerConnection(); +// this.pc.addStream(this.localStream); +// this.isStarted = true; +// console.log('isInitiator', this.isInitiator); +// if (this.isInitiator) { +// this.doCall(); +// } +// } +// } + + +// // //this will need to be changed to our version of hangUp +// // window.onbeforeunload = function () { +// // sendMessage('bye'); +// // }; + +// createPeerConnection() { +// try { +// this.pc = new RTCPeerConnection(undefined); +// this.pc.onicecandidate = this.handleIceCandidate; +// this.pc.onaddstream = this.handleRemoteStreamAdded; +// this.pc.onremovestream = this.handleRemoteStreamRemoved; +// console.log('Created RTCPeerConnnection'); +// } catch (e) { +// console.log('Failed to create PeerConnection, exception: ' + e.message); +// alert('Cannot create RTCPeerConnection object.'); +// return; +// } +// } + +// handleIceCandidate(event: any) { +// console.log('icecandidate event: ', event); +// if (event.candidate) { +// this.sendMessage({ +// type: 'candidate', +// label: event.candidate.sdpMLineIndex, +// id: event.candidate.sdpMid, +// candidate: event.candidate.candidate +// }); +// } else { +// console.log('End of candidates.'); +// } +// } + +// handleCreateOfferError(event: any) { +// console.log('createOffer() error: ', event); +// } + +// doCall() { +// console.log('Sending offer to peer'); +// this.pc.createOffer(this.setLocalAndSendMessage, this.handleCreateOfferError); +// } + +// doAnswer() { +// console.log('Sending answer to peer.'); +// this.pc.createAnswer().then( +// this.setLocalAndSendMessage, +// this.onCreateSessionDescriptionError +// ); +// } + +// setLocalAndSendMessage(sessionDescription: any) { +// this.pc.setLocalDescription(sessionDescription); +// console.log('setLocalAndSendMessage sending message', sessionDescription); +// this.sendMessage(sessionDescription); +// } + +// onCreateSessionDescriptionError(error: any) { +// console.log('Failed to create session description: ' + error.toString()); +// } + + +// requestTurn(turnURL: any) { +// var turnExists = false; +// let self = this; +// for (var i in this.pcConfig.iceServers) { +// if (this.pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') { +// turnExists = true; +// this.turnReady = true; +// break; +// } +// } +// if (!turnExists) { +// console.log('Getting TURN server from ', turnURL); +// // No TURN server. Get one from computeengineondemand.appspot.com: +// var xhr = new XMLHttpRequest(); +// xhr.onreadystatechange = function () { +// if (xhr.readyState === 4 && xhr.status === 200) { +// var turnServer = JSON.parse(xhr.responseText); +// console.log('Got TURN server: ', turnServer); +// self.pcConfig.iceServers.push({ +// 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn, +// //'credential': turnServer.password +// }); +// self.turnReady = true; +// } +// }; +// xhr.open('GET', turnURL, true); +// xhr.send(); +// } +// } + +// handleRemoteStreamAdded(event: MediaStreamEvent) { +// console.log('Remote stream added.'); +// this.remoteStream = event.stream!; +// this.remoteVideo!.srcObject = this.remoteStream; +// } + +// handleRemoteStreamRemoved(event: MediaStreamEvent) { +// console.log('Remote stream removed. Event: ', event); +// } + +// hangup() { +// console.log('Hanging up.'); +// if (this.pc) { +// stop(); +// this.sendMessage('bye'); +// } + +// if (this.localStream) { +// this.localStream.getTracks().forEach(track => track.stop()); +// } + +// } + +// handleRemoteHangup() { +// console.log('Session terminated.'); +// stop(); +// this.isInitiator = false; +// } + +// stop() { +// this.isStarted = false; +// this.pc.close(); +// this.pc = null; +// } + + +// } \ No newline at end of file diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 7b091062c..acba95354 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -8,7 +8,6 @@ import { InkingControl } from "../InkingControl"; import "../../views/nodes/WebBox.scss"; import "./DashWebRTC.scss"; import adapter from 'webrtc-adapter'; -import { DashWebRTC } from "./DashWebRTC"; import { DocServer } from "../../DocServer"; import { DocumentView } from "../nodes/DocumentView"; import { Utils } from "../../../Utils"; @@ -34,307 +33,337 @@ export class DashWebRTCVideo extends React.Component { - //Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: 'bye', room: this.roomOfCam }); - this.webRTCManager!.hangup(); + this.hangup(); } componentWillUnmount() { - // DocumentDecorations.Instance.removeCloseCall(this.closeConnection); } - // componentDidMount() { - // // DashWebRTC.setVideoObjects(this.localVideoEl!, this.peerVideoEl!); - // //DashWebRTC.init(); - // } - - - // componentDidMount() { - // this.callButton!.disabled = true; - // this.hangupButton!.disabled = true; - // // navigator.mediaDevices.getUserMedia(mediaStreamConstraints).then(this.gotLocalMediaStream).catch(this.handleLocalMediaStreamError); - // this.localVideoEl!.addEventListener('loadedmetadata', this.logVideoLoaded); - // this.peerVideoEl!.addEventListener('loadedmetadata', this.logVideoLoaded); - // this.peerVideoEl!.addEventListener('onresize', this.logResizedVideo); - // } - - - // gotLocalMediaStream = (mediaStream: MediaStream) => { - // this.localStream = mediaStream; - // if (this.localVideoEl) { - // this.localVideoEl.srcObject = mediaStream; - // } - // this.trace('Received local stream.'); - // this.callButton!.disabled = false; - - // } - - // gotRemoteMediaStream = (event: MediaStreamEvent) => { - // let mediaStream = event.stream; - // this.peerVideoEl!.srcObject = mediaStream; - // this.remoteStream = mediaStream!; + init(room: string) { - // } + this.curRoom = room; + let self = this; - // handleLocalMediaStreamError = (error: string) => { - // //console.log("navigator.getUserMedia error: ", error); - // this.trace(`navigator.getUserMedia error: ${error.toString()}.`); + if (room !== '') { + DocServer._socket.emit('create or join', room); + console.log('Attempted to create or join room', room); - // } - - // logVideoLoaded = (event: any) => { - // let video = event.target; - // this.trace(`${video.id} videoWidth: ${video.videoWidth}px, ` + - // `videoHeight: ${video.videoHeight}px.`); - // } + } - // logResizedVideo = (event: any) => { - // this.logVideoLoaded(event); + DocServer._socket.on('created', function (room: string) { + console.log('Created room ' + room); + self.isInitiator = true; + }); + + DocServer._socket.on('full', function (room: string) { + console.log('Room ' + room + ' is full'); + }); + + DocServer._socket.on('join', function (room: string) { + console.log('Another peer made a request to join room ' + room); + console.log('This peer is the initiator of room ' + room + '!'); + self.isChannelReady = true; + }); + + + DocServer._socket.on('joined', function (room: string) { + console.log('joined: ' + room); + self.isChannelReady = true; + }); + + + DocServer._socket.on('log', function (array: any) { + console.log.apply(console, array); + }); + + // This client receives a message + DocServer._socket.on('message', function (message: any) { + console.log('Client received message:', message); + if (message.message === 'got user media') { + self.maybeStart(); + } else if (message.message.type === 'offer') { + if (!self.isInitiator && !self.isStarted) { + self.maybeStart(); + } + self.pc.setRemoteDescription(new RTCSessionDescription(message.message)); + self.doAnswer(); + } else if (message.message.type === 'answer' && self.isStarted) { + self.pc.setRemoteDescription(new RTCSessionDescription(message.message)); + } else if (message.message.type === 'candidate' && self.isStarted) { + let candidate = new RTCIceCandidate({ + sdpMLineIndex: message.message.label, + candidate: message.message.candidate + }); + self.pc.addIceCandidate(candidate); + } else if (message === 'bye' && self.isStarted) { + self.handleRemoteHangup(); + } + }); + + navigator.mediaDevices.getUserMedia({ + audio: false, + video: true + }) + .then(this.gotStream) + .catch(function (e) { + alert('getUserMedia() error: ' + e.name); + }); + + //Trying this one out!!! + console.log('Getting user media with constraints', this.constraints); + + if (location.hostname !== 'localhost') { + this.requestTurn( + 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913' + ); + } - // if (this.startTime) { - // let elapsedTime = window.performance.now() - this.startTime; - // this.startTime = null; - // this.trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`); - // } - // } + } - // handleConnection = (event: any) => { - // let peerConnection = event.target; - // let iceCandidate = event.candidate; - // if (iceCandidate) { - // let newIceCandidate: RTCIceCandidate = new RTCIceCandidate(iceCandidate); - // let otherPeer: any = this.getOtherPeer(peerConnection); + private sendMessage = (message: any) => { + console.log('Client sending message: ', message); + Utils.Emit(DocServer._socket, MessageStore.NotifyRoommates, { message: message, room: this.curRoom }); + //DocServer._socket.emit('message', message); + } - // otherPeer.addIceCandidate(newIceCandidate).then(() => { - // this.handleConnectionSuccess(peerConnection); - // }).catch((error: any) => { - // this.handleConnectionFailure(peerConnection, error); - // }); - // this.trace(`${this.getPeerName(peerConnection)} ICE candidate:\n` + - // `${event.candidate.candidate}.`); - // } - // } - // // Logs that the connection succeeded. - // handleConnectionSuccess = (peerConnection: any) => { - // this.trace(`${this.getPeerName(peerConnection)} addIceCandidate success.`); - // } - // handleConnectionFailure = (peerConnection: any, error: any) => { - // this.trace(`${this.getPeerName(peerConnection)} failed to add ICE Candidate:\n` + - // `${error.toString()}.`); + // setVideoObjects(localVideo: HTMLVideoElement, remoteVideo: HTMLVideoElement) { + // this.localVideo = localVideo; + // this.remoteVideo = remoteVideo; // } - // // Logs changes to the connection state. - // handleConnectionChange = (event: any) => { - // let peerConnection = event.target; - // console.log('ICE state change event: ', event); - // this.trace(`${this.getPeerName(peerConnection)} ICE state: ` + - // `${peerConnection.iceConnectionState}.`); - // } + // setLocalVideoObject(localVideoRef: HTMLVideoElement) { + // this.localVideo = localVideoRef; - // // Logs error when setting session description fails. - // setSessionDescriptionError = (error: any) => { - // this.trace(`Failed to create session description: ${error.toString()}.`); // } - // // Logs success when setting session description. - // setDescriptionSuccess = (peerConnection: any, functionName: any) => { - // let peerName = this.getPeerName(peerConnection); - // this.trace(`${peerName} ${functionName} complete.`); + // setRemoteVideoObject(remoteVideoRef: HTMLVideoElement) { + // this.remoteVideo = remoteVideoRef; // } - // // Logs success when localDescription is set. - // setLocalDescriptionSuccess = (peerConnection: any) => { - // this.setDescriptionSuccess(peerConnection, 'setLocalDescription'); - // } - // // Logs success when remoteDescription is set. - // setRemoteDescriptionSuccess = (peerConnection: any) => { - // this.setDescriptionSuccess(peerConnection, 'setRemoteDescription'); - // } - // createdOffer = (description: any) => { - // this.trace(`Offer from localPeerConnection:\n${description.sdp}`); - // this.trace('localPeerConnection setLocalDescription start.'); + private gotStream = (stream: any) => { + console.log('Adding local stream.'); + this.localStream = stream; + this.localVideoEl!.srcObject = stream; + this.sendMessage('got user media'); + if (this.isInitiator) { + this.maybeStart(); + } + } - // this.localPeerConnection.setLocalDescription(description).then(() => { - // this.setLocalDescriptionSuccess(this.localPeerConnection); - // }).catch(this.setSessionDescriptionError); + constraints = { + video: true, + audio: true + }; - // this.trace('remotePeerConnection setRemoteDescription start.'); - // this.remotePeerConnection.setRemoteDescription(description) - // .then(() => { - // this.setRemoteDescriptionSuccess(this.remotePeerConnection); - // }).catch(this.setSessionDescriptionError); - // this.trace('remotePeerConnection createAnswer start.'); - // this.remotePeerConnection.createAnswer() - // .then(this.createdAnswer) - // .catch(this.setSessionDescriptionError); - // } - // createdAnswer = (description: any) => { - // this.trace(`Answer from remotePeerConnection:\n${description.sdp}.`); + private maybeStart = () => { + console.log('>>>>>>> maybeStart() ', this.isStarted, this.localStream, this.isChannelReady); + if (!this.isStarted && typeof this.localStream !== 'undefined' && this.isChannelReady) { + console.log('>>>>>> creating peer connection'); + this.createPeerConnection(); + this.pc.addStream(this.localStream); + this.isStarted = true; + console.log('isInitiator', this.isInitiator); + if (this.isInitiator) { + this.doCall(); + } + } + } - // this.trace('remotePeerConnection setLocalDescription start.'); - // this.remotePeerConnection.setLocalDescription(description) - // .then(() => { - // this.setLocalDescriptionSuccess(this.remotePeerConnection); - // }).catch(this.setSessionDescriptionError); - // this.trace('localPeerConnection setRemoteDescription start.'); - // this.localPeerConnection.setRemoteDescription(description) - // .then(() => { - // this.setRemoteDescriptionSuccess(this.localPeerConnection); - // }).catch(this.setSessionDescriptionError); - // } + // //this will need to be changed to our version of hangUp + // window.onbeforeunload = function () { + // sendMessage('bye'); + // }; + + private createPeerConnection = () => { + try { + this.pc = new RTCPeerConnection(undefined); + this.pc.onicecandidate = this.handleIceCandidate; + this.pc.onaddstream = this.handleRemoteStreamAdded; + this.pc.onremovestream = this.handleRemoteStreamRemoved; + console.log('Created RTCPeerConnnection'); + } catch (e) { + console.log('Failed to create PeerConnection, exception: ' + e.message); + alert('Cannot create RTCPeerConnection object.'); + return; + } + } + private handleIceCandidate = (event: any) => { + console.log('icecandidate event: ', event); + if (event.candidate) { + this.sendMessage({ + type: 'candidate', + label: event.candidate.sdpMLineIndex, + id: event.candidate.sdpMid, + candidate: event.candidate.candidate + }); + } else { + console.log('End of candidates.'); + } + } - // startAction = () => { - // this.startButton!.disabled = true; - // navigator.mediaDevices.getUserMedia(mediaStreamConstraints) - // .then(this.gotLocalMediaStream).catch(this.handleLocalMediaStreamError); - // this.trace('Requesting local stream.'); - // } + private handleCreateOfferError = (event: any) => { + console.log('createOffer() error: ', event); + } + private doCall = () => { + console.log('Sending offer to peer'); + this.pc.createOffer(this.setLocalAndSendMessage, this.handleCreateOfferError); + } - // // Handles call button action: creates peer connection. - // callAction = () => { - // this.callButton!.disabled = true; - // this.hangupButton!.disabled = false; + private doAnswer = () => { + console.log('Sending answer to peer.'); + this.pc.createAnswer().then( + this.setLocalAndSendMessage, + this.onCreateSessionDescriptionError + ); + } - // this.trace('Starting call.'); - // this.startTime = window.performance.now(); + private setLocalAndSendMessage = (sessionDescription: any) => { + this.pc.setLocalDescription(sessionDescription); + console.log('setLocalAndSendMessage sending message', sessionDescription); + this.sendMessage(sessionDescription); + } - // // Get local media stream tracks. - // const videoTracks = this.localStream!.getVideoTracks(); - // const audioTracks = this.localStream!.getAudioTracks(); - // if (videoTracks.length > 0) { - // this.trace(`Using video device: ${videoTracks[0].label}.`); - // } - // if (audioTracks.length > 0) { - // this.trace(`Using audio device: ${audioTracks[0].label}.`); - // } + private onCreateSessionDescriptionError = (error: any) => { + console.log('Failed to create session description: ' + error.toString()); + } - // let servers: RTCConfiguration | undefined = undefined; // Allows for RTC server configuration. - // // Create peer connections and add behavior. - // this.localPeerConnection = new RTCPeerConnection(servers); - // this.trace('Created local peer connection object localPeerConnection.'); + private requestTurn = (turnURL: any) => { + var turnExists = false; + let self = this; + for (var i in this.pcConfig.iceServers) { + if (this.pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') { + turnExists = true; + this.turnReady = true; + break; + } + } + if (!turnExists) { + console.log('Getting TURN server from ', turnURL); + // No TURN server. Get one from computeengineondemand.appspot.com: + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + var turnServer = JSON.parse(xhr.responseText); + console.log('Got TURN server: ', turnServer); + self.pcConfig.iceServers.push({ + 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn, + //'credential': turnServer.password + }); + self.turnReady = true; + } + }; + xhr.open('GET', turnURL, true); + xhr.send(); + } + } - // this.localPeerConnection.addEventListener('icecandidate', this.handleConnection); - // this.localPeerConnection.addEventListener( - // 'iceconnectionstatechange', this.handleConnectionChange); + private handleRemoteStreamAdded = (event: MediaStreamEvent) => { + console.log('Remote stream added.'); + this.remoteStream = event.stream!; + this.peerVideoEl!.srcObject = this.remoteStream; + } - // this.remotePeerConnection = new RTCPeerConnection(servers); - // this.trace('Created remote peer connection object remotePeerConnection.'); + private handleRemoteStreamRemoved = (event: MediaStreamEvent) => { + console.log('Remote stream removed. Event: ', event); + } - // this.remotePeerConnection.addEventListener('icecandidate', this.handleConnection); - // this.remotePeerConnection.addEventListener( - // 'iceconnectionstatechange', this.handleConnectionChange); - // this.remotePeerConnection.addEventListener('addstream', this.gotRemoteMediaStream); + private hangup = () => { + console.log('Hanging up.'); + if (this.pc) { + stop(); + this.sendMessage('bye'); + } - // // Add local stream to connection and create offer to connect. - // this.localPeerConnection.addStream(this.localStream); - // this.trace('Added local stream to localPeerConnection.'); + if (this.localStream) { + this.localStream.getTracks().forEach(track => track.stop()); + } - // this.trace('localPeerConnection createOffer start.'); - // this.localPeerConnection.createOffer(offerOptions) - // .then(this.createdOffer).catch(this.setSessionDescriptionError); - // } + } + private handleRemoteHangup = () => { + console.log('Session terminated.'); + stop(); + this.isInitiator = false; + } - // // Handles hangup action: ends up call, closes connections and resets peers. - // hangupAction = () => { - // this.localPeerConnection.close(); - // this.remotePeerConnection.close(); - // this.localPeerConnection = null; - // this.remotePeerConnection = null; - // this.hangupButton!.disabled = true; - // this.callButton!.disabled = false; - // this.trace('Ending call.'); - // } + private stop = () => { + this.isStarted = false; + this.pc.close(); + this.pc = null; + } - // // Gets the "other" peer connection. - // getOtherPeer = (peerConnection: any) => { - // return (peerConnection === this.localPeerConnection) ? - // this.remotePeerConnection : this.localPeerConnection; - // } - // // Gets the name of a certain peer connection. - // getPeerName = (peerConnection: any) => { - // return (peerConnection === this.localPeerConnection) ? - // 'localPeerConnection' : 'remotePeerConnection'; - // } - // // Logs an action (text) and the time when it happened on the console. - // trace = (text: string) => { - // text = text.trim(); - // const now = (window.performance.now() / 1000).toFixed(3); - // console.log(now, text); - // } /** * Function that submits the title entered by user on enter press. */ - onEnterKeyDown = (e: React.KeyboardEvent) => { + private onEnterKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === 13) { let submittedTitle = this.roomText!.value; this.roomText!.value = ""; this.roomText!.blur(); this.roomOfCam = submittedTitle; - this.webRTCManager!.init(submittedTitle); + this.init(submittedTitle); } } - - - - - - - - - - - - - - - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DashWebRTCVideo, fieldKey); } @@ -365,11 +394,11 @@ export class DashWebRTCVideo extends React.Component this.roomText = e!} onKeyDown={this.onEnterKeyDown} /> {/* diff --git a/src/server/index.ts b/src/server/index.ts index 2b18fc578..72ddb5bd7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -86,51 +86,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: secureHandler: ({ res }) => res.redirect("/home") }); - // Utils.AddServerHandler(socket, MessageStore.NotifyRoommates, message => HandleRoommateNotification(socket, message)); - // // Utils.AddServerHandler(socket, MessageStore.HangUpCall, message => HandleHangUp(socket, message)); - // // Utils.AddRoomHandler(socket, "create or join", HandleCreateOrJoin); - // function HandleRoommateNotification(socket: Socket, message: RoomMessage) { - // //socket.broadcast.emit('message', message); - // console.log("The room that sent this: ", message.room, " and message is : ", message.message); - // server.sockets.in(message.room).emit('message', message); - - // } - - // function HandleCreateOrJoin(socket: io.Socket, room: string) { - // console.log("Received request to create or join room " + room); - - - // let clientsInRoom = server.sockets.adapter.rooms[room]; - // let numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; - // console.log('Room ' + room + ' now has ' + numClients + ' client(s)'); - - - // if (numClients === 0) { - // socket.join(room); - // console.log('Client ID ' + socket.id + ' created room ' + room); - // socket.emit('created', room, socket.id); - - // } else if (numClients === 1) { - // console.log('Client ID ' + socket.id + ' joined room ' + room); - // server.sockets.in(room).emit('join', room); - // socket.join(room); - // socket.emit('joined', room, socket.id); - // server.sockets.in(room).emit('ready'); - - // } else { - // socket.emit('full', room); - // } - - - - - - // } - - // function HandleHangUp(socket: io.Socket, message: string) { - // console.log("Receive bye from someone"); - // } addSupervisedRoute({ method: Method.GET, subscription: "/serverHeartbeat", @@ -165,6 +121,52 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: WebSocket.start(isRelease); } +// Utils.AddServerHandler(socket, MessageStore.NotifyRoommates, message => HandleRoommateNotification(socket, message)); +// // Utils.AddServerHandler(socket, MessageStore.HangUpCall, message => HandleHangUp(socket, message)); +// // Utils.AddRoomHandler(socket, "create or join", HandleCreateOrJoin); + +// function HandleRoommateNotification(socket: Socket, message: RoomMessage) { +// //socket.broadcast.emit('message', message); +// console.log("The room that sent this: ", message.room, " and message is : ", message.message); +// server.sockets.in(message.room).emit('message', message); + +// } + +// function HandleCreateOrJoin(socket: io.Socket, room: string) { +// console.log("Received request to create or join room " + room); + + +// let clientsInRoom = server.sockets.adapter.rooms[room]; +// let numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; +// console.log('Room ' + room + ' now has ' + numClients + ' client(s)'); + + +// if (numClients === 0) { +// socket.join(room); +// console.log('Client ID ' + socket.id + ' created room ' + room); +// socket.emit('created', room, socket.id); + +// } else if (numClients === 1) { +// console.log('Client ID ' + socket.id + ' joined room ' + room); +// server.sockets.in(room).emit('join', room); +// socket.join(room); +// socket.emit('joined', room, socket.id); +// server.sockets.in(room).emit('ready'); + +// } else { +// socket.emit('full', room); +// } + + + + + +// } + +// function HandleHangUp(socket: io.Socket, message: string) { +// console.log("Receive bye from someone"); +// } + /** * This function can be used in two different ways. If not in release mode, * this is simply the logic that is invoked to start the server. In release mode, -- cgit v1.2.3-70-g09d2 From 277107f6d4b85036cd6c2ba5934da9b542788462 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush <47069173+mamoush34@users.noreply.github.com> Date: Sun, 9 Feb 2020 14:38:16 -0500 Subject: Another cleanUp --- src/server/index.ts | 45 --------------------------------------------- 1 file changed, 45 deletions(-) (limited to 'src/server/index.ts') diff --git a/src/server/index.ts b/src/server/index.ts index 72ddb5bd7..2101de1d2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -121,51 +121,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: WebSocket.start(isRelease); } -// Utils.AddServerHandler(socket, MessageStore.NotifyRoommates, message => HandleRoommateNotification(socket, message)); -// // Utils.AddServerHandler(socket, MessageStore.HangUpCall, message => HandleHangUp(socket, message)); -// // Utils.AddRoomHandler(socket, "create or join", HandleCreateOrJoin); - -// function HandleRoommateNotification(socket: Socket, message: RoomMessage) { -// //socket.broadcast.emit('message', message); -// console.log("The room that sent this: ", message.room, " and message is : ", message.message); -// server.sockets.in(message.room).emit('message', message); - -// } - -// function HandleCreateOrJoin(socket: io.Socket, room: string) { -// console.log("Received request to create or join room " + room); - - -// let clientsInRoom = server.sockets.adapter.rooms[room]; -// let numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; -// console.log('Room ' + room + ' now has ' + numClients + ' client(s)'); - - -// if (numClients === 0) { -// socket.join(room); -// console.log('Client ID ' + socket.id + ' created room ' + room); -// socket.emit('created', room, socket.id); - -// } else if (numClients === 1) { -// console.log('Client ID ' + socket.id + ' joined room ' + room); -// server.sockets.in(room).emit('join', room); -// socket.join(room); -// socket.emit('joined', room, socket.id); -// server.sockets.in(room).emit('ready'); - -// } else { -// socket.emit('full', room); -// } - - - - - -// } - -// function HandleHangUp(socket: io.Socket, message: string) { -// console.log("Receive bye from someone"); -// } /** * This function can be used in two different ways. If not in release mode, -- cgit v1.2.3-70-g09d2 From 3f3fbcf0c2ea60625f20f3d06388723645e78170 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 10 Feb 2020 12:07:32 -0500 Subject: server session now local --- package-lock.json | 417 +++------------------ package.json | 1 - src/server/DashSession/DashSessionAgent.ts | 5 +- .../Session/agents/applied_session_agent.ts | 58 +++ src/server/DashSession/Session/agents/monitor.ts | 298 +++++++++++++++ .../Session/agents/process_message_router.ts | 41 ++ .../Session/agents/promisified_ipc_manager.ts | 173 +++++++++ .../DashSession/Session/agents/server_worker.ts | 160 ++++++++ src/server/DashSession/Session/utilities/repl.ts | 128 +++++++ .../Session/utilities/session_config.ts | 129 +++++++ .../DashSession/Session/utilities/utilities.ts | 37 ++ src/server/index.ts | 2 +- 12 files changed, 1072 insertions(+), 377 deletions(-) create mode 100644 src/server/DashSession/Session/agents/applied_session_agent.ts create mode 100644 src/server/DashSession/Session/agents/monitor.ts create mode 100644 src/server/DashSession/Session/agents/process_message_router.ts create mode 100644 src/server/DashSession/Session/agents/promisified_ipc_manager.ts create mode 100644 src/server/DashSession/Session/agents/server_worker.ts create mode 100644 src/server/DashSession/Session/utilities/repl.ts create mode 100644 src/server/DashSession/Session/utilities/session_config.ts create mode 100644 src/server/DashSession/Session/utilities/utilities.ts (limited to 'src/server/index.ts') diff --git a/package-lock.json b/package-lock.json index 0b8bd140c..f38f23acb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -374,7 +374,8 @@ "@types/chai": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz", - "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==" + "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==", + "dev": true }, "@types/classnames": { "version": "2.2.9", @@ -602,7 +603,8 @@ "@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true }, "@types/mongodb": { "version": "3.3.14", @@ -1995,7 +1997,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -5417,8 +5419,8 @@ "bundled": true, "optional": true, "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, "balanced-match": { @@ -5501,14 +5503,14 @@ "bundled": true, "optional": true, "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.3" + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" } }, "glob": { @@ -5516,12 +5518,12 @@ "bundled": true, "optional": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "has-unicode": { @@ -5550,8 +5552,8 @@ "bundled": true, "optional": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -5683,10 +5685,10 @@ "bundled": true, "optional": true, "requires": { - "are-we-there-yet": "1.1.5", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" } }, "number-is-nan": { @@ -5704,7 +5706,7 @@ "bundled": true, "optional": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "os-homedir": { @@ -5741,10 +5743,10 @@ "bundled": true, "optional": true, "requires": { - "deep-extend": "0.6.0", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" }, "dependencies": { "minimist": { @@ -5759,13 +5761,13 @@ "bundled": true, "optional": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.4", - "isarray": "1.0.0", - "process-nextick-args": "2.0.1", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "rimraf": { @@ -5811,9 +5813,9 @@ "bundled": true, "optional": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "string_decoder": { @@ -5861,7 +5863,7 @@ "bundled": true, "optional": true, "requires": { - "string-width": "1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -12629,11 +12631,6 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, - "picomatch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==" - }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -14249,334 +14246,6 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, - "resilient-server-session": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/resilient-server-session/-/resilient-server-session-1.1.9.tgz", - "integrity": "sha512-XSVujTyJOQMACllXUvWOSHY4GK4JI6aECjCrQR0UBvd2+hdjM1euffspn2b+7M0fepo+bJ71YrAOA9M34ChBZw==", - "requires": { - "@types/chai": "^4.2.7", - "@types/express": "^4.17.2", - "@types/mocha": "^5.2.7", - "@types/node": "^10.12.30", - "@types/request-promise": "^4.1.42", - "@types/uuid": "^3.4.6", - "chai": "^4.2.0", - "colors": "^1.4.0", - "express": "^4.17.1", - "jsonschema": "^1.2.5", - "mocha": "^7.0.0", - "request": "^2.88.0", - "request-promise": "^4.2.5", - "typescript": "^3.7.4", - "uuid": "^3.3.3" - }, - "dependencies": { - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "optional": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "mocha": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.1.tgz", - "integrity": "sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg==", - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", - "yargs-unparser": "1.6.0" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "requires": { - "picomatch": "^2.0.4" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" - } - }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", diff --git a/package.json b/package.json index 250509e80..6f54023d6 100644 --- a/package.json +++ b/package.json @@ -222,7 +222,6 @@ "readline": "^1.3.0", "request": "^2.88.0", "request-promise": "^4.2.5", - "resilient-server-session": "^1.1.9", "rimraf": "^3.0.0", "serializr": "^1.5.4", "sharp": "^0.23.4", diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index d61e9aac1..85bfe14de 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -8,8 +8,11 @@ import { launchServer, onWindows } from ".."; import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs"; import * as Archiver from "archiver"; import { resolve } from "path"; -import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session"; import rimraf = require("rimraf"); +import { AppliedSessionAgent, ExitHandler } from "./Session/agents/applied_session_agent"; +import { ServerWorker } from "./Session/agents/server_worker"; +import { Monitor } from "./Session/agents/monitor"; +import { MessageHandler } from "./Session/agents/promisified_ipc_manager"; /** * If we're the monitor (master) thread, we should launch the monitor logic for the session. diff --git a/src/server/DashSession/Session/agents/applied_session_agent.ts b/src/server/DashSession/Session/agents/applied_session_agent.ts new file mode 100644 index 000000000..46c9e22ed --- /dev/null +++ b/src/server/DashSession/Session/agents/applied_session_agent.ts @@ -0,0 +1,58 @@ +import { isMaster } from "cluster"; +import { Monitor } from "./monitor"; +import { ServerWorker } from "./server_worker"; +import { Utilities } from "../utilities/utilities"; + +export type ExitHandler = (reason: Error | boolean) => void | Promise; + +export abstract class AppliedSessionAgent { + + // the following two methods allow the developer to create a custom + // session and use the built in customization options for each thread + protected abstract async initializeMonitor(monitor: Monitor): Promise; + protected abstract async initializeServerWorker(): Promise; + + private launched = false; + + public killSession = (reason: string, graceful = true, errorCode = 0) => { + const target = isMaster ? this.sessionMonitor : this.serverWorker; + target.killSession(reason, graceful, errorCode); + } + + private sessionMonitorRef: Monitor | undefined; + public get sessionMonitor(): Monitor { + if (!isMaster) { + this.serverWorker.emit("kill", { + graceful: false, + reason: "Cannot access the session monitor directly from the server worker thread.", + errorCode: 1 + }); + throw new Error(); + } + return this.sessionMonitorRef!; + } + + private serverWorkerRef: ServerWorker | undefined; + public get serverWorker(): ServerWorker { + if (isMaster) { + throw new Error("Cannot access the server worker directly from the session monitor thread"); + } + return this.serverWorkerRef!; + } + + public async launch(): Promise { + if (!this.launched) { + this.launched = true; + if (isMaster) { + this.sessionMonitorRef = Monitor.Create() + const sessionKey = await this.initializeMonitor(this.sessionMonitorRef); + this.sessionMonitorRef.finalize(sessionKey); + } else { + this.serverWorkerRef = await this.initializeServerWorker(); + } + } else { + throw new Error("Cannot launch a session thread more than once per process."); + } + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts new file mode 100644 index 000000000..1d4ea6fb5 --- /dev/null +++ b/src/server/DashSession/Session/agents/monitor.ts @@ -0,0 +1,298 @@ +import { ExitHandler } from "./applied_session_agent"; +import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from "../utilities/session_config"; +import Repl, { ReplAction } from "../utilities/repl"; +import { isWorker, setupMaster, on, Worker, fork } from "cluster"; +import { manage, MessageHandler } from "./promisified_ipc_manager"; +import { red, cyan, white, yellow, blue } from "colors"; +import { exec, ExecOptions } from "child_process"; +import { validate, ValidationError } from "jsonschema"; +import { Utilities } from "../utilities/utilities"; +import { readFileSync } from "fs"; +import IPCMessageReceiver from "./process_message_router"; +import { ServerWorker } from "./server_worker"; + +/** + * Validates and reads the configuration file, accordingly builds a child process factory + * and spawns off an initial process that will respawn as predecessors die. + */ +export class Monitor extends IPCMessageReceiver { + private static count = 0; + private finalized = false; + private exitHandlers: ExitHandler[] = []; + private readonly config: Configuration; + private activeWorker: Worker | undefined; + private key: string | undefined; + private repl: Repl; + + public static Create() { + if (isWorker) { + ServerWorker.IPCManager.emit("kill", { + reason: "cannot create a monitor on the worker process.", + graceful: false, + errorCode: 1 + }); + process.exit(1); + } else if (++Monitor.count > 1) { + console.error(red("cannot create more than one monitor.")); + process.exit(1); + } else { + return new Monitor(); + } + } + + private constructor() { + super(); + console.log(this.timestamp(), cyan("initializing session...")); + this.configureInternalHandlers(); + this.config = this.loadAndValidateConfiguration(); + this.initializeClusterFunctions(); + this.repl = this.initializeRepl(); + } + + protected configureInternalHandlers = () => { + // handle exceptions in the master thread - there shouldn't be many of these + // the IPC (inter process communication) channel closed exception can't seem + // to be caught in a try catch, and is inconsequential, so it is ignored + process.on("uncaughtException", ({ message, stack }): void => { + if (message !== "Channel closed") { + this.mainLog(red(message)); + if (stack) { + this.mainLog(`uncaught exception\n${red(stack)}`); + } + } + }); + + this.on("kill", ({ reason, graceful, errorCode }) => this.killSession(reason, graceful, errorCode)); + this.on("lifecycle", ({ event }) => console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${event})`)); + } + + private initializeClusterFunctions = () => { + // determines whether or not we see the compilation / initialization / runtime output of each child server process + const output = this.config.showServerOutput ? "inherit" : "ignore"; + setupMaster({ stdio: ["ignore", output, output, "ipc"] }); + + // a helpful cluster event called on the master thread each time a child process exits + on("exit", ({ process: { pid } }, code, signal) => { + const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`; + this.mainLog(cyan(prompt)); + // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one + this.spawn(); + }); + } + + public finalize = (sessionKey: string): void => { + if (this.finalized) { + throw new Error("Session monitor is already finalized"); + } + this.finalized = true; + this.key = sessionKey; + this.spawn(); + } + + public readonly coreHooks = Object.freeze({ + onCrashDetected: (listener: MessageHandler<{ error: Error }>) => this.on(Monitor.IntrinsicEvents.CrashDetected, listener), + onServerRunning: (listener: MessageHandler<{ isFirstTime: boolean }>) => this.on(Monitor.IntrinsicEvents.ServerRunning, listener) + }); + + /** + * Kill this session and its active child + * server process, either gracefully (may wait + * indefinitely, but at least allows active networking + * requests to complete) or immediately. + */ + public killSession = async (reason: string, graceful = true, errorCode = 0) => { + this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`)); + this.mainLog(`session exit reason: ${(red(reason))}`); + await this.executeExitHandlers(true); + await this.killActiveWorker(graceful, true); + process.exit(errorCode); + } + + /** + * Execute the list of functions registered to be called + * whenever the process exits. + */ + public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); + + /** + * Extend the default repl by adding in custom commands + * that can invoke application logic external to this module + */ + public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { + this.repl.registerCommand(basename, argPatterns, action); + } + + public exec = (command: string, options?: ExecOptions) => { + return new Promise(resolve => { + exec(command, { ...options, encoding: "utf8" }, (error, stdout, stderr) => { + if (error) { + this.execLog(red(`unable to execute ${white(command)}`)); + error.message.split("\n").forEach(line => line.length && this.execLog(red(`(error) ${line}`))); + } else { + let outLines: string[], errorLines: string[]; + if ((outLines = stdout.split("\n").filter(line => line.length)).length) { + outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`))); + } + if ((errorLines = stderr.split("\n").filter(line => line.length)).length) { + errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`))); + } + } + resolve(); + }); + }); + } + + /** + * Generates a blue UTC string associated with the time + * of invocation. + */ + private timestamp = () => blue(`[${new Date().toUTCString()}]`); + + /** + * A formatted, identified and timestamped log in color + */ + public mainLog = (...optionalParams: any[]) => { + console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams); + } + + /** + * A formatted, identified and timestamped log in color for non- + */ + private execLog = (...optionalParams: any[]) => { + console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams); + } + + /** + * Reads in configuration .json file only once, in the master thread + * and pass down any variables the pertinent to the child processes as environment variables. + */ + private loadAndValidateConfiguration = (): Configuration => { + let config: Configuration; + try { + console.log(this.timestamp(), cyan("validating configuration...")); + config = JSON.parse(readFileSync('./session.config.json', 'utf8')); + const options = { + throwError: true, + allowUnknownAttributes: false + }; + // ensure all necessary and no excess information is specified by the configuration file + validate(config, configurationSchema, options); + config = Utilities.preciseAssign({}, defaultConfig, config); + } catch (error) { + if (error instanceof ValidationError) { + console.log(red("\nSession configuration failed.")); + console.log("The given session.config.json configuration file is invalid."); + console.log(`${error.instance}: ${error.stack}`); + process.exit(0); + } else if (error.code === "ENOENT" && error.path === "./session.config.json") { + console.log(cyan("Loading default session parameters...")); + console.log("Consider including a session.config.json configuration file in your project root for customization."); + config = Utilities.preciseAssign({}, defaultConfig); + } else { + console.log(red("\nSession configuration failed.")); + console.log("The following unknown error occurred during configuration."); + console.log(error.stack); + process.exit(0); + } + } finally { + const { identifiers } = config!; + Object.keys(identifiers).forEach(key => { + const resolved = key as keyof Identifiers; + const { text, color } = identifiers[resolved]; + identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`); + }); + return config!; + } + } + + /** + * Builds the repl that allows the following commands to be typed into stdin of the master thread. + */ + private initializeRepl = (): Repl => { + const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` }); + const boolean = /true|false/; + const number = /\d+/; + const letters = /[a-zA-Z]+/; + repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0)); + repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean")); + repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true")); + repl.registerCommand("set", [/polling/, number, boolean], args => { + const newPollingIntervalSeconds = Math.floor(Number(args[1])); + if (newPollingIntervalSeconds < 0) { + this.mainLog(red("the polling interval must be a non-negative integer")); + } else { + if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) { + this.config.polling.intervalSeconds = newPollingIntervalSeconds; + if (args[2] === "true") { + Monitor.IPCManager.emit("updatePollingInterval", { newPollingIntervalSeconds }); + } + } + } + }); + return repl; + } + + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + + /** + * Attempts to kill the active worker gracefully, unless otherwise specified. + */ + private killActiveWorker = async (graceful = true, isSessionEnd = false): Promise => { + if (this.activeWorker && !this.activeWorker.isDead()) { + if (graceful) { + Monitor.IPCManager.emit("manualExit", { isSessionEnd }); + } else { + await ServerWorker.IPCManager.destroy(); + this.activeWorker.process.kill(); + } + } + } + + /** + * Allows the caller to set the port at which the target (be it the server, + * the websocket, some other custom port) is listening. If an immediate restart + * is specified, this monitor will kill the active child and re-launch the server + * at the port. Otherwise, the updated port won't be used until / unless the child + * dies on its own and triggers a restart. + */ + private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => { + if (value > 1023 && value < 65536) { + this.config.ports[port] = value; + if (immediateRestart) { + this.killActiveWorker(); + } + } else { + this.mainLog(red(`${port} is an invalid port number`)); + } + } + + /** + * Kills the current active worker and proceeds to spawn a new worker, + * feeding in configuration information as environment variables. + */ + private spawn = async (): Promise => { + await this.killActiveWorker(); + const { config: { polling, ports }, key } = this; + this.activeWorker = fork({ + pollingRoute: polling.route, + pollingFailureTolerance: polling.failureTolerance, + serverPort: ports.server, + socketPort: ports.socket, + pollingIntervalSeconds: polling.intervalSeconds, + session_key: key + }); + Monitor.IPCManager = manage(this.activeWorker.process, this.handlers); + this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker?.process.pid}`)); + } + +} + +export namespace Monitor { + + export enum IntrinsicEvents { + KeyGenerated = "key_generated", + CrashDetected = "crash_detected", + ServerRunning = "server_running" + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts new file mode 100644 index 000000000..6cc8aa941 --- /dev/null +++ b/src/server/DashSession/Session/agents/process_message_router.ts @@ -0,0 +1,41 @@ +import { MessageHandler, PromisifiedIPCManager, HandlerMap } from "./promisified_ipc_manager"; + +export default abstract class IPCMessageReceiver { + + protected static IPCManager: PromisifiedIPCManager; + protected handlers: HandlerMap = {}; + + protected abstract configureInternalHandlers: () => void; + + /** + * Add a listener at this message. When the monitor process + * receives a message, it will invoke all registered functions. + */ + public on = (name: string, handler: MessageHandler) => { + const handlers = this.handlers[name]; + if (!handlers) { + this.handlers[name] = [handler]; + } else { + handlers.push(handler); + } + } + + /** + * Unregister a given listener at this message. + */ + public off = (name: string, handler: MessageHandler) => { + const handlers = this.handlers[name]; + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + } + + /** + * Unregister all listeners at this message. + */ + public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]); + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts new file mode 100644 index 000000000..9f0db8330 --- /dev/null +++ b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts @@ -0,0 +1,173 @@ +import { Utilities } from "../utilities/utilities"; +import { ChildProcess } from "child_process"; + +/** + * Convenience constructor + * @param target the process / worker to which to attach the specialized listeners + */ +export function manage(target: IPCTarget, handlers?: HandlerMap) { + return new PromisifiedIPCManager(target, handlers); +} + +/** + * Captures the logic to execute upon receiving a message + * of a certain name. + */ +export type HandlerMap = { [name: string]: MessageHandler[] }; + +/** + * This will always literally be a child process. But, though setting + * up a manager in the parent will indeed see the target as the ChildProcess, + * setting up a manager in the child will just see itself as a regular NodeJS.Process. + */ +export type IPCTarget = NodeJS.Process | ChildProcess; + +/** + * Specifies a general message format for this API + */ +export type Message = { + name: string; + args?: T; +}; +export type MessageHandler = (args: T) => (any | Promise); + +/** + * When a message is emitted, it is embedded with private metadata + * to facilitate the resolution of promises, etc. + */ +interface InternalMessage extends Message { metadata: Metadata } +interface Metadata { isResponse: boolean; id: string } +type InternalMessageHandler = (message: InternalMessage) => (any | Promise); + +/** + * Allows for the transmission of the error's key features over IPC. + */ +export interface ErrorLike { + name?: string; + message?: string; + stack?: string; +} + +/** + * The arguments returned in a message sent from the target upon completion. + */ +export interface Response { + results?: T[]; + error?: ErrorLike; +} + +const destroyEvent = "__destroy__"; + +/** + * This is a wrapper utility class that allows the caller process + * to emit an event and return a promise that resolves when it and all + * other processes listening to its emission of this event have completed. + */ +export class PromisifiedIPCManager { + private readonly target: IPCTarget; + private pendingMessages: { [id: string]: string } = {}; + private isDestroyed = false; + private get callerIsTarget() { + return process.pid === this.target.pid; + } + + constructor(target: IPCTarget, handlers?: HandlerMap) { + this.target = target; + if (handlers) { + handlers[destroyEvent] = [this.destroyHelper]; + this.target.addListener("message", this.generateInternalHandler(handlers)); + } + } + + /** + * This routine uniquely identifies each message, then adds a general + * message listener that waits for a response with the same id before resolving + * the promise. + */ + public emit = async (name: string, args?: any): Promise> => { + if (this.isDestroyed) { + const error = { name: "FailedDispatch", message: "Cannot use a destroyed IPC manager to emit a message." }; + return { error }; + } + return new Promise>(resolve => { + const messageId = Utilities.guid(); + const responseHandler: InternalMessageHandler = ({ metadata: { id, isResponse }, args }) => { + if (isResponse && id === messageId) { + this.target.removeListener("message", responseHandler); + resolve(args); + } + }; + this.target.addListener("message", responseHandler); + const message = { name, args, metadata: { id: messageId, isResponse: false } }; + if (!(this.target.send && this.target.send(message))) { + const error: ErrorLike = { name: "FailedDispatch", message: "Either the target's send method was undefined or the act of sending failed." }; + resolve({ error }); + this.target.removeListener("message", responseHandler); + } + }); + } + + /** + * Invoked from either the parent or the child process, this allows + * any unresolved promises to continue in the target process, but dispatches a dummy + * completion response for each of the pending messages, allowing their + * promises in the caller to resolve. + */ + public destroy = () => { + return new Promise(async resolve => { + if (this.callerIsTarget) { + this.destroyHelper(); + } else { + await this.emit(destroyEvent); + } + resolve(); + }); + } + + /** + * Dispatches the dummy responses and sets the isDestroyed flag to true. + */ + private destroyHelper = () => { + const { pendingMessages } = this; + this.isDestroyed = true; + Object.keys(pendingMessages).forEach(id => { + const error: ErrorLike = { name: "ManagerDestroyed", message: "The IPC manager was destroyed before the response could be returned." }; + const message: InternalMessage = { name: pendingMessages[id], args: { error }, metadata: { id, isResponse: true } }; + this.target.send?.(message) + }); + this.pendingMessages = {}; + } + + /** + * This routine receives a uniquely identified message. If the message is itself a response, + * it is ignored to avoid infinite mutual responses. Otherwise, the routine awaits its completion using whatever + * router the caller has installed, and then sends a response containing the original message id, + * which will ultimately invoke the responseHandler of the original emission and resolve the + * sender's promise. + */ + private generateInternalHandler = (handlers: HandlerMap): MessageHandler => async (message: InternalMessage) => { + const { name, args, metadata } = message; + if (name && metadata && !metadata.isResponse) { + const { id } = metadata; + this.pendingMessages[id] = name; + let error: Error | undefined; + let results: any[] | undefined; + try { + const registered = handlers[name]; + if (registered) { + results = await Promise.all(registered.map(handler => handler(args))); + } + } catch (e) { + error = e; + } + if (!this.isDestroyed && this.target.send) { + const metadata = { id, isResponse: true }; + const response: Response = { results , error }; + const message = { name, args: response , metadata }; + delete this.pendingMessages[id]; + this.target.send(message); + } + } + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/server_worker.ts b/src/server/DashSession/Session/agents/server_worker.ts new file mode 100644 index 000000000..976d27226 --- /dev/null +++ b/src/server/DashSession/Session/agents/server_worker.ts @@ -0,0 +1,160 @@ +import { ExitHandler } from "./applied_session_agent"; +import { isMaster } from "cluster"; +import { manage } from "./promisified_ipc_manager"; +import IPCMessageReceiver from "./process_message_router"; +import { red, green, white, yellow } from "colors"; +import { get } from "request-promise"; +import { Monitor } from "./monitor"; + +/** + * Effectively, each worker repairs the connection to the server by reintroducing a consistent state + * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification + * email if the server encounters an uncaught exception or if the server cannot be reached. + */ +export class ServerWorker extends IPCMessageReceiver { + private static count = 0; + private shouldServerBeResponsive = false; + private exitHandlers: ExitHandler[] = []; + private pollingFailureCount = 0; + private pollingIntervalSeconds: number; + private pollingFailureTolerance: number; + private pollTarget: string; + private serverPort: number; + private isInitialized = false; + + public static Create(work: Function) { + if (isMaster) { + console.error(red("cannot create a worker on the monitor process.")); + process.exit(1); + } else if (++ServerWorker.count > 1) { + ServerWorker.IPCManager.emit("kill", { + reason: "cannot create more than one worker on a given worker process.", + graceful: false, + errorCode: 1 + }); + process.exit(1); + } else { + return new ServerWorker(work); + } + } + + /** + * Allows developers to invoke application specific logic + * by hooking into the exiting of the server process. + */ + public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); + + /** + * Kill the session monitor (parent process) from this + * server worker (child process). This will also kill + * this process (child process). + */ + public killSession = (reason: string, graceful = true, errorCode = 0) => this.emit("kill", { reason, graceful, errorCode }); + + /** + * A convenience wrapper to tell the session monitor (parent process) + * to carry out the action with the specified message and arguments. + */ + public emit = async (name: string, args?: any) => ServerWorker.IPCManager.emit(name, args); + + private constructor(work: Function) { + super(); + this.configureInternalHandlers(); + ServerWorker.IPCManager = manage(process, this.handlers); + this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`)); + + const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env; + this.serverPort = Number(serverPort); + this.pollingIntervalSeconds = Number(pollingIntervalSeconds); + this.pollingFailureTolerance = Number(pollingFailureTolerance); + this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`; + + work(); + this.pollServer(); + } + + /** + * Set up message and uncaught exception handlers for this + * server process. + */ + protected configureInternalHandlers = () => { + // updates the local values of variables to the those sent from master + this.on("updatePollingInterval", ({ newPollingIntervalSeconds }) => this.pollingIntervalSeconds = newPollingIntervalSeconds); + this.on("manualExit", async ({ isSessionEnd }) => { + await ServerWorker.IPCManager.destroy(); + await this.executeExitHandlers(isSessionEnd); + process.exit(0); + }); + + // one reason to exit, as the process might be in an inconsistent state after such an exception + process.on('uncaughtException', this.proactiveUnplannedExit); + process.on('unhandledRejection', reason => { + const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`); + this.proactiveUnplannedExit(appropriateError); + }); + } + + /** + * Execute the list of functions registered to be called + * whenever the process exits. + */ + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + + /** + * Notify master thread (which will log update in the console) of initialization via IPC. + */ + public lifecycleNotification = (event: string) => this.emit("lifecycle", { event }); + + /** + * Called whenever the process has a reason to terminate, either through an uncaught exception + * in the process (potentially inconsistent state) or the server cannot be reached. + */ + private proactiveUnplannedExit = async (error: Error): Promise => { + this.shouldServerBeResponsive = false; + // communicates via IPC to the master thread that it should dispatch a crash notification email + this.emit(Monitor.IntrinsicEvents.CrashDetected, { error }); + await this.executeExitHandlers(error); + // notify master thread (which will log update in the console) of crash event via IPC + this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`)); + this.lifecycleNotification(red(error.message)); + await ServerWorker.IPCManager.destroy(); + process.exit(1); + } + + /** + * This monitors the health of the server by submitting a get request to whatever port / route specified + * by the configuration every n seconds, where n is also given by the configuration. + */ + private pollServer = async (): Promise => { + await new Promise(resolve => { + setTimeout(async () => { + try { + await get(this.pollTarget); + if (!this.shouldServerBeResponsive) { + // notify monitor thread that the server is up and running + this.lifecycleNotification(green(`listening on ${this.serverPort}...`)); + this.emit(Monitor.IntrinsicEvents.ServerRunning, { isFirstTime: !this.isInitialized }); + this.isInitialized = true; + } + this.shouldServerBeResponsive = true; + } catch (error) { + // if we expect the server to be unavailable, i.e. during compilation, + // the listening variable is false, activeExit will return early and the child + // process will continue + if (this.shouldServerBeResponsive) { + if (++this.pollingFailureCount > this.pollingFailureTolerance) { + this.proactiveUnplannedExit(error); + } else { + this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`)); + } + } + } finally { + resolve(); + } + }, 1000 * this.pollingIntervalSeconds); + }); + // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed + this.pollServer(); + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/repl.ts b/src/server/DashSession/Session/utilities/repl.ts new file mode 100644 index 000000000..643141286 --- /dev/null +++ b/src/server/DashSession/Session/utilities/repl.ts @@ -0,0 +1,128 @@ +import { createInterface, Interface } from "readline"; +import { red, green, white } from "colors"; + +export interface Configuration { + identifier: () => string | string; + onInvalid?: (command: string, validCommand: boolean) => string | string; + onValid?: (success?: string) => string | string; + isCaseSensitive?: boolean; +} + +export type ReplAction = (parsedArgs: Array) => any | Promise; +export interface Registration { + argPatterns: RegExp[]; + action: ReplAction; +} + +export default class Repl { + private identifier: () => string | string; + private onInvalid: ((command: string, validCommand: boolean) => string) | string; + private onValid: ((success: string) => string) | string; + private isCaseSensitive: boolean; + private commandMap = new Map(); + public interface: Interface; + private busy = false; + private keys: string | undefined; + + constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) { + this.identifier = prompt; + this.onInvalid = onInvalid || this.usage; + this.onValid = onValid || this.success; + this.isCaseSensitive = isCaseSensitive ?? true; + this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput); + } + + private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier(); + + private usage = (command: string, validCommand: boolean) => { + if (validCommand) { + const formatted = white(command); + const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n')); + return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`; + } else { + const resolved = this.keys; + if (resolved) { + return resolved; + } + const members: string[] = []; + const keys = this.commandMap.keys(); + let next: IteratorResult; + while (!(next = keys.next()).done) { + members.push(next.value); + } + return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`; + } + } + + private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`; + + public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { + const existing = this.commandMap.get(basename); + const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input)); + const registration = { argPatterns: converted, action }; + if (existing) { + existing.push(registration); + } else { + this.commandMap.set(basename, [registration]); + } + } + + private invalid = (command: string, validCommand: boolean) => { + console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand))); + this.busy = false; + } + + private valid = (command: string) => { + console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command))); + this.busy = false; + } + + private considerInput = async (line: string) => { + if (this.busy) { + console.log(red("Busy")); + return; + } + this.busy = true; + line = line.trim(); + if (this.isCaseSensitive) { + line = line.toLowerCase(); + } + const [command, ...args] = line.split(/\s+/g); + if (!command) { + return this.invalid(command, false); + } + const registered = this.commandMap.get(command); + if (registered) { + const { length } = args; + const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length); + for (const { argPatterns, action } of candidates) { + const parsed: string[] = []; + let matched = true; + if (length) { + for (let i = 0; i < length; i++) { + let matches: RegExpExecArray | null; + if ((matches = argPatterns[i].exec(args[i])) === null) { + matched = false; + break; + } + parsed.push(matches[0]); + } + } + if (!length || matched) { + const result = action(parsed); + const resolve = () => this.valid(`${command} ${parsed.join(" ")}`); + if (result instanceof Promise) { + result.then(resolve); + } else { + resolve(); + } + return; + } + } + this.invalid(command, true); + } else { + this.invalid(command, false); + } + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/session_config.ts b/src/server/DashSession/Session/utilities/session_config.ts new file mode 100644 index 000000000..b0e65dde4 --- /dev/null +++ b/src/server/DashSession/Session/utilities/session_config.ts @@ -0,0 +1,129 @@ +import { Schema } from "jsonschema"; +import { yellow, red, cyan, green, blue, magenta, Color, grey, gray, white, black } from "colors"; + +const colorPattern = /black|red|green|yellow|blue|magenta|cyan|white|gray|grey/; + +const identifierProperties: Schema = { + type: "object", + properties: { + text: { + type: "string", + minLength: 1 + }, + color: { + type: "string", + pattern: colorPattern + } + } +}; + +const portProperties: Schema = { + type: "number", + minimum: 1024, + maximum: 65535 +}; + +export const configurationSchema: Schema = { + id: "/configuration", + type: "object", + properties: { + showServerOutput: { type: "boolean" }, + ports: { + type: "object", + properties: { + server: portProperties, + socket: portProperties + }, + required: ["server"], + additionalProperties: true + }, + identifiers: { + type: "object", + properties: { + master: identifierProperties, + worker: identifierProperties, + exec: identifierProperties + } + }, + polling: { + type: "object", + additionalProperties: false, + properties: { + intervalSeconds: { + type: "number", + minimum: 1, + maximum: 86400 + }, + route: { + type: "string", + pattern: /\/[a-zA-Z]*/g + }, + failureTolerance: { + type: "number", + minimum: 0, + } + } + }, + } +}; + +type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black"; + +export const colorMapping: Map = new Map([ + ["yellow", yellow], + ["red", red], + ["cyan", cyan], + ["green", green], + ["blue", blue], + ["magenta", magenta], + ["grey", grey], + ["gray", gray], + ["white", white], + ["black", black] +]); + +interface Identifier { + text: string; + color: ColorLabel; +} + +export interface Identifiers { + master: Identifier; + worker: Identifier; + exec: Identifier; +} + +export interface Configuration { + showServerOutput: boolean; + identifiers: Identifiers; + ports: { [description: string]: number }; + polling: { + route: string; + intervalSeconds: number; + failureTolerance: number; + }; +} + +export const defaultConfig: Configuration = { + showServerOutput: false, + identifiers: { + master: { + text: "__monitor__", + color: "yellow" + }, + worker: { + text: "__server__", + color: "magenta" + }, + exec: { + text: "__exec__", + color: "green" + } + }, + ports: { server: 3000 }, + polling: { + route: "/", + intervalSeconds: 30, + failureTolerance: 0 + } +}; \ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/utilities.ts b/src/server/DashSession/Session/utilities/utilities.ts new file mode 100644 index 000000000..eb8de9d7e --- /dev/null +++ b/src/server/DashSession/Session/utilities/utilities.ts @@ -0,0 +1,37 @@ +import { v4 } from "uuid"; + +export namespace Utilities { + + export function guid() { + return v4(); + } + + /** + * At any arbitrary layer of nesting within the configuration objects, any single value that + * is not specified by the configuration is given the default counterpart. If, within an object, + * one peer is given by configuration and two are not, the one is preserved while the two are given + * the default value. + * @returns the composition of all of the assigned objects, much like Object.assign(), but with more + * granularity in the overwriting of nested objects + */ + export function preciseAssign(target: any, ...sources: any[]): any { + for (const source of sources) { + preciseAssignHelper(target, source); + } + return target; + } + + export function preciseAssignHelper(target: any, source: any) { + Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => { + let targetValue: any, sourceValue: any; + if (sourceValue = source[property]) { + if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") { + preciseAssignHelper(targetValue, sourceValue); + } else { + target[property] = sourceValue; + } + } + }); + } + +} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 2101de1d2..88f5fa3bf 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -24,7 +24,7 @@ import { Logger } from "./ProcessFactory"; import { yellow } from "colors"; import { DashSessionAgent } from "./DashSession/DashSessionAgent"; import SessionManager from "./ApiManagers/SessionManager"; -import { AppliedSessionAgent } from "resilient-server-session"; +import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; export const onWindows = process.platform === "win32"; export let sessionAgent: AppliedSessionAgent; -- cgit v1.2.3-70-g09d2