@dcl-regenesislabs/bevy-headless-server 0.1.0-31413755159.commit-5b7586c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/bin/cli.js +156 -0
- package/install.js +20 -0
- package/lib/resolve-binary.js +82 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @dcl-regenesislabs/bevy-headless-server
|
|
2
|
+
|
|
3
|
+
Decentraland authoritative scene server, powered by the [bevy explorer](https://github.com/decentraland/bevy-explorer)
|
|
4
|
+
engine. A drop-in replacement for `@dcl/hammurabi-server`: same CLI contract, native binary
|
|
5
|
+
instead of Node + Babylon.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @dcl-regenesislabs/bevy-headless-server --realm http://localhost:8000
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The Decentraland SDK spawns this automatically for scenes with `authoritativeMultiplayer`
|
|
12
|
+
enabled; you rarely need to run it by hand.
|
|
13
|
+
|
|
14
|
+
## Options
|
|
15
|
+
|
|
16
|
+
| Flag | Meaning |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `--realm <url>` | Realm to serve. Required. |
|
|
19
|
+
| `--position <x,y>` | Parcel to load. Defaults to `0,0`. |
|
|
20
|
+
| `--production` | Production mode; disables preview-only behaviour. |
|
|
21
|
+
| `--tick-hz <n>` | Scene tick rate. Defaults to 30. |
|
|
22
|
+
| `--timeout <secs>` | Exit cleanly after N seconds. |
|
|
23
|
+
|
|
24
|
+
`--scene-id`, `--private-key` and `--env` are accepted for hammurabi compatibility and ignored.
|
|
25
|
+
|
|
26
|
+
## Orchestrated mode (multiplayer-server)
|
|
27
|
+
|
|
28
|
+
`--orchestrated` starts the engine in multi-scene worker mode: no realm needed, scenes are
|
|
29
|
+
added and removed over stdin (JSON lines) with pre-minted comms adapters, and control
|
|
30
|
+
events come back on stdout with the `@bevy-ctl ` prefix.
|
|
31
|
+
|
|
32
|
+
Orchestrators that manage the process themselves can skip the CLI and resolve the engine
|
|
33
|
+
path programmatically:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const { resolveBinary } = require('@dcl-regenesislabs/bevy-headless-server')
|
|
37
|
+
spawn(resolveBinary(), ['--orchestrated'], { stdio: ['pipe', 'pipe', 'inherit'] })
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How the binary is delivered
|
|
41
|
+
|
|
42
|
+
The engine ships as four platform packages (`darwin-arm64`, `darwin-x64`, `linux-x64`,
|
|
43
|
+
`win32-x64`) listed under `optionalDependencies`; your package manager installs only the one
|
|
44
|
+
matching your machine. Each contains two files that **must stay in the same directory** —
|
|
45
|
+
the engine execs its scene-runtime sidecar from its own location.
|
|
46
|
+
|
|
47
|
+
Set `DCL_BEVY_SERVER_PATH` to an absolute path to use a pre-installed engine instead
|
|
48
|
+
(useful for Electron hosts that bundle their own copy).
|
|
49
|
+
|
|
50
|
+
Exit code `78` means the engine is permanently unavailable here — unsupported platform,
|
|
51
|
+
missing binary, or bad arguments — so a caller can fall back to another implementation
|
|
52
|
+
instead of retrying.
|
|
53
|
+
|
|
54
|
+
## Linux runtime dependencies
|
|
55
|
+
|
|
56
|
+
The engine links the graphics/audio stack even when running headless:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
sudo apt install libasound2 libudev1 libgl1 libx11-6 libxext6
|
|
60
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('child_process')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const {
|
|
7
|
+
resolveBinary,
|
|
8
|
+
UnsupportedPlatformError,
|
|
9
|
+
MissingBinaryError
|
|
10
|
+
} = require('../lib/resolve-binary')
|
|
11
|
+
|
|
12
|
+
// Exit code 78 (EX_CONFIG) means "permanently unavailable here" — callers use it to
|
|
13
|
+
// fall back to another server implementation instead of retrying.
|
|
14
|
+
const EXIT_UNAVAILABLE = 78
|
|
15
|
+
|
|
16
|
+
const USAGE = `
|
|
17
|
+
Decentraland authoritative scene server (bevy engine)
|
|
18
|
+
|
|
19
|
+
Usage: bevy-headless-server --realm <url> [options]
|
|
20
|
+
|
|
21
|
+
--realm <url> Realm to serve. Required unless --orchestrated.
|
|
22
|
+
--position <x,y> Parcel to load. Default 0,0.
|
|
23
|
+
--production Production mode (disables preview-only behaviour).
|
|
24
|
+
--orchestrated Multi-scene mode driven over stdin by an orchestrator
|
|
25
|
+
(the multiplayer-server worker contract).
|
|
26
|
+
--tick-hz <n> Scene tick rate. Default 30.
|
|
27
|
+
--timeout <secs> Exit cleanly after N seconds.
|
|
28
|
+
--version Print version and exit.
|
|
29
|
+
-h, --help This message.
|
|
30
|
+
|
|
31
|
+
Environment:
|
|
32
|
+
DCL_BEVY_SERVER_PATH Absolute path to a pre-installed \`headless\` binary,
|
|
33
|
+
bypassing the bundled platform package.
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
function fail(message, code) {
|
|
37
|
+
process.stderr.write(`bevy-headless-server: ${message}\n`)
|
|
38
|
+
process.exit(code)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Translate the hammurabi-server CLI contract into bevy-headless flags. */
|
|
42
|
+
function translate(argv) {
|
|
43
|
+
const out = []
|
|
44
|
+
let realm = null
|
|
45
|
+
let position = null
|
|
46
|
+
let production = false
|
|
47
|
+
let orchestrated = false
|
|
48
|
+
const passthrough = { '--tick-hz': true, '--timeout': true, '--scene-threads': true }
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const arg = argv[i]
|
|
52
|
+
// hammurabi accepts --flag=value; bevy's parser wants them separated.
|
|
53
|
+
const eq = arg.indexOf('=')
|
|
54
|
+
const name = eq === -1 ? arg : arg.slice(0, eq)
|
|
55
|
+
const inlineValue = eq === -1 ? null : arg.slice(eq + 1)
|
|
56
|
+
const takeValue = () => (inlineValue !== null ? inlineValue : argv[++i])
|
|
57
|
+
|
|
58
|
+
switch (name) {
|
|
59
|
+
case '--realm':
|
|
60
|
+
realm = takeValue()
|
|
61
|
+
break
|
|
62
|
+
case '--position':
|
|
63
|
+
position = takeValue()
|
|
64
|
+
break
|
|
65
|
+
case '--production':
|
|
66
|
+
production = true
|
|
67
|
+
break
|
|
68
|
+
case '--orchestrated':
|
|
69
|
+
orchestrated = true
|
|
70
|
+
break
|
|
71
|
+
case '-h':
|
|
72
|
+
case '--help':
|
|
73
|
+
process.stdout.write(USAGE)
|
|
74
|
+
process.exit(0)
|
|
75
|
+
break
|
|
76
|
+
case '--version':
|
|
77
|
+
process.stdout.write(`${require('../package.json').version}\n`)
|
|
78
|
+
process.exit(0)
|
|
79
|
+
break
|
|
80
|
+
// Accepted by hammurabi, no equivalent here. Warn rather than fail so an
|
|
81
|
+
// orchestrator passing extra flags still boots.
|
|
82
|
+
case '--scene-id':
|
|
83
|
+
case '--private-key':
|
|
84
|
+
case '--env':
|
|
85
|
+
takeValue()
|
|
86
|
+
process.stderr.write(`bevy-headless-server: ignoring unsupported flag ${name}\n`)
|
|
87
|
+
break
|
|
88
|
+
default:
|
|
89
|
+
if (passthrough[name]) {
|
|
90
|
+
out.push(name, takeValue())
|
|
91
|
+
} else {
|
|
92
|
+
process.stderr.write(`bevy-headless-server: ignoring unknown flag ${arg}\n`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// orchestrated mode gets scenes (with their content URLs) over stdin, so no realm
|
|
98
|
+
if (!realm && !orchestrated) fail('--realm is required', EXIT_UNAVAILABLE)
|
|
99
|
+
if (realm) {
|
|
100
|
+
try {
|
|
101
|
+
// eslint-disable-next-line no-new
|
|
102
|
+
new URL(realm)
|
|
103
|
+
} catch (e) {
|
|
104
|
+
fail(`--realm is not a valid URL: ${realm}`, EXIT_UNAVAILABLE)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (position && !/^-?\d+,-?\d+$/.test(position)) {
|
|
108
|
+
fail(`--position must be "x,y", got: ${position}`, EXIT_UNAVAILABLE)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const args = orchestrated ? ['--orchestrated'] : ['--server-mode']
|
|
112
|
+
if (realm) args.unshift('--realm', realm)
|
|
113
|
+
if (position) args.push('--location', position)
|
|
114
|
+
if (!production) args.push('--preview')
|
|
115
|
+
return args.concat(out)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function main() {
|
|
119
|
+
const args = translate(process.argv.slice(2))
|
|
120
|
+
|
|
121
|
+
let exe
|
|
122
|
+
try {
|
|
123
|
+
exe = resolveBinary()
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err instanceof UnsupportedPlatformError || err instanceof MissingBinaryError) {
|
|
126
|
+
fail(err.message, EXIT_UNAVAILABLE)
|
|
127
|
+
}
|
|
128
|
+
throw err
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const child = spawn(exe, args, { stdio: 'inherit', env: process.env })
|
|
132
|
+
|
|
133
|
+
child.on('error', (err) => {
|
|
134
|
+
if (process.platform === 'linux' && /ENOENT|not found/i.test(err.message)) {
|
|
135
|
+
process.stderr.write(
|
|
136
|
+
'bevy-headless-server: the engine failed to start. On Linux it needs:\n' +
|
|
137
|
+
' sudo apt install libasound2 libudev1 libgl1 libx11-6 libxext6\n'
|
|
138
|
+
)
|
|
139
|
+
process.exit(EXIT_UNAVAILABLE)
|
|
140
|
+
}
|
|
141
|
+
fail(`failed to start the engine: ${err.message}`, EXIT_UNAVAILABLE)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const forward = (signal) => () => {
|
|
145
|
+
if (!child.killed) child.kill(signal)
|
|
146
|
+
}
|
|
147
|
+
process.on('SIGTERM', forward('SIGTERM'))
|
|
148
|
+
process.on('SIGINT', forward('SIGINT'))
|
|
149
|
+
process.on('exit', forward('SIGTERM'))
|
|
150
|
+
|
|
151
|
+
child.on('close', (code, signal) => {
|
|
152
|
+
process.exit(signal ? 1 : code === null ? 1 : code)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main()
|
package/install.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Advisory postinstall check. The binary itself arrives through optionalDependencies;
|
|
4
|
+
// this only turns npm's silent optional-dependency skip (npm/cli#4828, and every
|
|
5
|
+
// --ignore-scripts / --no-optional install) into a readable message at install time
|
|
6
|
+
// rather than a confusing failure at first run.
|
|
7
|
+
|
|
8
|
+
const { resolveBinary, platformKey } = require('./lib/resolve-binary')
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
resolveBinary()
|
|
12
|
+
} catch (err) {
|
|
13
|
+
const key = platformKey()
|
|
14
|
+
process.stderr.write(
|
|
15
|
+
`\n@dcl-regenesislabs/bevy-headless-server: ${err.message}\n` +
|
|
16
|
+
(key
|
|
17
|
+
? ` Fix with: npm install @dcl-regenesislabs/bevy-headless-server-${key}@${require('./package.json').version}\n\n`
|
|
18
|
+
: '\n')
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// Platform packages ship `headless` and `dcl_deno_ipc` side by side. The engine
|
|
7
|
+
// spawns the sidecar from its own directory under a fixed name, so the pair must
|
|
8
|
+
// never be split or copied apart.
|
|
9
|
+
const SUPPORTED = {
|
|
10
|
+
'darwin arm64': 'darwin-arm64',
|
|
11
|
+
'darwin x64': 'darwin-x64',
|
|
12
|
+
'linux x64': 'linux-x64',
|
|
13
|
+
'win32 x64': 'win32-x64'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function platformKey() {
|
|
17
|
+
return SUPPORTED[`${process.platform} ${process.arch}`]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class UnsupportedPlatformError extends Error {}
|
|
21
|
+
class MissingBinaryError extends Error {}
|
|
22
|
+
|
|
23
|
+
function resolveFromPackage(key) {
|
|
24
|
+
const pkg = `@dcl-regenesislabs/bevy-headless-server-${key}`
|
|
25
|
+
const dir = path.dirname(require.resolve(`${pkg}/package.json`))
|
|
26
|
+
return path.join(dir, 'bin')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Absolute path to the `headless` executable.
|
|
31
|
+
* DCL_BEVY_SERVER_PATH overrides everything (Creator Hub / pre-seeded installs).
|
|
32
|
+
*/
|
|
33
|
+
function resolveBinary() {
|
|
34
|
+
const override = process.env.DCL_BEVY_SERVER_PATH
|
|
35
|
+
if (override) {
|
|
36
|
+
if (!fs.existsSync(override)) {
|
|
37
|
+
throw new MissingBinaryError(`DCL_BEVY_SERVER_PATH points at a missing file: ${override}`)
|
|
38
|
+
}
|
|
39
|
+
return verifySidecar(override)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const key = platformKey()
|
|
43
|
+
if (!key) {
|
|
44
|
+
throw new UnsupportedPlatformError(
|
|
45
|
+
`no bevy-headless build for ${process.platform}-${process.arch} ` +
|
|
46
|
+
`(supported: ${Object.values(SUPPORTED).join(', ')})`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let binDir
|
|
51
|
+
try {
|
|
52
|
+
binDir = resolveFromPackage(key)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new MissingBinaryError(
|
|
55
|
+
`@dcl-regenesislabs/bevy-headless-server-${key} is not installed. ` +
|
|
56
|
+
`If your package manager skipped optional dependencies, reinstall with them enabled ` +
|
|
57
|
+
`or install that package explicitly.`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const exe = path.join(binDir, process.platform === 'win32' ? 'headless.exe' : 'headless')
|
|
62
|
+
if (!fs.existsSync(exe)) {
|
|
63
|
+
throw new MissingBinaryError(`corrupt install: ${exe} is missing — reinstall the package`)
|
|
64
|
+
}
|
|
65
|
+
return verifySidecar(exe)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function verifySidecar(exe) {
|
|
69
|
+
const sidecar = path.join(
|
|
70
|
+
path.dirname(exe),
|
|
71
|
+
process.platform === 'win32' ? 'dcl_deno_ipc.exe' : 'dcl_deno_ipc'
|
|
72
|
+
)
|
|
73
|
+
if (!fs.existsSync(sidecar)) {
|
|
74
|
+
throw new MissingBinaryError(
|
|
75
|
+
`the scene runtime sidecar is missing next to the engine (expected ${sidecar}). ` +
|
|
76
|
+
`The two binaries must live in the same directory.`
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return exe
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { resolveBinary, platformKey, SUPPORTED, UnsupportedPlatformError, MissingBinaryError }
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dcl-regenesislabs/bevy-headless-server",
|
|
3
|
+
"version": "0.1.0-31413755159.commit-5b7586c",
|
|
4
|
+
"description": "Decentraland authoritative scene server (bevy engine) — drop-in replacement for @dcl/hammurabi-server",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/decentraland/bevy-explorer.git",
|
|
9
|
+
"directory": "deploy/headless/launcher"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"bevy-headless-server": "bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "lib/resolve-binary.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/cli.js",
|
|
17
|
+
"lib/resolve-binary.js",
|
|
18
|
+
"install.js",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"postinstall": "node install.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"optionalDependencies": {
|
|
28
|
+
"@dcl-regenesislabs/bevy-headless-server-darwin-arm64": "0.1.0-31413755159.commit-5b7586c",
|
|
29
|
+
"@dcl-regenesislabs/bevy-headless-server-darwin-x64": "0.1.0-31413755159.commit-5b7586c",
|
|
30
|
+
"@dcl-regenesislabs/bevy-headless-server-linux-x64": "0.1.0-31413755159.commit-5b7586c",
|
|
31
|
+
"@dcl-regenesislabs/bevy-headless-server-win32-x64": "0.1.0-31413755159.commit-5b7586c"
|
|
32
|
+
},
|
|
33
|
+
"commit": "5b7586c8b7e79106680c4d7fb8ab8fad842531b8"
|
|
34
|
+
}
|