wip
This commit is contained in:
+16
-7
@@ -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
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user