Files
GDevelop/GDJS/Runtime/debugger-client/websocket-debugger-client.ts
T
Aurélien Vivet 9d0cbebdec Make disposed debugger clients inert
Detach the WebSocket handlers and cancel the pending hard reload before
closing, drop messages sent after dispose, and release the debugger client
and in-game editor references when the game is disposed.
2026-10-08 15:18:27 +02:00

134 lines
4.9 KiB
TypeScript

namespace gdjs {
const logger = new gdjs.Logger('Debugger client (websocket)');
/**
* This debugger client connects to a websocket server, exchanging
* and receiving messages with this server.
* @category Debugging > Debugger Client
*/
export class WebsocketDebuggerClient extends gdjs.AbstractDebuggerClient {
_ws: WebSocket | null;
constructor(runtimeGame: RuntimeGame) {
super(runtimeGame);
this._ws = null;
if (typeof WebSocket === 'undefined') {
logger.log("WebSocket is not defined, the debugger won't work.");
return;
}
const that = this;
try {
// Find the WebSocket server to connect to using the address that was stored
// in the options by the editor. If not, try the default address, though it's unlikely
// to work - which is ok, the game can run without a debugger server.
const runtimeGameOptions = runtimeGame.getAdditionalOptions();
const address =
(runtimeGameOptions &&
runtimeGameOptions.websocketDebuggerServerAddress) ||
'127.0.0.1';
const port =
(runtimeGameOptions &&
runtimeGameOptions.websocketDebuggerServerPort) ||
'3030';
this._ws = new WebSocket('ws://' + address + ':' + port + '/');
} catch {
try {
this._ws = new WebSocket(
(location.protocol === 'https:' ? 'wss://' : 'ws://') +
location.hostname +
':' +
location.port +
'/'
);
} catch {
try {
this._ws = new WebSocket('ws://localhost:3030/');
} catch {
logger.log(
"WebSocket could not initialize, debugger/hot-reload won't work."
);
return;
}
}
}
this._ws.onopen = function open() {
logger.info('Debugger connection open');
};
this._ws.onclose = function close() {
logger.info('Debugger connection closed');
if (that._runtimegame.isInGameEdition()) {
// Sometimes, for example if the editor is launched for a long time and the device goes to sleep,
// the WebSocket connection between the editor and the game is closed. When we are in in-game edition,
// we can't afford to lose the connection because it means the editor is unusable.
// In this case, we hard reload the game to re-establish a new connection.
that._hardReloadTimeoutId = setTimeout(() => {
logger.info(
'Debugger connection closed while in in-game edition - this is suspicious so hard reloading to re-establish a new connection.'
);
that.launchHardReload();
}, 1000);
}
};
this._ws.onerror = function errored(error) {
logger.warn('Debugger client error:', error);
};
this._ws.onmessage = function incoming(message) {
let data: any = null;
try {
data = JSON.parse(message.data);
} catch (error) {
logger.info('Debugger received a badly formatted message:', error);
}
that.handleCommand(data);
};
}
private hasLoggedError: boolean = false;
private _wasDisposed: boolean = false;
/** The hard reload scheduled when the connection is lost in in-game edition. */
private _hardReloadTimeoutId: NodeJS.Timeout | null = null;
dispose(): void {
this._wasDisposed = true;
if (this._hardReloadTimeoutId !== null) {
clearTimeout(this._hardReloadTimeoutId);
this._hardReloadTimeoutId = null;
}
if (this._ws) {
// Detach the handlers before closing: `close()` fires `onclose` (and
// `onerror` if the connection was still being established), which must
// not trigger the hard reload done when the connection is lost in
// in-game edition.
this._ws.onopen = null;
this._ws.onclose = null;
this._ws.onerror = null;
this._ws.onmessage = null;
this._ws.close();
}
}
protected _sendMessage(message: string) {
// Once disposed, messages are silently dropped: the connection is closed
// and logging a warning would call this method again.
if (this._wasDisposed) return;
if (!this._ws) {
// The error can be logged only once, since logger.warn will call this function again,
// leading to an endless recursive call if we do not call it only once.
if (!this.hasLoggedError) {
this.hasLoggedError = true;
logger.warn('No connection to debugger opened to send a message.');
}
return;
}
if (this._ws.readyState === WebSocket.OPEN) this._ws.send(message);
}
}
//Register the class to let the engine use it.
/** @category Debugging > Debugger Client */
// @ts-ignore
export const DebuggerClient = WebsocketDebuggerClient;
}