Refactor Debugger to use a PreviewDebuggerServer abstraction

This will allow the PreviewLaunchers to also start this debugger server (and not only give this ability to the Debugger editor tab).
In the future, this also allow the web-app to have a different implementation of a debugger server.

Don't show in changelog
This commit is contained in:
Florian Rival
2020-05-16 00:44:20 +02:00
committed by Florian Rival
parent 9c6972ec0a
commit 6326c185f4
8 changed files with 243 additions and 135 deletions

View File

@@ -3,7 +3,7 @@ import { t } from '@lingui/macro';
import * as React from 'react';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import { type DebuggerId } from '.';
import { type DebuggerId } from '../Export/PreviewLauncher.flow';
type Props = {|
selectedId: DebuggerId,

View File

@@ -10,13 +10,11 @@ import Text from '../UI/Text';
import PlaceholderLoader from '../UI/PlaceholderLoader';
import PlaceholderMessage from '../UI/PlaceholderMessage';
import Background from '../UI/Background';
import optionalRequire from '../Utils/OptionalRequire';
import EmptyMessage from '../UI/EmptyMessage';
const electron = optionalRequire('electron');
const ipcRenderer = electron ? electron.ipcRenderer : null;
//Each game connected to the debugger server is identified by a unique number
export type DebuggerId = number;
import {
type PreviewDebuggerServer,
type DebuggerId,
} from '../Export/PreviewLauncher.flow';
export type ProfilerMeasuresSection = {|
time: number,
@@ -34,13 +32,15 @@ type Props = {|
project: gdProject,
setToolbar: React.Node => void,
isActive: boolean,
previewDebuggerServer: PreviewDebuggerServer,
|};
type State = {|
debuggerServerStarted: boolean,
debuggerServerState: 'started' | 'stopped',
debuggerServerError: ?any,
debuggerIds: Array<DebuggerId>,
unregisterDebuggerServerCallbacks: ?() => void,
debuggerGameData: { [DebuggerId]: any },
profilerOutputs: { [DebuggerId]: ProfilerOutput },
profilingInProgress: { [DebuggerId]: boolean },
@@ -53,9 +53,10 @@ type State = {|
*/
export default class Debugger extends React.Component<Props, State> {
state = {
debuggerServerStarted: false,
debuggerServerState: this.props.previewDebuggerServer.getServerState(),
debuggerServerError: null,
debuggerIds: [],
debuggerIds: this.props.previewDebuggerServer.getExistingDebuggerIds(),
unregisterDebuggerServerCallbacks: null,
debuggerGameData: {},
profilerOutputs: {},
profilingInProgress: {},
@@ -94,88 +95,71 @@ export default class Debugger extends React.Component<Props, State> {
}
componentWillUnmount() {
this._removeServerListeners();
if (this.state.unregisterDebuggerServerCallbacks) {
this.state.unregisterDebuggerServerCallbacks();
}
}
_removeServerListeners = () => {
if (!ipcRenderer) return;
ipcRenderer.removeAllListeners('debugger-send-message-done');
ipcRenderer.removeAllListeners('debugger-error-received');
ipcRenderer.removeAllListeners('debugger-connection-closed');
ipcRenderer.removeAllListeners('debugger-connection-opened');
ipcRenderer.removeAllListeners('debugger-start-server-done');
ipcRenderer.removeAllListeners('debugger-message-received');
};
_startServer = () => {
if (!ipcRenderer) return;
const { previewDebuggerServer } = this.props;
const { unregisterDebuggerServerCallbacks } = this.state;
if (
unregisterDebuggerServerCallbacks &&
previewDebuggerServer.getServerState() === 'started'
)
return; // Server already started and callbacks registered
this.setState({
debuggerServerStarted: false,
});
this._removeServerListeners();
if (unregisterDebuggerServerCallbacks) unregisterDebuggerServerCallbacks(); // Unregister old callbacks, if any
ipcRenderer.on('debugger-error-received', (event, err) => {
this.setState(
{
debuggerServerError: err,
},
() => this.updateToolbar()
);
});
ipcRenderer.on('debugger-connection-closed', (event, { id }) => {
const { debuggerIds, selectedId } = this.state;
const remainingDebuggerIds = debuggerIds.filter(
debuggerId => debuggerId !== id
);
this.setState(
{
debuggerIds: remainingDebuggerIds,
selectedId:
selectedId !== id
? selectedId
: remainingDebuggerIds.length
? remainingDebuggerIds[remainingDebuggerIds.length - 1]
: selectedId,
},
() => this.updateToolbar()
);
});
ipcRenderer.on('debugger-connection-opened', (event, { id }) => {
this.setState(
{
debuggerIds: [...this.state.debuggerIds, id],
selectedId: id,
},
() => this.updateToolbar()
);
});
ipcRenderer.on('debugger-start-server-done', event => {
this.setState(
{
debuggerServerStarted: true,
},
() => this.updateToolbar()
);
});
ipcRenderer.on('debugger-message-received', (event, { id, message }) => {
console.log('Processing message received for debugger');
try {
const data = JSON.parse(message);
this._handleMessage(id, data);
} catch (e) {
console.warn(
'Error while parsing message received from debugger client:',
e
// Register new callbacks
const unregisterCallbacks = previewDebuggerServer.registerCallbacks({
onErrorReceived: err => {
this.setState(
{
debuggerServerError: err,
},
() => this.updateToolbar()
);
}
},
onConnectionClosed: ({ id, debuggerIds }) => {
this.setState(
({ selectedId }) => ({
debuggerIds,
selectedId:
selectedId !== id
? selectedId
: debuggerIds.length
? debuggerIds[debuggerIds.length - 1]
: selectedId,
}),
() => this.updateToolbar()
);
},
onConnectionOpened: ({ id, debuggerIds }) => {
this.setState(
{
debuggerIds,
selectedId: id,
},
() => this.updateToolbar()
);
},
onServerStateChanged: () => {
this.setState(
{
debuggerServerState: previewDebuggerServer.getServerState(),
},
() => this.updateToolbar()
);
},
onHandleParsedMessage: ({ id, parsedMessage }) => {
this._handleMessage(id, parsedMessage);
},
});
previewDebuggerServer.startServer();
this.setState({
unregisterDebuggerServerCallbacks: unregisterCallbacks,
});
ipcRenderer.send('debugger-start-server');
};
_handleMessage = (id: DebuggerId, data: any) => {
@@ -210,42 +194,26 @@ export default class Debugger extends React.Component<Props, State> {
};
_play = (id: DebuggerId) => {
if (!ipcRenderer) return;
ipcRenderer.send('debugger-send-message', {
id,
message: '{"command": "play"}',
});
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, { command: 'play' });
};
_pause = (id: DebuggerId) => {
if (!ipcRenderer) return;
ipcRenderer.send('debugger-send-message', {
id,
message: '{"command": "pause"}',
});
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, { command: 'pause' });
};
_refresh = (id: DebuggerId) => {
if (!ipcRenderer) return;
ipcRenderer.send('debugger-send-message', {
id,
message: '{"command": "refresh"}',
});
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, { command: 'refresh' });
};
_edit = (id: DebuggerId, path: Array<string>, newValue: any) => {
if (!ipcRenderer) return false;
ipcRenderer.send('debugger-send-message', {
id,
message: JSON.stringify({
command: 'set',
path,
newValue,
}),
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, {
command: 'set',
path,
newValue,
});
setTimeout(() => this._refresh(id), 100);
@@ -253,37 +221,28 @@ export default class Debugger extends React.Component<Props, State> {
};
_call = (id: DebuggerId, path: Array<string>, args: Array<any>) => {
if (!ipcRenderer) return false;
ipcRenderer.send('debugger-send-message', {
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(
id,
message: JSON.stringify({
JSON.stringify({
command: 'call',
path,
args,
}),
});
})
);
setTimeout(() => this._refresh(id), 100);
return true;
};
_startProfiler = (id: DebuggerId) => {
if (!ipcRenderer) return;
ipcRenderer.send('debugger-send-message', {
id,
message: '{"command": "profiler.start"}',
});
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, { command: 'profiler.start' });
};
_stopProfiler = (id: DebuggerId) => {
if (!ipcRenderer) return;
ipcRenderer.send('debugger-send-message', {
id,
message: '{"command": "profiler.stop"}',
});
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, { command: 'profiler.stop' });
};
_hasSelectedDebugger = () => {
@@ -294,7 +253,7 @@ export default class Debugger extends React.Component<Props, State> {
render() {
const {
debuggerServerError,
debuggerServerStarted,
debuggerServerState,
selectedId,
debuggerIds,
debuggerGameData,
@@ -304,7 +263,7 @@ export default class Debugger extends React.Component<Props, State> {
return (
<Background>
{!debuggerServerStarted && !debuggerServerError && (
{debuggerServerState === 'stopped' && !debuggerServerError && (
<PlaceholderMessage>
<PlaceholderLoader />
<Text>
@@ -312,7 +271,7 @@ export default class Debugger extends React.Component<Props, State> {
</Text>
</PlaceholderMessage>
)}
{!debuggerServerStarted && debuggerServerError && (
{debuggerServerState === 'stopped' && debuggerServerError && (
<PlaceholderMessage>
<Text>
<Trans>
@@ -322,7 +281,7 @@ export default class Debugger extends React.Component<Props, State> {
</Text>
</PlaceholderMessage>
)}
{debuggerServerStarted && (
{debuggerServerState === 'started' && (
<Column expand noMargin>
<DebuggerSelector
selectedId={selectedId}

View File

@@ -0,0 +1,110 @@
// @flow
import optionalRequire from '../../../Utils/OptionalRequire';
import {
type PreviewDebuggerServerCallbacks,
type PreviewDebuggerServer,
type DebuggerId,
} from '../../PreviewLauncher.flow';
const electron = optionalRequire('electron');
const ipcRenderer = electron ? electron.ipcRenderer : null;
let debuggerServerState: 'started' | 'stopped' = 'stopped';
const callbacksList: Array<PreviewDebuggerServerCallbacks> = [];
const debuggerIds: Array<DebuggerId> = [];
const removeServerListeners = () => {
if (!ipcRenderer) return;
ipcRenderer.removeAllListeners('debugger-send-message-done');
ipcRenderer.removeAllListeners('debugger-error-received');
ipcRenderer.removeAllListeners('debugger-connection-closed');
ipcRenderer.removeAllListeners('debugger-connection-opened');
ipcRenderer.removeAllListeners('debugger-start-server-done');
ipcRenderer.removeAllListeners('debugger-message-received');
};
/**
* A debugger server implemented using Electron (this one is just a bridge to it,
* communicating through events with it).
*/
export const LocalPreviewDebuggerServer: PreviewDebuggerServer = {
startServer: () => {
if (!ipcRenderer) return;
if (debuggerServerState === 'started') return;
debuggerServerState = 'stopped';
removeServerListeners();
ipcRenderer.on('debugger-error-received', (event, err) => {
callbacksList.forEach(({ onErrorReceived }) => onErrorReceived(err));
});
ipcRenderer.on('debugger-connection-closed', (event, { id }) => {
const debuggerIdIndex = debuggerIds.indexOf(id);
if (debuggerIdIndex !== -1) debuggerIds.splice(debuggerIdIndex, 1);
callbacksList.forEach(({ onConnectionClosed }) =>
onConnectionClosed({
id,
debuggerIds,
})
);
});
ipcRenderer.on('debugger-connection-opened', (event, { id }) => {
debuggerIds.push(id);
callbacksList.forEach(({ onConnectionOpened }) =>
onConnectionOpened({
id,
debuggerIds,
})
);
});
ipcRenderer.on('debugger-start-server-done', event => {
console.info('Local preview debugger started');
debuggerServerState = 'started';
callbacksList.forEach(({ onServerStateChanged }) =>
onServerStateChanged()
);
});
ipcRenderer.on('debugger-message-received', (event, { id, message }) => {
console.info('Processing message received for debugger');
try {
const parsedMessage = JSON.parse(message);
callbacksList.forEach(({ onHandleParsedMessage }) =>
onHandleParsedMessage({ id, parsedMessage })
);
} catch (e) {
console.warn(
'Error while parsing message received from debugger client:',
e
);
}
});
ipcRenderer.send('debugger-start-server');
},
sendMessage: (id: DebuggerId, message: Object) => {
if (!ipcRenderer) return;
if (debuggerServerState === 'stopped') {
console.error('Cannot send message when debugger server is stopped.');
return;
}
ipcRenderer.send('debugger-send-message', {
id,
message: JSON.stringify(message),
});
},
getServerState: () => debuggerServerState,
getExistingDebuggerIds: () => debuggerIds,
registerCallbacks: (callbacks: PreviewDebuggerServerCallbacks) => {
callbacksList.push(callbacks);
return () => {
const callbacksIndex = callbacksList.indexOf(callbacks);
if (callbacksIndex !== -1) callbacksList.splice(callbacksIndex, 1);
};
},
};

View File

@@ -11,6 +11,7 @@ import assignIn from 'lodash/assignIn';
import { type PreviewOptions } from '../../PreviewLauncher.flow';
import { findLocalIp } from './LocalIpFinder';
import SubscriptionChecker from '../../../Profile/SubscriptionChecker';
import { LocalPreviewDebuggerServer } from './LocalPreviewDebuggerServer';
const electron = optionalRequire('electron');
const path = optionalRequire('path');
const ipcRenderer = electron ? electron.ipcRenderer : null;
@@ -175,6 +176,8 @@ export default class LocalPreviewLauncher extends React.Component<
});
};
getPreviewDebuggerServer() { return LocalPreviewDebuggerServer; }
_checkSubscription = () => {
if (!this._subscriptionChecker) return true;

View File

@@ -12,6 +12,7 @@ export type PreviewOptions = {|
export type PreviewLauncherInterface = {
launchPreview: (options: PreviewOptions) => Promise<any>,
canDoNetworkPreview: () => boolean,
+getPreviewDebuggerServer: () => PreviewDebuggerServer,
};
/** The props that PreviewLauncher must support */
@@ -28,3 +29,30 @@ export type PreviewLauncherComponent = React.AbstractComponent<
PreviewLauncherProps,
PreviewLauncherInterface
>;
/** Each game connected to the debugger server is identified by a unique number. */
export type DebuggerId = number;
/** The callbacks for a debugger server used for previews. */
export type PreviewDebuggerServerCallbacks = {|
onErrorReceived: (err: Error) => void,
onServerStateChanged: () => void,
onConnectionClosed: ({|
id: DebuggerId,
debuggerIds: Array<DebuggerId>,
|}) => void,
onConnectionOpened: ({|
id: DebuggerId,
debuggerIds: Array<DebuggerId>,
|}) => void,
onHandleParsedMessage: ({| id: DebuggerId, parsedMessage: Object |}) => void,
|};
/** Interface to run a debugger server for previews. */
export type PreviewDebuggerServer = {|
startServer: () => void,
getServerState: () => 'started' | 'stopped',
getExistingDebuggerIds: () => Array<DebuggerId>,
sendMessage: (id: DebuggerId, message: Object) => void,
registerCallbacks: (callbacks: PreviewDebuggerServerCallbacks) => () => void,
|};

View File

@@ -6,6 +6,9 @@ import {
type ResourceSource,
type ChooseResourceFunction,
} from '../../ResourcesList/ResourceSource.flow';
import {
type PreviewDebuggerServer,
} from '../../Export/PreviewLauncher.flow';
import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow';
export type RenderEditorContainerProps = {|
@@ -29,6 +32,7 @@ export type RenderEditorContainerProps = {|
layoutName: ?string,
externalLayoutName?: ?string
) => void,
previewDebuggerServer: PreviewDebuggerServer,
// Opening other editors:
onOpenDebugger: () => void,

View File

@@ -76,6 +76,7 @@ export class DebuggerEditorContainer extends React.Component<
project={project}
setToolbar={this.props.setToolbar}
isActive={this.props.isActive}
previewDebuggerServer={this.props.previewDebuggerServer}
ref={editor => (this.editor = editor)}
/>
<SubscriptionChecker

View File

@@ -1655,6 +1655,9 @@ const MainFrame = (props: Props) => {
setPreviewedLayout,
onOpenDebugger: openDebugger,
onOpenExternalEvents: openExternalEvents,
previewDebuggerServer:
_previewLauncher.current &&
_previewLauncher.current.getPreviewDebuggerServer(),
onOpenLayout: name =>
openLayout(name, {
openEventsEditor: true,