diff --git a/packages/protocol/src/browser/client.ts b/packages/protocol/src/browser/client.ts index b20dea23..9323df11 100644 --- a/packages/protocol/src/browser/client.ts +++ b/packages/protocol/src/browser/client.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events"; import { Emitter } from "@coder/events"; import { logger, field } from "@coder/logger"; -import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, ClientMessage, WorkingInitMessage, EvalEventMessage } from "../proto"; +import { Ping, NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, ClientMessage, WorkingInitMessage, EvalEventMessage } from "../proto"; import { ReadWriteConnection, InitData, OperatingSystem, SharedProcessData } from "../common/connection"; import { ActiveEvalHelper, EvalHelper, Disposer, ServerActiveEvalHelper } from "../common/helpers"; import { stringify, parse } from "../common/util"; @@ -22,6 +22,11 @@ export class Client { private readonly sharedProcessActiveEmitter = new Emitter(); public readonly onSharedProcessActive = this.sharedProcessActiveEmitter.event; + // The socket timeout is 60s, so we need to send a ping periodically to + // prevent it from closing. + private pingTimeout: NodeJS.Timer | number | undefined; + private readonly pingTimeoutDelay = 30000; + /** * @param connection Established connection to the server */ @@ -43,9 +48,16 @@ export class Client { } }); + connection.onClose(() => { + clearTimeout(this.pingTimeout as any); // tslint:disable-line no-any + this.pingTimeout = undefined; + }); + this.initDataPromise = new Promise((resolve): void => { this.initDataEmitter.event(resolve); }); + + this.startPinging(); } public dispose(): void { @@ -214,6 +226,28 @@ export class Client { socketPath: sharedProcessActiveMessage.getSocketPath(), logPath: sharedProcessActiveMessage.getLogPath(), }); + } else if (message.hasPong()) { + // Nothing to do since we run the pings on a timer, in case either message + // is dropped which would break the ping cycle. + } else { + throw new Error("unknown message type"); } } + + private startPinging = (): void => { + if (typeof this.pingTimeout !== "undefined") { + return; + } + + const schedulePing = (): void => { + this.pingTimeout = setTimeout(() => { + const clientMsg = new ClientMessage(); + clientMsg.setPing(new Ping()); + this.connection.send(clientMsg.serializeBinary()); + schedulePing(); + }, this.pingTimeoutDelay); + }; + + schedulePing(); + } } diff --git a/packages/protocol/src/node/server.ts b/packages/protocol/src/node/server.ts index 62fd7354..ac973163 100644 --- a/packages/protocol/src/node/server.ts +++ b/packages/protocol/src/node/server.ts @@ -3,7 +3,7 @@ import * as path from "path"; import { mkdir } from "fs"; import { promisify } from "util"; import { logger, field } from "@coder/logger"; -import { ClientMessage, WorkingInitMessage, ServerMessage } from "../proto"; +import { Pong, ClientMessage, WorkingInitMessage, ServerMessage } from "../proto"; import { evaluate, ActiveEvaluation } from "./evaluate"; import { ForkProvider } from "../common/helpers"; import { ReadWriteConnection } from "../common/connection"; @@ -116,6 +116,11 @@ export class Server { return; } e.onEvent(evalEventMessage); + } else if (message.hasPing()) { + logger.trace("ping"); + const srvMsg = new ServerMessage(); + srvMsg.setPong(new Pong()); + this.connection.send(srvMsg.serializeBinary()); } else { throw new Error("unknown message type"); } diff --git a/packages/protocol/src/proto/client.proto b/packages/protocol/src/proto/client.proto index b8d62457..a29839ab 100644 --- a/packages/protocol/src/proto/client.proto +++ b/packages/protocol/src/proto/client.proto @@ -7,6 +7,8 @@ message ClientMessage { // node.proto NewEvalMessage new_eval = 11; EvalEventMessage eval_event = 12; + + Ping ping = 13; } } @@ -21,6 +23,8 @@ message ServerMessage { // vscode.proto SharedProcessActiveMessage shared_process_active = 17; + + Pong pong = 18; } } diff --git a/packages/protocol/src/proto/client_pb.d.ts b/packages/protocol/src/proto/client_pb.d.ts index 2f91ebfd..ce223fff 100644 --- a/packages/protocol/src/proto/client_pb.d.ts +++ b/packages/protocol/src/proto/client_pb.d.ts @@ -16,6 +16,11 @@ export class ClientMessage extends jspb.Message { getEvalEvent(): node_pb.EvalEventMessage | undefined; setEvalEvent(value?: node_pb.EvalEventMessage): void; + hasPing(): boolean; + clearPing(): void; + getPing(): node_pb.Ping | undefined; + setPing(value?: node_pb.Ping): void; + getMsgCase(): ClientMessage.MsgCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ClientMessage.AsObject; @@ -31,12 +36,14 @@ export namespace ClientMessage { export type AsObject = { newEval?: node_pb.NewEvalMessage.AsObject, evalEvent?: node_pb.EvalEventMessage.AsObject, + ping?: node_pb.Ping.AsObject, } export enum MsgCase { MSG_NOT_SET = 0, NEW_EVAL = 11, EVAL_EVENT = 12, + PING = 13, } } @@ -66,6 +73,11 @@ export class ServerMessage extends jspb.Message { getSharedProcessActive(): vscode_pb.SharedProcessActiveMessage | undefined; setSharedProcessActive(value?: vscode_pb.SharedProcessActiveMessage): void; + hasPong(): boolean; + clearPong(): void; + getPong(): node_pb.Pong | undefined; + setPong(value?: node_pb.Pong): void; + getMsgCase(): ServerMessage.MsgCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ServerMessage.AsObject; @@ -84,6 +96,7 @@ export namespace ServerMessage { evalEvent?: node_pb.EvalEventMessage.AsObject, init?: WorkingInitMessage.AsObject, sharedProcessActive?: vscode_pb.SharedProcessActiveMessage.AsObject, + pong?: node_pb.Pong.AsObject, } export enum MsgCase { @@ -93,6 +106,7 @@ export namespace ServerMessage { EVAL_EVENT = 15, INIT = 16, SHARED_PROCESS_ACTIVE = 17, + PONG = 18, } } diff --git a/packages/protocol/src/proto/client_pb.js b/packages/protocol/src/proto/client_pb.js index 798ac203..2d451abd 100644 --- a/packages/protocol/src/proto/client_pb.js +++ b/packages/protocol/src/proto/client_pb.js @@ -43,7 +43,7 @@ if (goog.DEBUG && !COMPILED) { * @private {!Array>} * @const */ -proto.ClientMessage.oneofGroups_ = [[11,12]]; +proto.ClientMessage.oneofGroups_ = [[11,12,13]]; /** * @enum {number} @@ -51,7 +51,8 @@ proto.ClientMessage.oneofGroups_ = [[11,12]]; proto.ClientMessage.MsgCase = { MSG_NOT_SET: 0, NEW_EVAL: 11, - EVAL_EVENT: 12 + EVAL_EVENT: 12, + PING: 13 }; /** @@ -91,7 +92,8 @@ proto.ClientMessage.prototype.toObject = function(opt_includeInstance) { proto.ClientMessage.toObject = function(includeInstance, msg) { var f, obj = { newEval: (f = msg.getNewEval()) && node_pb.NewEvalMessage.toObject(includeInstance, f), - evalEvent: (f = msg.getEvalEvent()) && node_pb.EvalEventMessage.toObject(includeInstance, f) + evalEvent: (f = msg.getEvalEvent()) && node_pb.EvalEventMessage.toObject(includeInstance, f), + ping: (f = msg.getPing()) && node_pb.Ping.toObject(includeInstance, f) }; if (includeInstance) { @@ -138,6 +140,11 @@ proto.ClientMessage.deserializeBinaryFromReader = function(msg, reader) { reader.readMessage(value,node_pb.EvalEventMessage.deserializeBinaryFromReader); msg.setEvalEvent(value); break; + case 13: + var value = new node_pb.Ping; + reader.readMessage(value,node_pb.Ping.deserializeBinaryFromReader); + msg.setPing(value); + break; default: reader.skipField(); break; @@ -183,6 +190,14 @@ proto.ClientMessage.serializeBinaryToWriter = function(message, writer) { node_pb.EvalEventMessage.serializeBinaryToWriter ); } + f = message.getPing(); + if (f != null) { + writer.writeMessage( + 13, + f, + node_pb.Ping.serializeBinaryToWriter + ); + } }; @@ -246,6 +261,36 @@ proto.ClientMessage.prototype.hasEvalEvent = function() { }; +/** + * optional Ping ping = 13; + * @return {?proto.Ping} + */ +proto.ClientMessage.prototype.getPing = function() { + return /** @type{?proto.Ping} */ ( + jspb.Message.getWrapperField(this, node_pb.Ping, 13)); +}; + + +/** @param {?proto.Ping|undefined} value */ +proto.ClientMessage.prototype.setPing = function(value) { + jspb.Message.setOneofWrapperField(this, 13, proto.ClientMessage.oneofGroups_[0], value); +}; + + +proto.ClientMessage.prototype.clearPing = function() { + this.setPing(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.ClientMessage.prototype.hasPing = function() { + return jspb.Message.getField(this, 13) != null; +}; + + /** * Generated by JsPbCodeGenerator. @@ -272,7 +317,7 @@ if (goog.DEBUG && !COMPILED) { * @private {!Array>} * @const */ -proto.ServerMessage.oneofGroups_ = [[13,14,15,16,17]]; +proto.ServerMessage.oneofGroups_ = [[13,14,15,16,17,18]]; /** * @enum {number} @@ -283,7 +328,8 @@ proto.ServerMessage.MsgCase = { EVAL_DONE: 14, EVAL_EVENT: 15, INIT: 16, - SHARED_PROCESS_ACTIVE: 17 + SHARED_PROCESS_ACTIVE: 17, + PONG: 18 }; /** @@ -326,7 +372,8 @@ proto.ServerMessage.toObject = function(includeInstance, msg) { evalDone: (f = msg.getEvalDone()) && node_pb.EvalDoneMessage.toObject(includeInstance, f), evalEvent: (f = msg.getEvalEvent()) && node_pb.EvalEventMessage.toObject(includeInstance, f), init: (f = msg.getInit()) && proto.WorkingInitMessage.toObject(includeInstance, f), - sharedProcessActive: (f = msg.getSharedProcessActive()) && vscode_pb.SharedProcessActiveMessage.toObject(includeInstance, f) + sharedProcessActive: (f = msg.getSharedProcessActive()) && vscode_pb.SharedProcessActiveMessage.toObject(includeInstance, f), + pong: (f = msg.getPong()) && node_pb.Pong.toObject(includeInstance, f) }; if (includeInstance) { @@ -388,6 +435,11 @@ proto.ServerMessage.deserializeBinaryFromReader = function(msg, reader) { reader.readMessage(value,vscode_pb.SharedProcessActiveMessage.deserializeBinaryFromReader); msg.setSharedProcessActive(value); break; + case 18: + var value = new node_pb.Pong; + reader.readMessage(value,node_pb.Pong.deserializeBinaryFromReader); + msg.setPong(value); + break; default: reader.skipField(); break; @@ -457,6 +509,14 @@ proto.ServerMessage.serializeBinaryToWriter = function(message, writer) { vscode_pb.SharedProcessActiveMessage.serializeBinaryToWriter ); } + f = message.getPong(); + if (f != null) { + writer.writeMessage( + 18, + f, + node_pb.Pong.serializeBinaryToWriter + ); + } }; @@ -610,6 +670,36 @@ proto.ServerMessage.prototype.hasSharedProcessActive = function() { }; +/** + * optional Pong pong = 18; + * @return {?proto.Pong} + */ +proto.ServerMessage.prototype.getPong = function() { + return /** @type{?proto.Pong} */ ( + jspb.Message.getWrapperField(this, node_pb.Pong, 18)); +}; + + +/** @param {?proto.Pong|undefined} value */ +proto.ServerMessage.prototype.setPong = function(value) { + jspb.Message.setOneofWrapperField(this, 18, proto.ServerMessage.oneofGroups_[0], value); +}; + + +proto.ServerMessage.prototype.clearPong = function() { + this.setPong(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.ServerMessage.prototype.hasPong = function() { + return jspb.Message.getField(this, 18) != null; +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/packages/protocol/src/proto/node.proto b/packages/protocol/src/proto/node.proto index e71a04f0..e0610761 100644 --- a/packages/protocol/src/proto/node.proto +++ b/packages/protocol/src/proto/node.proto @@ -26,3 +26,7 @@ message EvalDoneMessage { uint64 id = 1; string response = 2; } + +message Ping {} + +message Pong {} diff --git a/packages/protocol/src/proto/node_pb.d.ts b/packages/protocol/src/proto/node_pb.d.ts index 1b36e86b..7a1a8744 100644 --- a/packages/protocol/src/proto/node_pb.d.ts +++ b/packages/protocol/src/proto/node_pb.d.ts @@ -119,3 +119,35 @@ export namespace EvalDoneMessage { } } +export class Ping extends jspb.Message { + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): Ping.AsObject; + static toObject(includeInstance: boolean, msg: Ping): Ping.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: Ping, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): Ping; + static deserializeBinaryFromReader(message: Ping, reader: jspb.BinaryReader): Ping; +} + +export namespace Ping { + export type AsObject = { + } +} + +export class Pong extends jspb.Message { + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): Pong.AsObject; + static toObject(includeInstance: boolean, msg: Pong): Pong.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: Pong, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): Pong; + static deserializeBinaryFromReader(message: Pong, reader: jspb.BinaryReader): Pong; +} + +export namespace Pong { + export type AsObject = { + } +} + diff --git a/packages/protocol/src/proto/node_pb.js b/packages/protocol/src/proto/node_pb.js index 289eb9a9..62585b06 100644 --- a/packages/protocol/src/proto/node_pb.js +++ b/packages/protocol/src/proto/node_pb.js @@ -15,6 +15,8 @@ goog.exportSymbol('proto.EvalDoneMessage', null, global); goog.exportSymbol('proto.EvalEventMessage', null, global); goog.exportSymbol('proto.EvalFailedMessage', null, global); goog.exportSymbol('proto.NewEvalMessage', null, global); +goog.exportSymbol('proto.Ping', null, global); +goog.exportSymbol('proto.Pong', null, global); /** * Generated by JsPbCodeGenerator. @@ -843,4 +845,236 @@ proto.EvalDoneMessage.prototype.setResponse = function(value) { }; + +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.Ping = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.Ping, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.Ping.displayName = 'proto.Ping'; +} + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.Ping.prototype.toObject = function(opt_includeInstance) { + return proto.Ping.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.Ping} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.Ping.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.Ping} + */ +proto.Ping.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.Ping; + return proto.Ping.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.Ping} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.Ping} + */ +proto.Ping.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.Ping.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.Ping.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.Ping} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.Ping.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.Pong = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.Pong, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.Pong.displayName = 'proto.Pong'; +} + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.Pong.prototype.toObject = function(opt_includeInstance) { + return proto.Pong.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.Pong} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.Pong.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.Pong} + */ +proto.Pong.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.Pong; + return proto.Pong.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.Pong} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.Pong} + */ +proto.Pong.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.Pong.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.Pong.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.Pong} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.Pong.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + goog.object.extend(exports, proto);