remove references to testgame on the server, add gameId to router
This commit is contained in:
+7
-2
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import '@fontsource/roboto/300.css';
|
import '@fontsource/roboto/300.css';
|
||||||
import '@fontsource/roboto/400.css';
|
import '@fontsource/roboto/400.css';
|
||||||
@@ -10,10 +10,15 @@ import '@fontsource/roboto/700.css';
|
|||||||
import './reset.css';
|
import './reset.css';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
|
export const GameIdContext = React.createContext<string>(undefined);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const params = useParams();
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Outlet />
|
<GameIdContext.Provider value={params.gameId}>
|
||||||
|
<Outlet />
|
||||||
|
</GameIdContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { faLock, faLockOpen, faBook, faHammer, faBan } from '@fortawesome/free-solid-svg-icons'
|
import { faLock, faLockOpen, faBook, faHammer, faBan } from '@fortawesome/free-solid-svg-icons'
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
import { useLoadDocQuery, ComputedInventoryItem } from '../state/api';
|
import { useLoadDocQuery, ComputedInventoryItem } from '../state/api';
|
||||||
|
import { GameIdContext } from '../App';
|
||||||
|
|
||||||
export function Inventory({ tactics, lemmas, definitions, setInventoryDoc } :
|
export function Inventory({ tactics, lemmas, definitions, setInventoryDoc } :
|
||||||
{lemmas: ComputedInventoryItem[],
|
{lemmas: ComputedInventoryItem[],
|
||||||
@@ -77,8 +78,8 @@ function InventoryItem({name, displayName, locked, disabled, showDoc}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Documentation({name, type}) {
|
export function Documentation({name, type}) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
const doc = useLoadDocQuery({type: type, name: name})
|
const doc = useLoadDocQuery({game: gameId, type: type, name: name})
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<h2 className="doc">{doc.data?.displayName}</h2>
|
<h2 className="doc">{doc.data?.displayName}</h2>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import Markdown from './Markdown';
|
|||||||
|
|
||||||
import Split from 'react-split'
|
import Split from 'react-split'
|
||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
|
import { GameIdContext } from '../App';
|
||||||
|
|
||||||
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(null as any);
|
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(null as any);
|
||||||
|
|
||||||
@@ -125,9 +126,9 @@ function PlayableLevel({worldId, levelId}) {
|
|||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery()
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const level = useLoadLevelQuery({world: worldId, level: levelId})
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@@ -209,8 +210,8 @@ function PlayableLevel({worldId, levelId}) {
|
|||||||
<Markdown>{level?.data?.conclusion}</Markdown>
|
<Markdown>{level?.data?.conclusion}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
{levelId >= gameInfo.data?.worldSize[worldId] ?
|
{levelId >= gameInfo.data?.worldSize[worldId] ?
|
||||||
<Button to={`/`}><FontAwesomeIcon icon={faHome} /></Button> :
|
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
||||||
<Button to={`/world/${worldId}/level/${levelId + 1}`}>
|
<Button to={`/game/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
||||||
Next <FontAwesomeIcon icon={faArrowRight} /></Button>}
|
Next <FontAwesomeIcon icon={faArrowRight} /></Button>}
|
||||||
|
|
||||||
</div>}
|
</div>}
|
||||||
@@ -230,7 +231,8 @@ function PlayableLevel({worldId, levelId}) {
|
|||||||
export default Level
|
export default Level
|
||||||
|
|
||||||
function Introduction({worldId}) {
|
function Introduction({worldId}) {
|
||||||
const gameInfo = useGetGameInfoQuery()
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div style={gameInfo.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
<div style={gameInfo.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
||||||
@@ -245,8 +247,8 @@ function Introduction({worldId}) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="conclusion">
|
<div className="conclusion">
|
||||||
{0 == gameInfo.data?.worldSize[worldId] ?
|
{0 == gameInfo.data?.worldSize[worldId] ?
|
||||||
<Button to={`/`}><FontAwesomeIcon icon={faHome} /></Button> :
|
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
|
||||||
<Button to={`/world/${worldId}/level/1`}>
|
<Button to={`/game/${gameId}/world/${worldId}/level/1`}>
|
||||||
Start <FontAwesomeIcon icon={faArrowRight} />
|
Start <FontAwesomeIcon icon={faArrowRight} />
|
||||||
</Button>}
|
</Button>}
|
||||||
</div>
|
</div>
|
||||||
@@ -255,11 +257,12 @@ function Introduction({worldId}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
|
function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
|
||||||
const gameInfo = useGetGameInfoQuery()
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
|
||||||
<div>
|
<div>
|
||||||
<Button to={`/`}><FontAwesomeIcon icon={faHome} /></Button>
|
<Button to={`/game/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button>
|
||||||
<span className="app-bar-title">
|
<span className="app-bar-title">
|
||||||
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
|
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
|
||||||
</span>
|
</span>
|
||||||
@@ -269,10 +272,10 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
|
|||||||
{levelTitle}
|
{levelTitle}
|
||||||
</span>
|
</span>
|
||||||
<Button disabled={levelId <= 0} inverted={true}
|
<Button disabled={levelId <= 0} inverted={true}
|
||||||
to={`/world/${worldId}/level/${levelId - 1}`}
|
to={`/game/${gameId}/world/${worldId}/level/${levelId - 1}`}
|
||||||
><FontAwesomeIcon icon={faArrowLeft} /> Previous</Button>
|
><FontAwesomeIcon icon={faArrowLeft} /> Previous</Button>
|
||||||
<Button disabled={levelId >= gameInfo.data?.worldSize[worldId]} inverted={true}
|
<Button disabled={levelId >= gameInfo.data?.worldSize[worldId]} inverted={true}
|
||||||
to={`/world/${worldId}/level/${levelId + 1}`}
|
to={`/game/${gameId}/world/${worldId}/level/${levelId + 1}`}
|
||||||
>Next <FontAwesomeIcon icon={faArrowRight} /></Button>
|
>Next <FontAwesomeIcon icon={faArrowRight} /></Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -282,6 +285,7 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle}) {
|
|||||||
function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
||||||
|
|
||||||
const connection = React.useContext(ConnectionContext)
|
const connection = React.useContext(ConnectionContext)
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
||||||
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
||||||
@@ -308,7 +312,7 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
|
|||||||
theme: 'vs-code-theme-converted'
|
theme: 'vs-code-theme-converted'
|
||||||
})
|
})
|
||||||
|
|
||||||
const infoProvider = new InfoProvider(connection.getLeanClient())
|
const infoProvider = new InfoProvider(connection.getLeanClient(gameId))
|
||||||
|
|
||||||
const editorApi = infoProvider.getApi()
|
const editorApi = infoProvider.getApi()
|
||||||
|
|
||||||
@@ -358,7 +362,7 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
|
|||||||
return () => { infoProvider.dispose(); editor.dispose() }
|
return () => { infoProvider.dispose(); editor.dispose() }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const {leanClient, leanClientStarted} = useLeanClient()
|
const {leanClient, leanClientStarted} = useLeanClient(gameId)
|
||||||
|
|
||||||
// Create model when level changes
|
// Create model when level changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -391,7 +395,8 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo
|
|||||||
|
|
||||||
/** Open all files in this world on the server so that they will load faster when accessed */
|
/** Open all files in this world on the server so that they will load faster when accessed */
|
||||||
function useLoadWorldFiles(worldId) {
|
function useLoadWorldFiles(worldId) {
|
||||||
const gameInfo = useGetGameInfoQuery()
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ import { useGetGameInfoQuery } from '../state/api';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
import { selectCompleted } from '../state/progress';
|
import { selectCompleted } from '../state/progress';
|
||||||
|
import { GameIdContext } from '../App';
|
||||||
|
|
||||||
|
|
||||||
function LevelIcon({ worldId, levelId, position }) {
|
function LevelIcon({ worldId, levelId, position }) {
|
||||||
|
const gameId = React.useContext(GameIdContext)
|
||||||
const completed = useSelector(selectCompleted(worldId,levelId))
|
const completed = useSelector(selectCompleted(worldId,levelId))
|
||||||
// TODO: relative positioning?
|
// TODO: relative positioning?
|
||||||
return (
|
return (
|
||||||
<Link to={`/world/${worldId}/level/${levelId}`} key={`/world/${worldId}/level/${levelId}`}>
|
<Link to={`/game/${gameId}/${worldId}/level/${levelId}`} key={`/game/${gameId}/world/${worldId}/level/${levelId}`}>
|
||||||
<circle fill={completed ? "green" :"#aaa"} cx={position.x + Math.sin(levelId/5) * 9} cy={position.y - Math.cos(levelId/5) * 9} r="0.8" />
|
<circle fill={completed ? "green" :"#aaa"} cx={position.x + Math.sin(levelId/5) * 9} cy={position.y - Math.cos(levelId/5) * 9} r="0.8" />
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
@@ -29,7 +31,8 @@ function LevelIcon({ worldId, levelId, position }) {
|
|||||||
function Welcome() {
|
function Welcome() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery()
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
const { nodes, bounds }: any = gameInfo.data ? computeWorldLayout(gameInfo.data?.worlds) : {nodes: []}
|
const { nodes, bounds }: any = gameInfo.data ? computeWorldLayout(gameInfo.data?.worlds) : {nodes: []}
|
||||||
|
|
||||||
@@ -55,7 +58,7 @@ function Welcome() {
|
|||||||
let position: cytoscape.Position = nodes[id].position
|
let position: cytoscape.Position = nodes[id].position
|
||||||
|
|
||||||
svgElements.push(
|
svgElements.push(
|
||||||
<Link key={`world${id}`} to={`/world/${id}/level/0`}>
|
<Link key={`world${id}`} to={`/game/${gameId}/world/${id}/level/0`}>
|
||||||
<circle className="world-circle" cx={position.x} cy={position.y} r="8" />
|
<circle className="world-circle" cx={position.x} cy={position.y} r="8" />
|
||||||
<text className="world-name"
|
<text className="world-name"
|
||||||
x={position.x} y={position.y}>{nodes[id].data.title ? nodes[id].data.title : id}</text>
|
x={position.x} y={position.y}>{nodes[id].data.title ? nodes[id].data.title : id}</text>
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
|
|
||||||
export class Connection {
|
export class Connection {
|
||||||
private leanClient = null
|
private game: string = undefined // We only keep a connection to a single game at a time
|
||||||
|
private leanClient: LeanClient = null
|
||||||
getLeanClient(): LeanClient {
|
|
||||||
if (this.leanClient === null) {
|
|
||||||
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/'
|
|
||||||
|
|
||||||
|
getLeanClient(game): LeanClient {
|
||||||
|
if (this.game !== game) {
|
||||||
|
if (this.leanClient) {
|
||||||
|
this.leanClient.stop() // Stop previous Lean client
|
||||||
|
}
|
||||||
|
this.game = game
|
||||||
|
// Start a new Lean client for the new `gameId`.
|
||||||
|
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + game
|
||||||
const uri = monaco.Uri.parse('file:///')
|
const uri = monaco.Uri.parse('file:///')
|
||||||
this.leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
|
this.leanClient = new LeanClient(socketUrl, undefined, uri, () => {})
|
||||||
}
|
}
|
||||||
@@ -22,9 +27,9 @@ export class Connection {
|
|||||||
/** If not already started, starts the Lean client. resolves the returned promise as soon as a
|
/** If not already started, starts the Lean client. resolves the returned promise as soon as a
|
||||||
* Lean client is running.
|
* Lean client is running.
|
||||||
*/
|
*/
|
||||||
startLeanClient = () => {
|
startLeanClient = (game) => {
|
||||||
return new Promise<LeanClient>((resolve) => {
|
return new Promise<LeanClient>((resolve) => {
|
||||||
const leanClient = this.getLeanClient()
|
const leanClient = this.getLeanClient(game)
|
||||||
if (leanClient.isRunning()) {
|
if (leanClient.isRunning()) {
|
||||||
resolve(leanClient)
|
resolve(leanClient)
|
||||||
} else {
|
} else {
|
||||||
@@ -47,8 +52,8 @@ export const connection = new Connection()
|
|||||||
|
|
||||||
export const ConnectionContext = React.createContext(null);
|
export const ConnectionContext = React.createContext(null);
|
||||||
|
|
||||||
export const useLeanClient = () => {
|
export const useLeanClient = (gameId) => {
|
||||||
const leanClient = connection.getLeanClient()
|
const leanClient = connection.getLeanClient(gameId)
|
||||||
const [leanClientStarted, setLeanClientStarted] = useState(leanClient.isStarted())
|
const [leanClientStarted, setLeanClientStarted] = useState(leanClient.isStarted())
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -13,21 +13,26 @@ import ErrorPage from './ErrorPage';
|
|||||||
import Welcome from './components/Welcome';
|
import Welcome from './components/Welcome';
|
||||||
import Level from './components/Level';
|
import Level from './components/Level';
|
||||||
import { monacoSetup } from 'lean4web/client/src/monacoSetup';
|
import { monacoSetup } from 'lean4web/client/src/monacoSetup';
|
||||||
|
import { redirect } from 'react-router-dom';
|
||||||
|
|
||||||
monacoSetup()
|
monacoSetup()
|
||||||
|
|
||||||
const router = createHashRouter([
|
const router = createHashRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
loader: () => redirect("/game/testgame")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/game/:gameId",
|
||||||
element: <App />,
|
element: <App />,
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/game/:gameId",
|
||||||
element: <Welcome />,
|
element: <Welcome />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/world/:worldId/level/:levelId",
|
path: "/game/:gameId/world/:worldId/level/:levelId",
|
||||||
element: <Level />,
|
element: <Level />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ interface Doc {
|
|||||||
|
|
||||||
|
|
||||||
const customBaseQuery = async (
|
const customBaseQuery = async (
|
||||||
args : {method: string, params?: any},
|
args : {game: string, method: string, params?: any},
|
||||||
{ signal, dispatch, getState, extra },
|
{ signal, dispatch, getState, extra },
|
||||||
extraOptions
|
extraOptions
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const connection : Connection = extra.connection
|
const connection : Connection = extra.connection
|
||||||
let leanClient = await connection.startLeanClient()
|
let leanClient = await connection.startLeanClient(args.game)
|
||||||
console.log(`Sending request ${args.method}`)
|
console.log(`Sending request ${args.method}`)
|
||||||
let res = await leanClient.sendRequest(args.method, args.params)
|
let res = await leanClient.sendRequest(args.method, args.params)
|
||||||
console.log('Received response', res)
|
console.log('Received response', res)
|
||||||
@@ -59,14 +59,14 @@ export const apiSlice = createApi({
|
|||||||
reducerPath: 'gameApi',
|
reducerPath: 'gameApi',
|
||||||
baseQuery: customBaseQuery,
|
baseQuery: customBaseQuery,
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getGameInfo: builder.query<GameInfo, void>({
|
getGameInfo: builder.query<GameInfo, {game: string}>({
|
||||||
query: () => {return {method: 'info', params: {}}},
|
query: ({game}) => {return {game, method: 'info', params: {}}},
|
||||||
}),
|
}),
|
||||||
loadLevel: builder.query<LevelInfo, {world: string, level: number}>({
|
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
|
||||||
query: ({world, level}) => {return {method: "loadLevel", params: {world, level}}},
|
query: ({game, world, level}) => {return {game, method: "loadLevel", params: {world, level}}},
|
||||||
}),
|
}),
|
||||||
loadDoc: builder.query<Doc, {name: string, type: "lemma"|"tactic"}>({
|
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
|
||||||
query: ({name, type}) => {return {method: "loadDoc", params: {name, type}}},
|
query: ({game, name, type}) => {return {game, method: "loadDoc", params: {name, type}}},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
+41
-24
@@ -6,6 +6,18 @@ import * as url from 'url';
|
|||||||
import * as rpc from 'vscode-ws-jsonrpc';
|
import * as rpc from 'vscode-ws-jsonrpc';
|
||||||
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
|
||||||
|
|
||||||
|
const games = {
|
||||||
|
testgame: {
|
||||||
|
name: "TestGame",
|
||||||
|
module: "TestGame",
|
||||||
|
queueLength: 5
|
||||||
|
},
|
||||||
|
nng: {
|
||||||
|
name: "NNG",
|
||||||
|
module: "NNG",
|
||||||
|
queueLength: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const __filename = url.fileURLToPath(import.meta.url);
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||||
@@ -23,23 +35,18 @@ const wss = new WebSocketServer({ server })
|
|||||||
const environment = process.env.NODE_ENV
|
const environment = process.env.NODE_ENV
|
||||||
const isDevelopment = environment === 'development'
|
const isDevelopment = environment === 'development'
|
||||||
|
|
||||||
let cmd, cmdArgs, cwd;
|
/** We keep queues of started Lean Server processes to be ready when a user arrives */
|
||||||
if (isDevelopment) {
|
const queue = {}
|
||||||
cmd = "./gameserver";
|
|
||||||
cmdArgs = ["--server"];
|
|
||||||
cwd = "./leanserver/build/bin/"
|
|
||||||
} else{
|
|
||||||
cmd = "docker";
|
|
||||||
cmdArgs = ["run", "--runtime=runsc", "--network=none", "--rm", "-i", "testgame:latest"];
|
|
||||||
cwd = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
/** We keep a queue of started Lean Server processes to be ready when a user arrives */
|
|
||||||
const queue = []
|
|
||||||
const queueLength = 5
|
const queueLength = 5
|
||||||
|
|
||||||
function startServerProcess() {
|
function startServerProcess(gameId) {
|
||||||
const serverProcess = cp.spawn(cmd, cmdArgs, { cwd })
|
const serverProcess = isDevelopment
|
||||||
|
? cp.spawn("./gameserver",
|
||||||
|
["--server", gameId, games[gameId].module, games[gameId].name],
|
||||||
|
{ cwd: "./leanserver/build/bin/" })
|
||||||
|
: cp.spawn("docker",
|
||||||
|
["run", "--runtime=runsc", "--network=none", "--rm", "-i", `${gameId}:latest`],
|
||||||
|
{ cwd: "." })
|
||||||
serverProcess.on('error', error =>
|
serverProcess.on('error', error =>
|
||||||
console.error(`Launching Lean Server failed: ${error}`)
|
console.error(`Launching Lean Server failed: ${error}`)
|
||||||
);
|
);
|
||||||
@@ -52,22 +59,32 @@ function startServerProcess() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** start Lean Server processes to refill the queue */
|
/** start Lean Server processes to refill the queue */
|
||||||
function fillQueue() {
|
function fillQueue(gameId) {
|
||||||
while (queue.length < queueLength) {
|
while (queue[gameId].length < games[gameId].queueLength) {
|
||||||
const serverProcess = startServerProcess()
|
const serverProcess = startServerProcess(gameId)
|
||||||
queue.push(serverProcess)
|
queue[gameId].push(serverProcess)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fillQueue()
|
for (let gameId in games) {
|
||||||
|
queue[gameId] = []
|
||||||
|
fillQueue(gameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlRegEx = new RegExp("^/websocket/(.*)$")
|
||||||
|
|
||||||
|
wss.addListener("connection", function(ws, req) {
|
||||||
|
const reRes = urlRegEx.exec(req.url)
|
||||||
|
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return; }
|
||||||
|
const gameId = reRes[1]
|
||||||
|
if (!games[gameId]) { console.error(`Unknown game: ${gameId}`); return; }
|
||||||
|
|
||||||
wss.addListener("connection", function(ws) {
|
|
||||||
let ps;
|
let ps;
|
||||||
if (isDevelopment) { // Don't use queue in development
|
if (isDevelopment) { // Don't use queue in development
|
||||||
ps = startServerProcess()
|
ps = startServerProcess(gameId)
|
||||||
} else {
|
} else {
|
||||||
ps = queue.shift() // Pick the first Lean process; it's likely to be ready immediately
|
ps = queue[gameId].shift() // Pick the first Lean process; it's likely to be ready immediately
|
||||||
fillQueue()
|
fillQueue(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = {
|
const socket = {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ where addErrorMessage (info : SourceInfo) (s : MessageData) :=
|
|||||||
|
|
||||||
open Elab Meta Expr in
|
open Elab Meta Expr in
|
||||||
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets : Bool)
|
||||||
(couldBeEndSnap : Bool) (levelParams : Game.DidOpenLevelParams) : IO Snapshot := do
|
(couldBeEndSnap : Bool) (levelParams : Game.DidOpenLevelParams) (initParams : Lsp.InitializeParams): IO Snapshot := do
|
||||||
-- Recognize end snap
|
-- Recognize end snap
|
||||||
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
if inputCtx.input.atEnd snap.mpState.pos ∧ couldBeEndSnap then
|
||||||
let endSnap : Snapshot := {
|
let endSnap : Snapshot := {
|
||||||
@@ -142,7 +142,7 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets
|
|||||||
let (output, _) ← IO.FS.withIsolatedStreams (isolateStderr := server.stderrAsMessages.get snap.cmdState.scopes.head!.opts) <| liftM (m := BaseIO) do
|
let (output, _) ← IO.FS.withIsolatedStreams (isolateStderr := server.stderrAsMessages.get snap.cmdState.scopes.head!.opts) <| liftM (m := BaseIO) do
|
||||||
Elab.Command.catchExceptions
|
Elab.Command.catchExceptions
|
||||||
(getResetInfoTrees *> do
|
(getResetInfoTrees *> do
|
||||||
let some level ← GameServer.getLevelByFileName? inputCtx.fileName
|
let some level ← GameServer.getLevelByFileName? initParams inputCtx.fileName
|
||||||
| throwError "Level not found: {inputCtx.fileName}"
|
| throwError "Level not found: {inputCtx.fileName}"
|
||||||
let scope := level.scope
|
let scope := level.scope
|
||||||
|
|
||||||
@@ -244,7 +244,8 @@ where
|
|||||||
hOut.writeLspNotification { method := "$/game/completed", param }
|
hOut.writeLspNotification { method := "$/game/completed", param }
|
||||||
|
|
||||||
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
/-- Elaborates the next command after `parentSnap` and emits diagnostics into `hOut`. -/
|
||||||
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken) (levelParams : Game.DidOpenLevelParams)
|
private def nextSnap (ctx : WorkerContext) (m : DocumentMeta) (cancelTk : CancelToken)
|
||||||
|
(levelParams : Game.DidOpenLevelParams) (initParams : Lsp.InitializeParams)
|
||||||
: AsyncElabM (Option Snapshot) := do
|
: AsyncElabM (Option Snapshot) := do
|
||||||
cancelTk.check
|
cancelTk.check
|
||||||
let s ← get
|
let s ← get
|
||||||
@@ -261,7 +262,8 @@ where
|
|||||||
-- Make sure that there is at least one snap after the head snap, so that
|
-- Make sure that there is at least one snap after the head snap, so that
|
||||||
-- we can see the current goal even on an empty document
|
-- we can see the current goal even on an empty document
|
||||||
let couldBeEndSnap := s.snaps.size > 1
|
let couldBeEndSnap := s.snaps.size > 1
|
||||||
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap levelParams
|
let snap ← compileProof m.mkInputContext lastSnap ctx.clientHasWidgets couldBeEndSnap
|
||||||
|
levelParams initParams
|
||||||
set { s with snaps := s.snaps.push snap }
|
set { s with snaps := s.snaps.push snap }
|
||||||
-- TODO(MH): check for interrupt with increased precision
|
-- TODO(MH): check for interrupt with increased precision
|
||||||
cancelTk.check
|
cancelTk.check
|
||||||
@@ -299,7 +301,7 @@ where
|
|||||||
publishIleanInfoUpdate m ctx.hOut snaps
|
publishIleanInfoUpdate m ctx.hOut snaps
|
||||||
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
return AsyncList.ofList snaps.toList ++ AsyncList.delayed (← EIO.asTask (ε := ElabTaskError) (prio := .dedicated) do
|
||||||
IO.sleep startAfterMs
|
IO.sleep startAfterMs
|
||||||
AsyncList.unfoldAsync (nextSnap ctx m cancelTk levelParams) { snaps })
|
AsyncList.unfoldAsync (nextSnap ctx m cancelTk levelParams ctx.initParams) { snaps })
|
||||||
|
|
||||||
end Elab
|
end Elab
|
||||||
|
|
||||||
@@ -362,8 +364,9 @@ section Initialization
|
|||||||
fileMap := default
|
fileMap := default
|
||||||
|
|
||||||
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
def compileHeader (m : DocumentMeta) (hOut : FS.Stream) (opts : Options) (hasWidgets : Bool)
|
||||||
(levelModule : Name) : IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
(levelModule : Name) (initParams : InitializeParams): IO (Syntax × Task (Except Error (Snapshot × SearchPath))) := do
|
||||||
let gameDir := "../../../testgame"
|
let some gameDir := GameServer.gameDirFromInitParams initParams
|
||||||
|
| throwServerError s!"Invalid rootUri: {initParams.rootUri?}"
|
||||||
|
|
||||||
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
||||||
let out ← IO.Process.output
|
let out ← IO.Process.output
|
||||||
@@ -426,7 +429,8 @@ section Initialization
|
|||||||
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
def initializeWorker (meta : DocumentMeta) (i o e : FS.Stream) (initParams : InitializeParams) (opts : Options)
|
||||||
(levelParams : Game.DidOpenLevelParams) : IO (WorkerContext × WorkerState) := do
|
(levelParams : Game.DidOpenLevelParams) : IO (WorkerContext × WorkerState) := do
|
||||||
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
let clientHasWidgets := initParams.initializationOptions?.bind (·.hasWidgets?) |>.getD false
|
||||||
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets) levelParams.levelModule
|
let (headerStx, headerTask) ← compileHeader meta o opts (hasWidgets := clientHasWidgets)
|
||||||
|
levelParams.levelModule initParams
|
||||||
let cancelTk ← CancelToken.new
|
let cancelTk ← CancelToken.new
|
||||||
let ctx :=
|
let ctx :=
|
||||||
{ hIn := i
|
{ hIn := i
|
||||||
@@ -506,6 +510,7 @@ section MainLoop
|
|||||||
set st
|
set st
|
||||||
match msg with
|
match msg with
|
||||||
| Message.request id method (some params) =>
|
| Message.request id method (some params) =>
|
||||||
|
if method == "Game.getInteractiveGoals" then throwServerError "HELLO"
|
||||||
handleRequest id method (toJson params)
|
handleRequest id method (toJson params)
|
||||||
mainLoop levelParams
|
mainLoop levelParams
|
||||||
| Message.notification "exit" none =>
|
| Message.notification "exit" none =>
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ def handleDidOpenLevel (params : Json) : GameServerM Unit := do
|
|||||||
let fw ← findFileWorker! m.uri
|
let fw ← findFileWorker! m.uri
|
||||||
-- let s ← get
|
-- let s ← get
|
||||||
let c ← read
|
let c ← read
|
||||||
let some lvl ← GameServer.getLevelByFileName? ((System.Uri.fileUriToPath? m.uri).getD m.uri |>.toString)
|
let some lvl ← GameServer.getLevelByFileName? c.initParams ((System.Uri.fileUriToPath? m.uri).getD m.uri |>.toString)
|
||||||
| do
|
| do
|
||||||
c.hLog.putStr s!"Level not found: {m.uri}"
|
c.hLog.putStr s!"Level not found: {m.uri} {c.initParams.rootUri?}"
|
||||||
c.hLog.flush
|
c.hLog.flush
|
||||||
-- Send an extra notification to the file worker to inform it about the level data
|
-- Send an extra notification to the file worker to inform it about the level data
|
||||||
fw.stdin.writeLspNotification {
|
fw.stdin.writeLspNotification {
|
||||||
|
|||||||
@@ -13,15 +13,26 @@ open Meta
|
|||||||
|
|
||||||
namespace GameServer
|
namespace GameServer
|
||||||
|
|
||||||
def levelIdFromFileName? (fileName : String) : Option LevelId := Id.run do
|
def splitRootUri (initParams : Lsp.InitializeParams) (i : Nat): Option String := Id.run do
|
||||||
let fileParts := fileName.splitOn "/"
|
let some rootUri := initParams.rootUri?
|
||||||
if fileParts.length == 3 then
|
| return none
|
||||||
if let some level := fileParts[2]!.toNat? then
|
let rootUriParts := rootUri.splitOn "/"
|
||||||
return some {game := `TestGame, world := fileParts[1]!, level := level}
|
if rootUriParts.length == 3 then
|
||||||
|
return rootUriParts[i]?
|
||||||
return none
|
return none
|
||||||
|
|
||||||
def getLevelByFileName? [Monad m] [MonadEnv m] (fileName : String) : m (Option GameLevel) := do
|
def levelIdFromFileName? (initParams : Lsp.InitializeParams) (fileName : String) : Option LevelId := Id.run do
|
||||||
let some levelId := levelIdFromFileName? fileName
|
let fileParts := fileName.splitOn "/"
|
||||||
|
if fileParts.length == 3 then
|
||||||
|
if let (some level, some game) := (fileParts[2]!.toNat?, splitRootUri initParams 2) then
|
||||||
|
return some {game, world := fileParts[1]!, level := level}
|
||||||
|
return none
|
||||||
|
|
||||||
|
def gameDirFromInitParams (initParams : Lsp.InitializeParams) : Option String :=
|
||||||
|
(splitRootUri initParams 0).map (s!"../../../{·}")
|
||||||
|
|
||||||
|
def getLevelByFileName? [Monad m] [MonadEnv m] (initParams : Lsp.InitializeParams) (fileName : String) : m (Option GameLevel) := do
|
||||||
|
let some levelId := levelIdFromFileName? initParams fileName
|
||||||
| return none
|
| return none
|
||||||
return ← getLevel? levelId
|
return ← getLevel? levelId
|
||||||
|
|
||||||
@@ -114,9 +125,9 @@ def evalHintMessage : Expr → MetaM (Array Expr → MessageData) := fun _ => pu
|
|||||||
|
|
||||||
open Meta in
|
open Meta in
|
||||||
/-- Find all hints whose trigger matches the current goal -/
|
/-- Find all hints whose trigger matches the current goal -/
|
||||||
def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) : MetaM (Array GameHint) := do
|
def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams : Lsp.InitializeParams) : MetaM (Array GameHint) := do
|
||||||
goal.withContext do
|
goal.withContext do
|
||||||
let some level ← getLevelByFileName? doc.meta.mkInputContext.fileName
|
let some level ← getLevelByFileName? initParams doc.meta.mkInputContext.fileName
|
||||||
| throwError "Level not found: {doc.meta.mkInputContext.fileName}"
|
| throwError "Level not found: {doc.meta.mkInputContext.fileName}"
|
||||||
let hints ← level.hints.filterMapM fun hint => do
|
let hints ← level.hints.filterMapM fun hint => do
|
||||||
openAbstractCtxResult hint.goal fun hintFVars hintGoal => do
|
openAbstractCtxResult hint.goal fun hintFVars hintGoal => do
|
||||||
@@ -137,6 +148,7 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) : MetaM (Array
|
|||||||
open RequestM in
|
open RequestM in
|
||||||
def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option InteractiveGoals)) := do
|
def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option InteractiveGoals)) := do
|
||||||
let doc ← readDoc
|
let doc ← readDoc
|
||||||
|
let rc ← readThe RequestContext
|
||||||
let text := doc.meta.text
|
let text := doc.meta.text
|
||||||
let hoverPos := text.lspPosToUtf8Pos p.position
|
let hoverPos := text.lspPosToUtf8Pos p.position
|
||||||
-- TODO: I couldn't find a good condition to find the correct snap. So we are looking
|
-- TODO: I couldn't find a good condition to find the correct snap. So we are looking
|
||||||
@@ -152,7 +164,7 @@ def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Optio
|
|||||||
return List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore
|
return List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore
|
||||||
let goals ← ci.runMetaM {} do
|
let goals ← ci.runMetaM {} do
|
||||||
goals.mapM fun goal => do
|
goals.mapM fun goal => do
|
||||||
let hints ← findHints goal doc
|
let hints ← findHints goal doc rc.initParams
|
||||||
return ← goalToInteractive goal hints
|
return ← goalToInteractive goal hints
|
||||||
-- compute the goal diff
|
-- compute the goal diff
|
||||||
-- let goals ← ciAfter.runMetaM {} (do
|
-- let goals ← ciAfter.runMetaM {} (do
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/- This file is mostly copied from `Lean/Server/Watchdog.lean`. -/
|
/- This file is mostly copied from `Lean/Server/Watchdog.lean`. -/
|
||||||
import Lean
|
import Lean
|
||||||
import GameServer.Game
|
import GameServer.Game
|
||||||
|
import Lean.Server.Watchdog
|
||||||
|
|
||||||
namespace MyServer.Watchdog
|
namespace MyServer.Watchdog
|
||||||
open Lean
|
open Lean
|
||||||
@@ -76,9 +77,7 @@ def initAndRunWatchdogAux : GameServerM Unit := do
|
|||||||
catch _ => pure (Message.notification "exit" none)
|
catch _ => pure (Message.notification "exit" none)
|
||||||
| throwServerError "Got `shutdown` request, expected an `exit` notification"
|
| throwServerError "Got `shutdown` request, expected an `exit` notification"
|
||||||
|
|
||||||
def createEnv : IO Environment := do
|
def createEnv (gameDir : String) (module : String) : IO Environment := do
|
||||||
let gameDir := "../../../testgame"
|
|
||||||
|
|
||||||
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
-- Determine search paths of the game project by running `lake env printenv LEAN_PATH`.
|
||||||
let out ← IO.Process.output
|
let out ← IO.Process.output
|
||||||
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
{ cwd := gameDir, cmd := "lake", args := #["env","printenv","LEAN_PATH"] }
|
||||||
@@ -93,11 +92,17 @@ def createEnv : IO Environment := do
|
|||||||
-- Set the search path
|
-- Set the search path
|
||||||
Lean.searchPathRef.set paths
|
Lean.searchPathRef.set paths
|
||||||
|
|
||||||
let gameName := `TestGame
|
let env ← importModules [{ module := `Init : Import }, { module := module : Import }] {} 0
|
||||||
let env ← importModules [{ module := `Init : Import }, { module := gameName : Import }] {} 0
|
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
||||||
|
if args.length < 4 then
|
||||||
|
throwServerError s!"Expected 3 command line arguments in addition to `--server`:
|
||||||
|
game directory, the name of the main module, and the name of the game"
|
||||||
|
let gameId := args[1]!
|
||||||
|
let gameDir := s!"../../../{gameId}"
|
||||||
|
let module := args[2]!
|
||||||
|
let gameName := args[3]!
|
||||||
let workerPath := "./gameserver"
|
let workerPath := "./gameserver"
|
||||||
-- TODO: Do the following commands slow us down?
|
-- TODO: Do the following commands slow us down?
|
||||||
let srcSearchPath ← initSrcSearchPath (← getBuildDir)
|
let srcSearchPath ← initSrcSearchPath (← getBuildDir)
|
||||||
@@ -106,8 +111,11 @@ def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do
|
|||||||
let i ← maybeTee "wdIn.txt" false i
|
let i ← maybeTee "wdIn.txt" false i
|
||||||
let o ← maybeTee "wdOut.txt" true o
|
let o ← maybeTee "wdOut.txt" true o
|
||||||
let e ← maybeTee "wdErr.txt" true e
|
let e ← maybeTee "wdErr.txt" true e
|
||||||
let state := {env := ← createEnv, game := `TestGame}
|
let state := {env := ← createEnv gameDir module, game := gameName}
|
||||||
let initRequest ← i.readLspRequestAs "initialize" InitializeParams
|
let initRequest ← i.readLspRequestAs "initialize" InitializeParams
|
||||||
|
-- We misuse the `rootUri` field to store gameId, module, and gameName
|
||||||
|
let rootUri? := s!"{gameId}/{module}/{gameName}"
|
||||||
|
let initRequest := {initRequest with param := {initRequest.param with rootUri?}}
|
||||||
o.writeLspResponse {
|
o.writeLspResponse {
|
||||||
id := initRequest.id
|
id := initRequest.id
|
||||||
result := {
|
result := {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ unsafe def main : List String → IO UInt32 := fun args => do
|
|||||||
Lean.enableInitializersExecution
|
Lean.enableInitializersExecution
|
||||||
|
|
||||||
if args[0]? == some "--server" then
|
if args[0]? == some "--server" then
|
||||||
MyServer.Watchdog.watchdogMain []
|
MyServer.Watchdog.watchdogMain args
|
||||||
else if args[0]? == some "--worker" then
|
else if args[0]? == some "--worker" then
|
||||||
MyServer.FileWorker.workerMain {}
|
MyServer.FileWorker.workerMain {}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ RUN rm -f ./build/bin/gameserver
|
|||||||
RUN lake build
|
RUN lake build
|
||||||
|
|
||||||
WORKDIR /leanserver/build/bin/
|
WORKDIR /leanserver/build/bin/
|
||||||
CMD ["./gameserver", "--server"]
|
CMD ["./gameserver", "--server", "testgame", "TestGame", "TestGame"]
|
||||||
|
|||||||
Reference in New Issue
Block a user