diff --git a/newIDE/app/src/Debugger/DebuggerSelector.js b/newIDE/app/src/Debugger/DebuggerSelector.js index cf75486386..f134fb13ea 100644 --- a/newIDE/app/src/Debugger/DebuggerSelector.js +++ b/newIDE/app/src/Debugger/DebuggerSelector.js @@ -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, diff --git a/newIDE/app/src/Debugger/index.js b/newIDE/app/src/Debugger/index.js index ea6cf2ac27..4a91e65432 100644 --- a/newIDE/app/src/Debugger/index.js +++ b/newIDE/app/src/Debugger/index.js @@ -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, + unregisterDebuggerServerCallbacks: ?() => void, + debuggerGameData: { [DebuggerId]: any }, profilerOutputs: { [DebuggerId]: ProfilerOutput }, profilingInProgress: { [DebuggerId]: boolean }, @@ -53,9 +53,10 @@ type State = {| */ export default class Debugger extends React.Component { 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 { } 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 { }; _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, 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 { }; _call = (id: DebuggerId, path: Array, args: Array) => { - 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 { render() { const { debuggerServerError, - debuggerServerStarted, + debuggerServerState, selectedId, debuggerIds, debuggerGameData, @@ -304,7 +263,7 @@ export default class Debugger extends React.Component { return ( - {!debuggerServerStarted && !debuggerServerError && ( + {debuggerServerState === 'stopped' && !debuggerServerError && ( @@ -312,7 +271,7 @@ export default class Debugger extends React.Component { )} - {!debuggerServerStarted && debuggerServerError && ( + {debuggerServerState === 'stopped' && debuggerServerError && ( @@ -322,7 +281,7 @@ export default class Debugger extends React.Component { )} - {debuggerServerStarted && ( + {debuggerServerState === 'started' && ( = []; +const debuggerIds: Array = []; + +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); + }; + }, +}; diff --git a/newIDE/app/src/Export/LocalExporters/LocalPreviewLauncher/index.js b/newIDE/app/src/Export/LocalExporters/LocalPreviewLauncher/index.js index 60ab9e5dca..b584c8c7e4 100644 --- a/newIDE/app/src/Export/LocalExporters/LocalPreviewLauncher/index.js +++ b/newIDE/app/src/Export/LocalExporters/LocalPreviewLauncher/index.js @@ -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; diff --git a/newIDE/app/src/Export/PreviewLauncher.flow.js b/newIDE/app/src/Export/PreviewLauncher.flow.js index bec2bd93e0..3346af246d 100644 --- a/newIDE/app/src/Export/PreviewLauncher.flow.js +++ b/newIDE/app/src/Export/PreviewLauncher.flow.js @@ -12,6 +12,7 @@ export type PreviewOptions = {| export type PreviewLauncherInterface = { launchPreview: (options: PreviewOptions) => Promise, 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, + |}) => void, + onConnectionOpened: ({| + id: DebuggerId, + debuggerIds: Array, + |}) => 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, + sendMessage: (id: DebuggerId, message: Object) => void, + registerCallbacks: (callbacks: PreviewDebuggerServerCallbacks) => () => void, +|}; diff --git a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js index 9121086f5e..f74063cbdc 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js +++ b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js @@ -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, diff --git a/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js index 05f9e550fe..5a68efaaa7 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/DebuggerEditorContainer.js @@ -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)} /> { setPreviewedLayout, onOpenDebugger: openDebugger, onOpenExternalEvents: openExternalEvents, + previewDebuggerServer: + _previewLauncher.current && + _previewLauncher.current.getPreviewDebuggerServer(), onOpenLayout: name => openLayout(name, { openEventsEditor: true,