This commit is contained in:
Jon Eugster
2024-09-12 12:02:20 +02:00
parent 3a885245ae
commit 64ad36dd4f
20 changed files with 7670 additions and 10399 deletions
+16 -7
View File
@@ -1,17 +1,21 @@
#/bin/bash
# Note: This fails if there is no default toolchain installed
ELAN_HOME=$(lake env printenv ELAN_HOME)
LEAN_ROOT="$(cd $1 && lean --print-prefix)"
LEAN_PATH="$(cd $1 && lake env printenv LEAN_PATH)"
# $1 : the game directory
# $2 : the lean4game folder
# $3 : the gameserver executable
# # print commands as they are executed
# set -x
(exec bwrap\
--bind $2 /lean4game \
--bind $1 /game \
--bind $ELAN_HOME /elan \
--bind /usr /usr \
--ro-bind $2 /lean4game \
--ro-bind $1 /game \
--ro-bind "$LEAN_ROOT" /lean \
--ro-bind /usr /usr \
--dev /dev \
--proc /proc \
--symlink usr/lib /lib\
@@ -19,8 +23,9 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
--symlink usr/bin /bin\
--symlink usr/sbin /sbin\
--clearenv \
--setenv PATH "/elan/bin:/bin" \
--setenv ELAN_HOME "/elan" \
--setenv PATH "/lean/bin" \
--setenv LAKE "/no" `# tries to invoke git otherwise` \
--setenv LEAN_PATH "$LEAN_PATH" \
--unshare-user \
--unshare-pid \
--unshare-net \
@@ -30,3 +35,7 @@ ELAN_HOME=$(lake env printenv ELAN_HOME)
--chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
./gameserver --server /game
)
# TODO
# --chdir "/game/.lake/packages/GameServer/server/.lake/build/bin/" \
# ./gameserver --server /game
+144 -102
View File
@@ -1,15 +1,18 @@
import { WebSocketServer } from 'ws';
import { WebSocketServer } from 'ws'
import express from 'express'
import path from 'path'
import * as cp from 'child_process';
import * as url from 'url';
import * as rpc from 'vscode-ws-jsonrpc';
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server';
import os from 'os';
import fs from 'fs';
import anonymize from 'ip-anonymize';
import * as cp from 'child_process'
import * as url from 'url'
import * as rpc from 'vscode-ws-jsonrpc'
import * as path from 'path'
import * as jsonrpcserver from 'vscode-ws-jsonrpc/server'
// import nocache from 'nocache'
import anonymize from 'ip-anonymize'
import os from 'os'
import fs from 'fs'
import http from 'http'
import https from 'https'
import { importTrigger, importStatus } from './import.mjs'
// import fs from 'fs'
/**
* Add a game here if the server should keep a queue of pre-loaded games ready at all times.
@@ -23,45 +26,71 @@ const queueLength = {
"g/trequetrum/lean4game-logic": 0,
}
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
let socketCounter = 0
const app = express()
function logStats() {
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
}
const PORT = process.env.PORT || 8080;
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
var router = express.Router();
router.get('/import/status/:owner/:repo', importStatus)
router.get('/import/trigger/:owner/:repo', importTrigger)
const server = app
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
.use('/i18n/g/:owner/:repo/:lang/*', (req, res, next) => {
const owner = req.params.owner;
const repo = req.params.repo
const lang = req.params.lang
const filename = req.params[0];
req.url = filename;
express.static(path.join(getGameDir(owner,repo),".i18n",lang))(req, res, next);
})
.use('/data/g/:owner/:repo/*', (req, res, next) => {
const owner = req.params.owner;
const repo = req.params.repo
const filename = req.params[0];
req.url = filename;
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
})
.use('/', router)
.listen(PORT, () => console.log(`Listening on ${PORT}`));
const wss = new WebSocketServer({ server })
var socketCounter = 0
const environment = process.env.NODE_ENV
const isDevelopment = environment === 'development'
const crtFile = process.env.SSL_CRT_FILE
const keyFile = process.env.SSL_KEY_FILE
const app = express()
var router = express.Router()
// Paths for game import logic
router.get('/import/status/:owner/:repo', importStatus)
router.get('/import/trigger/:owner/:repo', importTrigger)
app.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
app.use('/i18n/g/:owner/:repo/:lang/*', (req, res, next) => {
const owner = req.params.owner
const repo = req.params.repo
const lang = req.params.lang
const filename = req.params[0]
req.url = filename
express.static(path.join(getGameDir(owner,repo),".i18n",lang))(req, res, next)
})
app.use('/data/g/:owner/:repo/*', (req, res, next) => {
const owner = req.params.owner
const repo = req.params.repo
const filename = req.params[0]
req.url = filename
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next)
})
app.use('/', router)
let server
if (crtFile && keyFile) {
var privateKey = fs.readFileSync(keyFile, 'utf8')
var certificate = fs.readFileSync(crtFile, 'utf8')
var credentials = {key: privateKey, cert: certificate}
const PORT = process.env.PORT ?? 443
server = https.createServer(credentials, app).listen(PORT,
() => console.log(`HTTPS on port ${PORT}`))
// redirect http to https
express().get('*', function(req, res) {
res.redirect('https://' + req.headers.host + req.url).listen(80)
})
} else {
const PORT = process.env.PORT ?? 8080
server = app.listen(PORT,
() => console.log(`HTTP on port ${PORT}`))
}
const wss = new WebSocketServer({ server })
/** We keep queues of started Lean Server processes to be ready when a user arrives */
const queue = {}
@@ -85,7 +114,8 @@ function getGameDir(owner, repo) {
}
let game_dir = (owner == 'local') ?
path.join(__dirname, '..', '..', repo) : // note: here we need `repo` to be case sensitive
// note: in the local case we need `repo` to be case sensitive
path.join(__dirname, '..', '..', repo) :
path.join(__dirname, '..', 'games', `${owner}`, `${repo.toLowerCase()}`)
if(!fs.existsSync(game_dir)) {
@@ -93,16 +123,20 @@ function getGameDir(owner, repo) {
return ""
}
return game_dir;
return game_dir
}
function startServerProcess(owner, repo) {
let game_dir = getGameDir(owner, repo)
if (!game_dir) return;
if (!game_dir) { return }
let serverProcess
if (isDevelopment) {
console.warn("Running without Bubblewrap container!")
// TODO: use the gameserver
// serverProcess = cp.spawn("lean", ["--server"], { cwd: game_dir })
let args = ["--server", game_dir]
let binDir = path.join(game_dir, ".lake", "packages", "GameServer", "server", ".lake", "build", "bin")
// Note: `cwd` is important to be the `bin` directory as `Watchdog` calls `./gameserver` again
@@ -115,19 +149,24 @@ function startServerProcess(owner, repo) {
{ cwd: path.join(__dirname, "..", "server", ".lake", "build", "bin") })
}
} else {
serverProcess = cp.spawn("./bubblewrap.sh",
console.info("Running with Bubblewrap container.")
serverProcess = cp.spawn(
"./bubblewrap.sh",
[ game_dir, path.join(__dirname, '..')],
{ cwd: __dirname })
{ cwd: __dirname }
)
}
serverProcess.stderr.on('data', data =>
console.error(`Lean Server: ${data}`)
)
serverProcess.on('error', error =>
console.error(`Launching Lean Server failed: ${error}`)
)
if (serverProcess.stderr !== null) {
serverProcess.stderr.on('data', data =>
console.error(`Lean Server: ${data}`)
)
}
serverProcess.on('close', (code, _signal) => {
console.log(`Lean server exited with code ${code}`)
})
return serverProcess
}
@@ -152,61 +191,64 @@ function fillQueue(tag) {
// }
// }
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)$/
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 owner = reRes[1]
const repo = reRes[2]
// server expects URL of the form `/websocket/g/{owner}/{repo}`
const urlRegEx = /^\/websocket\/g\/([\w.-]+)\/([\w.-]+)\/?$/
const reRes = urlRegEx.exec(req.url)
if (!reRes) { console.error(`Connection refused because of invalid URL: ${req.url}`); return }
const owner = reRes[1]
const repo = reRes[2]
const tag = getTag(owner, repo)
const tag = getTag(owner, repo)
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
let ps
if (!queue[tag] || queue[tag].length == 0) {
ps = startServerProcess(owner, repo)
} else {
console.info('Got process from the queue')
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
// TODO
// async () => {
// fillQueue(tag)
// }
}
if (ps == null) {
console.error('server process is undefined/null')
return
}
let ps
if (!queue[tag] || queue[tag].length == 0) {
ps = startServerProcess(owner, repo)
} else {
console.info('Got process from the queue')
ps = queue[tag].shift() // Pick the first Lean process; it's likely to be ready immediately
fillQueue(tag)
const socket = {
onMessage: (cb) => { ws.on("message", cb) },
onError: (cb) => { ws.on("error", cb) },
onClose: (cb) => { ws.on("close", cb) },
send: (data, cb) => { ws.send(data,cb) }
}
const reader = new rpc.WebSocketMessageReader(socket)
const writer = new rpc.WebSocketMessageWriter(socket)
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
socketConnection.forward(serverConnection, message => {
if (isDevelopment) {
console.log(`CLIENT: ${JSON.stringify(message)}`)
}
if (ps == null) {
console.error('server process is undefined/null')
return
return message
})
serverConnection.forward(socketConnection, message => {
if (isDevelopment) {
console.log(`SERVER: ${JSON.stringify(message)}`)
}
return message
})
socketCounter += 1;
const ip = anonymize(req.headers['x-forwarded-for'] || req.socket.remoteAddress)
console.log(`[${new Date()}] Socket opened - ${ip}`)
ws.on('close', () => {
console.log(`[${new Date()}] Socket closed - ${ip}`)
socketCounter -= 1
})
const socket = {
onMessage: (cb) => { ws.on("message", cb) },
onError: (cb) => { ws.on("error", cb) },
onClose: (cb) => { ws.on("close", cb) },
send: (data, cb) => { ws.send(data,cb) }
}
const reader = new rpc.WebSocketMessageReader(socket)
const writer = new rpc.WebSocketMessageWriter(socket)
const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close())
const serverConnection = jsonrpcserver.createProcessStreamConnection(ps)
socketConnection.forward(serverConnection, message => {
if (isDevelopment) {console.log(`CLIENT: ${JSON.stringify(message)}`)}
return message;
})
serverConnection.forward(socketConnection, message => {
if (isDevelopment) {console.log(`SERVER: ${JSON.stringify(message)}`)}
return message;
});
socketConnection.onClose(() => serverConnection.dispose())
serverConnection.onClose(() => socketConnection.dispose())
console.log(`[${new Date()}] Number of open sockets - ${socketCounter}`)
console.log(`[${new Date()}] Free RAM - ${Math.round(os.freemem() / 1024 / 1024)} / ${Math.round(os.totalmem() / 1024 / 1024)} MB`)
ws.on('close', () => {
console.log(`[${new Date()}] Socket closed - ${ip}`)
socketCounter -= 1
})
socketConnection.onClose(() => serverConnection.dispose())
serverConnection.onClose(() => socketConnection.dispose())
console.log(`[${new Date()}] Socket opened - ${ip}`)
socketCounter += 1
logStats()
})