mirror of
https://github.com/timvisee/lazymc.git
synced 2025-08-02 13:12:01 -07:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dde9fdeab4 | ||
|
901fb62f25 | ||
|
dabeabeff4 | ||
|
96d7fc9dec | ||
|
5ffc6ee911 | ||
|
b71d0d1013 | ||
|
261acafab0 | ||
|
a04a5f93e9 | ||
|
10c57f87ea | ||
|
88fc5892a3 | ||
|
38d90681c7 | ||
|
2af20945cc | ||
|
ad638b5b3e | ||
|
69c2d580d5 | ||
|
c6cd08c993 | ||
|
69812f5b55 | ||
|
f172587fd5 | ||
|
e6021502d9 | ||
|
157905f140 | ||
|
72d132ae8b | ||
|
7a00c2df9e | ||
|
fe3bf63401 | ||
|
e9e58a766b | ||
|
f9be5c5a0f | ||
|
7c7595dcd3 |
@@ -106,6 +106,76 @@ build-x86_64-linux-musl:
|
|||||||
- lazymc-$TARGET
|
- lazymc-$TARGET
|
||||||
expire_in: 1 month
|
expire_in: 1 month
|
||||||
|
|
||||||
|
# Build using Rust stable on Linux for ARMv7
|
||||||
|
build-armv7-linux-gnu:
|
||||||
|
stage: build
|
||||||
|
image: ubuntu
|
||||||
|
needs: []
|
||||||
|
variables:
|
||||||
|
TARGET: armv7-unknown-linux-gnueabihf
|
||||||
|
cache:
|
||||||
|
<<: *rust-build-cache
|
||||||
|
before_script:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y --no-install-recommends build-essential
|
||||||
|
- |
|
||||||
|
apt-get install -y curl
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
source $HOME/.cargo/env
|
||||||
|
- |
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
script:
|
||||||
|
- apt-get install -y gcc-arm-linux-gnueabihf
|
||||||
|
- rustup target add $TARGET
|
||||||
|
|
||||||
|
- mkdir -p ~/.cargo
|
||||||
|
- 'echo "[target.$TARGET]" >> ~/.cargo/config'
|
||||||
|
- 'echo "linker = \"arm-linux-gnueabihf-gcc\"" >> ~/.cargo/config'
|
||||||
|
|
||||||
|
- cargo build --target=$TARGET --release --locked --verbose
|
||||||
|
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
|
||||||
|
artifacts:
|
||||||
|
name: lazymc-armv7-linux-gnu
|
||||||
|
paths:
|
||||||
|
- lazymc-$TARGET
|
||||||
|
expire_in: 1 month
|
||||||
|
|
||||||
|
# Build using Rust stable on Linux for aarch64
|
||||||
|
build-aarch64-linux-gnu:
|
||||||
|
stage: build
|
||||||
|
image: ubuntu
|
||||||
|
needs: []
|
||||||
|
variables:
|
||||||
|
TARGET: aarch64-unknown-linux-gnu
|
||||||
|
cache:
|
||||||
|
<<: *rust-build-cache
|
||||||
|
before_script:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y --no-install-recommends build-essential
|
||||||
|
- |
|
||||||
|
apt-get install -y curl
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
source $HOME/.cargo/env
|
||||||
|
- |
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
script:
|
||||||
|
- apt-get install -y gcc-aarch64-linux-gnu
|
||||||
|
- rustup target add $TARGET
|
||||||
|
|
||||||
|
- mkdir -p ~/.cargo
|
||||||
|
- 'echo "[target.$TARGET]" >> ~/.cargo/config'
|
||||||
|
- 'echo "linker = \"aarch64-linux-gnu-gcc\"" >> ~/.cargo/config'
|
||||||
|
|
||||||
|
- cargo build --target=$TARGET --release --locked --verbose
|
||||||
|
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
|
||||||
|
artifacts:
|
||||||
|
name: lazymc-aarch64-linux-gnu
|
||||||
|
paths:
|
||||||
|
- lazymc-$TARGET
|
||||||
|
expire_in: 1 month
|
||||||
|
|
||||||
# Build using Rust stable on macOS
|
# Build using Rust stable on macOS
|
||||||
build-macos:
|
build-macos:
|
||||||
stage: build
|
stage: build
|
||||||
@@ -185,6 +255,8 @@ release-gitlab-generic-package:
|
|||||||
variables:
|
variables:
|
||||||
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
||||||
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
||||||
|
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
||||||
|
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
|
||||||
MACOS_BIN: "lazymc-x86_64-apple-darwin"
|
MACOS_BIN: "lazymc-x86_64-apple-darwin"
|
||||||
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
||||||
before_script: []
|
before_script: []
|
||||||
@@ -198,6 +270,10 @@ release-gitlab-generic-package:
|
|||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN}
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN}
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_MUSL_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_MUSL_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}
|
||||||
|
- |
|
||||||
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_ARMV7_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}
|
||||||
|
- |
|
||||||
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_AARCH64_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${MACOS_BIN} ${PACKAGE_REGISTRY_URL}/${MACOS_BIN}
|
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${MACOS_BIN} ${PACKAGE_REGISTRY_URL}/${MACOS_BIN}
|
||||||
- |
|
- |
|
||||||
@@ -212,6 +288,8 @@ release-gitlab-release:
|
|||||||
variables:
|
variables:
|
||||||
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
|
||||||
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
|
||||||
|
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
|
||||||
|
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
|
||||||
MACOS_BIN: "lazymc-x86_64-apple-darwin"
|
MACOS_BIN: "lazymc-x86_64-apple-darwin"
|
||||||
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
|
||||||
before_script: []
|
before_script: []
|
||||||
@@ -225,6 +303,8 @@ release-gitlab-release:
|
|||||||
release-cli create --name "lazymc $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \
|
release-cli create --name "lazymc $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \
|
||||||
--assets-link "{\"name\":\"${LINUX_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN}\"}" \
|
--assets-link "{\"name\":\"${LINUX_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN}\"}" \
|
||||||
--assets-link "{\"name\":\"${LINUX_MUSL_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}\"}" \
|
--assets-link "{\"name\":\"${LINUX_MUSL_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}\"}" \
|
||||||
|
--assets-link "{\"name\":\"${LINUX_ARMV7_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}\"}" \
|
||||||
|
--assets-link "{\"name\":\"${LINUX_AARCH64_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}\"}" \
|
||||||
--assets-link "{\"name\":\"${MACOS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${MACOS_BIN}\"}" \
|
--assets-link "{\"name\":\"${MACOS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${MACOS_BIN}\"}" \
|
||||||
--assets-link "{\"name\":\"${WINDOWS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}\"}"
|
--assets-link "{\"name\":\"${WINDOWS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}\"}"
|
||||||
|
|
||||||
@@ -236,6 +316,8 @@ release-github:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- build-x86_64-linux-gnu
|
- build-x86_64-linux-gnu
|
||||||
- build-x86_64-linux-musl
|
- build-x86_64-linux-musl
|
||||||
|
- build-armv7-linux-gnu
|
||||||
|
- build-aarch64-linux-gnu
|
||||||
- build-macos
|
- build-macos
|
||||||
- build-x86_64-windows
|
- build-x86_64-windows
|
||||||
before_script: []
|
before_script: []
|
||||||
@@ -253,5 +335,7 @@ release-github:
|
|||||||
- ./github-release release --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --title "lazymc $CI_COMMIT_REF_NAME"
|
- ./github-release release --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --title "lazymc $CI_COMMIT_REF_NAME"
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-gnu --name lazymc-$CI_COMMIT_REF_NAME-linux-x64
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-gnu --name lazymc-$CI_COMMIT_REF_NAME-linux-x64
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-musl --name lazymc-$CI_COMMIT_REF_NAME-linux-x64-static
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-musl --name lazymc-$CI_COMMIT_REF_NAME-linux-x64-static
|
||||||
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-armv7-unknown-linux-gnueabihf --name lazymc-$CI_COMMIT_REF_NAME-linux-armv7
|
||||||
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-aarch64-unknown-linux-gnu --name lazymc-$CI_COMMIT_REF_NAME-linux-aarch64
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-apple-darwin --name lazymc-$CI_COMMIT_REF_NAME-macos
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-apple-darwin --name lazymc-$CI_COMMIT_REF_NAME-macos
|
||||||
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-pc-windows-msvc.exe --name lazymc-$CI_COMMIT_REF_NAME-windows.exe
|
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-pc-windows-msvc.exe --name lazymc-$CI_COMMIT_REF_NAME-windows.exe
|
||||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.2 (2021-11-15)
|
||||||
|
|
||||||
|
- Add Linux ARMv7 and aarch64 releases
|
||||||
|
- RCON now works if server is running while server command already quit
|
||||||
|
- Various RCON tweaks in an attempt to make it more robust and reliable (cooldown, exclusive lock, invocation spacing)
|
||||||
|
- Increase server monitoring timeout to 20 seconds
|
||||||
|
- Improve waiting for server logic when holding client
|
||||||
|
- Various fixes and improvements
|
||||||
|
|
||||||
|
## 0.1.1 (2021-11-14)
|
||||||
|
|
||||||
|
- Make server sleeping errors more descriptive
|
||||||
|
- Add server quit cooldown period, intended to prevent RCON errors due to RCON
|
||||||
|
server thread something quitting after main server
|
||||||
|
- Rewrite `enable-status = true` in `server.properties`
|
||||||
|
- Rewrite `prevent-proxy-connections = false` in `server.properties` if
|
||||||
|
Minecraft server has non-loopback address (other public IP)
|
||||||
|
- Add compile from source instructions to README
|
||||||
|
- Add Windows instructions to README
|
||||||
|
- Update dependencies
|
||||||
|
- Various fixes and improvements
|
||||||
|
|
||||||
## 0.1.0 (2021-11-11)
|
## 0.1.0 (2021-11-11)
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -614,7 +614,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazymc"
|
name = "lazymc"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -668,7 +668,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "minecraft-protocol"
|
name = "minecraft-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/timvisee/minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
|
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"minecraft-protocol-derive",
|
"minecraft-protocol-derive",
|
||||||
@@ -681,7 +681,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "minecraft-protocol-derive"
|
name = "minecraft-protocol-derive"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/timvisee/minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
|
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1077,9 +1077,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.69"
|
version = "1.0.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8"
|
checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lazymc"
|
name = "lazymc"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
|
authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -31,13 +31,13 @@ derive_builder = "0.10"
|
|||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
futures = { version = "0.3", default-features = false }
|
futures = { version = "0.3", default-features = false }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
minecraft-protocol = { git = "https://github.com/timvisee/minecraft-protocol", rev = "31041b8" }
|
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "31041b8" }
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
shlex = "1.1"
|
shlex = "1.1"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal"] }
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal", "sync"] }
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
|
|
||||||
# Feature: rcon
|
# Feature: rcon
|
||||||
|
83
README.md
83
README.md
@@ -18,9 +18,8 @@ lazymc functions as proxy between clients and the server. It handles all
|
|||||||
incoming status connections until the server is started and then transparently
|
incoming status connections until the server is started and then transparently
|
||||||
relays/proxies the rest. All without them noticing.
|
relays/proxies the rest. All without them noticing.
|
||||||
|
|
||||||
_Note: this is a prototype and may be incomplete._
|
https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81cc-5a7ab8b8e86b.mp4
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/856222/140804726-ba1a8e59-85d9-413b-8229-03be84b55d51.mp4
|
|
||||||
|
|
||||||
<details><summary>Click to see screenshots</summary>
|
<details><summary>Click to see screenshots</summary>
|
||||||
<p>
|
<p>
|
||||||
@@ -55,28 +54,27 @@ won't be able to set this up._
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
_Note: these instructions are for Linux & macOS, for Windows look
|
||||||
|
[here](./docs/usage-windows.md)._
|
||||||
|
|
||||||
Make sure you meet all [requirements](#requirements).
|
Make sure you meet all [requirements](#requirements).
|
||||||
|
|
||||||
_Note: Installation options are limited at this moment. Ready-to-go binaries
|
Download the appropriate binary for your system from the [latest
|
||||||
will be published later. For now we compile and install from source._
|
release][latest-release] page.
|
||||||
|
|
||||||
To compile and install you need Rust, install it through `rustup`: https://rustup.rs/
|
Place the binary in your Minecraft server directory, rename it if you like.
|
||||||
|
Open a terminal, go to the directory, and make sure you can invoke it:
|
||||||
When Rust is installed, compile and install `lazymc` from this git repository:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Compile and install lazymc from source
|
chmod a+x ./lazymc
|
||||||
cargo install -f --git https://github.com/timvisee/lazymc
|
./lazymc --help
|
||||||
|
|
||||||
# Ensure lazymc works
|
|
||||||
lazymc --help
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When `lazymc` is available, change into your server directory. Then set up the
|
When `lazymc` is set-up, change into your server directory if you haven't
|
||||||
[configuration](./res/lazymc.toml) and start it up:
|
already. Then set up the [configuration](./res/lazymc.toml) and start it up:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Change into your server directory
|
# Change into your server directory (if you haven't already)
|
||||||
cd server
|
cd server
|
||||||
|
|
||||||
# Generate lazymc configuration
|
# Generate lazymc configuration
|
||||||
@@ -90,9 +88,62 @@ nano lazymc.toml
|
|||||||
lazymc start
|
lazymc start
|
||||||
```
|
```
|
||||||
|
|
||||||
Everything should now be running. Connect with your Minecraft client to wake
|
Before you use this in production, please ensure starting and stopping the
|
||||||
|
server works as expected by connecting to it once. Watch `lazymc`s output while
|
||||||
|
it starts and stops. If stopping results in errors, fix this first to prevent
|
||||||
|
corrupting world/user data.
|
||||||
|
|
||||||
|
Follow this repository with the _Watch_ button on the top right to be notified of new releases.
|
||||||
|
|
||||||
|
Everything should now be ready to go! Connect with your Minecraft client to wake
|
||||||
your server up!
|
your server up!
|
||||||
|
|
||||||
|
_Note: If a binary for your system isn't provided, please [compile from
|
||||||
|
source](#compile-from-source)._
|
||||||
|
|
||||||
|
_Note: Installation options are limited at this moment. More will be added
|
||||||
|
later._
|
||||||
|
|
||||||
|
[latest-release]: https://github.com/timvisee/lazymc/releases/latest
|
||||||
|
|
||||||
|
## Compile from source
|
||||||
|
|
||||||
|
Make sure you meet all [requirements](#requirements).
|
||||||
|
|
||||||
|
To compile from source you need Rust, install it through `rustup`: https://rustup.rs/
|
||||||
|
|
||||||
|
When Rust is installed, compile and install `lazymc` from this git repository
|
||||||
|
directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile and install lazymc from source
|
||||||
|
cargo install -f --git https://github.com/timvisee/lazymc
|
||||||
|
|
||||||
|
# Ensure lazymc works
|
||||||
|
lazymc --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Or clone the repository and build it yourself:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://github.com/timvisee/lazymc
|
||||||
|
cd lazymc
|
||||||
|
|
||||||
|
# Compile
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run lazymc
|
||||||
|
./target/release/lazymc --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Third-party usage & implementations
|
||||||
|
|
||||||
|
A list of third-party implementations, projects using `lazymc`, that you might
|
||||||
|
find useful:
|
||||||
|
|
||||||
|
- Docker: [crbanman/papermc-lazymc](https://hub.docker.com/r/crbanman/papermc-lazymc) _(PaperMC with lazymc in Docker)_
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is released under the GNU GPL-3.0 license.
|
This project is released under the GNU GPL-3.0 license.
|
||||||
|
22
TODO.md
Normal file
22
TODO.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- Better organize code
|
||||||
|
- Resolve TODOs in code
|
||||||
|
- Don't drop errors, handle everywhere where needed (some were dropped while
|
||||||
|
prototyping to speed up development)
|
||||||
|
|
||||||
|
## Nice to have
|
||||||
|
|
||||||
|
- Use server whitelist/blacklist
|
||||||
|
- Console error if server already started on port, not through `lazymc`
|
||||||
|
- Kick with message if proxy-to-server connection fails for new client.
|
||||||
|
- Test configuration on start (server dir exists, command not empty)
|
||||||
|
- Also quit `lazymc` after CTRL+C signal, after server has stopped
|
||||||
|
- Dynamically increase/decrease server polling interval based on server state
|
||||||
|
- Server polling through query (`enable-query` in `server.properties`, uses GameSpy4 protocol)
|
||||||
|
|
||||||
|
## Experiment
|
||||||
|
|
||||||
|
- Lobby method: let players connect with an emulated empty server (like 2b2t's
|
||||||
|
queue), redirect them when the server started.
|
||||||
|
- `io_uring` on Linux for efficient proxying (see `tokio-uring`)
|
48
docs/usage-windows.md
Normal file
48
docs/usage-windows.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
## Usage on Windows
|
||||||
|
|
||||||
|
Make sure you meet all [requirements](../README.md#requirements).
|
||||||
|
|
||||||
|
Download the `lazymc-*-windows.exe` Windows executable for your system from the
|
||||||
|
[latest release][latest-release] page.
|
||||||
|
|
||||||
|
Place the binary in your Minecraft server directory, and rename it to
|
||||||
|
`lazymc.exe`.
|
||||||
|
|
||||||
|
Open a terminal, go to the server directory, and make sure you can execute it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.\lazymc --help
|
||||||
|
```
|
||||||
|
|
||||||
|
When `lazymc` is ready, set up the [configuration](./res/lazymc.toml) and start
|
||||||
|
it up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your Minecraft server directory:
|
||||||
|
|
||||||
|
# Generate lazymc configuration
|
||||||
|
.\lazymc config generate
|
||||||
|
|
||||||
|
# Edit configuration
|
||||||
|
# Set the correct server address, directory and start command
|
||||||
|
notepad lazymc.toml
|
||||||
|
|
||||||
|
# Start lazymc
|
||||||
|
.\lazymc start
|
||||||
|
```
|
||||||
|
|
||||||
|
Before you use this in production, please ensure starting and stopping the
|
||||||
|
server works as expected by connecting to it once. Watch `lazymc`s output while
|
||||||
|
it starts and stops. If stopping results in errors, fix this first to prevent
|
||||||
|
corrupting world/user data.
|
||||||
|
|
||||||
|
Follow this repository with the _Watch_ button on the top right to be notified of new releases.
|
||||||
|
|
||||||
|
Everything should now be ready to go! Connect with your Minecraft client to wake
|
||||||
|
your server up!
|
||||||
|
|
||||||
|
_Note: if you put `lazymc` in `PATH`, or if you
|
||||||
|
[install](../README.md#compile-from-source) it through Cargo, you can invoke
|
||||||
|
`lazymc` everywhere directly without the `.\` prefix.
|
||||||
|
|
||||||
|
[latest-release]: https://github.com/timvisee/lazymc/releases/latest
|
@@ -132,9 +132,15 @@ fn rewrite_server_properties(config: &Config) {
|
|||||||
let mut changes = HashMap::from([
|
let mut changes = HashMap::from([
|
||||||
("server-ip", config.server.address.ip().to_string()),
|
("server-ip", config.server.address.ip().to_string()),
|
||||||
("server-port", config.server.address.port().to_string()),
|
("server-port", config.server.address.port().to_string()),
|
||||||
|
("enable-status", "true".into()),
|
||||||
("query.port", config.server.address.port().to_string()),
|
("query.port", config.server.address.port().to_string()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// If connecting to server over non-loopback address, disable proxy blocking
|
||||||
|
if !config.server.address.ip().is_loopback() {
|
||||||
|
changes.extend([("prevent-proxy-connections", "false".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
// Add RCON configuration
|
// Add RCON configuration
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
if config.rcon.enabled {
|
if config.rcon.enabled {
|
||||||
|
@@ -1,4 +1,14 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use rust_rcon::{Connection, Error as RconError};
|
use rust_rcon::{Connection, Error as RconError};
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
/// Minecraft RCON quirk.
|
||||||
|
///
|
||||||
|
/// Wait this time between RCON operations.
|
||||||
|
/// The Minecraft RCON implementation is very broken and brittle, this is used in the hopes to
|
||||||
|
/// improve reliability.
|
||||||
|
const QUIRK_RCON_GRACE_TIME: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
/// An RCON client.
|
/// An RCON client.
|
||||||
pub struct Rcon {
|
pub struct Rcon {
|
||||||
@@ -19,7 +29,17 @@ impl Rcon {
|
|||||||
|
|
||||||
/// Send command over RCON.
|
/// Send command over RCON.
|
||||||
pub async fn cmd(&mut self, cmd: &str) -> Result<String, RconError> {
|
pub async fn cmd(&mut self, cmd: &str) -> Result<String, RconError> {
|
||||||
|
// Minecraft quirk
|
||||||
|
time::sleep(QUIRK_RCON_GRACE_TIME).await;
|
||||||
|
|
||||||
|
// Actually send RCON command
|
||||||
debug!(target: "lazymc::rcon", "Sending RCON: {}", cmd);
|
debug!(target: "lazymc::rcon", "Sending RCON: {}", cmd);
|
||||||
self.con.cmd(cmd).await
|
self.con.cmd(cmd).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Close connection.
|
||||||
|
pub async fn close(self) {
|
||||||
|
// Minecraft quirk
|
||||||
|
time::sleep(QUIRK_RCON_GRACE_TIME).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,10 +23,10 @@ use crate::server::{Server, State};
|
|||||||
const MONITOR_POLL_INTERVAL: Duration = Duration::from_secs(2);
|
const MONITOR_POLL_INTERVAL: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
/// Status request timeout in seconds.
|
/// Status request timeout in seconds.
|
||||||
const STATUS_TIMEOUT: u64 = 8;
|
const STATUS_TIMEOUT: u64 = 20;
|
||||||
|
|
||||||
/// Ping request timeout in seconds.
|
/// Ping request timeout in seconds.
|
||||||
const PING_TIMEOUT: u64 = 10;
|
const PING_TIMEOUT: u64 = 20;
|
||||||
|
|
||||||
/// Monitor server.
|
/// Monitor server.
|
||||||
pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
||||||
@@ -36,15 +36,17 @@ pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
|||||||
let mut poll_interval = time::interval(MONITOR_POLL_INTERVAL);
|
let mut poll_interval = time::interval(MONITOR_POLL_INTERVAL);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
poll_interval.tick().await;
|
||||||
|
|
||||||
// Poll server state and update internal status
|
// Poll server state and update internal status
|
||||||
trace!(target: "lazymc::monitor", "Fetching status for {} ... ", addr);
|
trace!(target: "lazymc::monitor", "Fetching status for {} ... ", addr);
|
||||||
let status = poll_server(&config, &server, addr).await;
|
let status = poll_server(&config, &server, addr).await;
|
||||||
match status {
|
match status {
|
||||||
// Got status, update
|
// Got status, update
|
||||||
Ok(Some(status)) => server.update_status(&config, Some(status)),
|
Ok(Some(status)) => server.update_status(&config, Some(status)).await,
|
||||||
|
|
||||||
// Error, reset status
|
// Error, reset status
|
||||||
Err(_) => server.update_status(&config, None),
|
Err(_) => server.update_status(&config, None).await,
|
||||||
|
|
||||||
// Didn't get status, but ping fallback worked, leave as-is, show warning
|
// Didn't get status, but ping fallback worked, leave as-is, show warning
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
@@ -53,22 +55,18 @@ pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sleep server when it's bedtime
|
// Sleep server when it's bedtime
|
||||||
if server.should_sleep(&config) {
|
if server.should_sleep(&config).await {
|
||||||
info!(target: "lazymc::montior", "Server has been idle, sleeping...");
|
info!(target: "lazymc::montior", "Server has been idle, sleeping...");
|
||||||
if !server.stop(&config).await {
|
server.stop(&config).await;
|
||||||
warn!(target: "lazymc", "Failed to stop server");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we should force kill server
|
// Check whether we should force kill server
|
||||||
if server.should_kill() {
|
if server.should_kill().await {
|
||||||
error!(target: "lazymc::montior", "Force killing server, took too long to start or stop");
|
error!(target: "lazymc::montior", "Force killing server, took too long to start or stop");
|
||||||
if !server.force_kill().await {
|
if !server.force_kill().await {
|
||||||
warn!(target: "lazymc", "Failed to force kill server");
|
warn!(target: "lazymc", "Failed to force kill server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
poll_interval.tick().await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,7 +20,7 @@ pub unsafe fn kill_gracefully(pid: u32) -> bool {
|
|||||||
let result = libc::kill(pid as i32, libc::SIGTERM);
|
let result = libc::kill(pid as i32, libc::SIGTERM);
|
||||||
|
|
||||||
if result != 0 {
|
if result != 0 {
|
||||||
trace!(target: "lazymc", "SIGTERM failed: {}", result);
|
warn!(target: "lazymc", "Sending SIGTERM signal to server failed: {}", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
result == 0
|
result == 0
|
||||||
|
200
src/server.rs
200
src/server.rs
@@ -1,14 +1,30 @@
|
|||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard};
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use minecraft_protocol::data::server_status::ServerStatus;
|
use minecraft_protocol::data::server_status::ServerStatus;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
use tokio::sync::watch;
|
||||||
|
#[cfg(feature = "rcon")]
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::os;
|
use crate::os;
|
||||||
|
|
||||||
|
/// Server cooldown after the process quit.
|
||||||
|
/// Used to give it some more time to quit forgotten threads, such as for RCON.
|
||||||
|
const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);
|
||||||
|
|
||||||
|
/// RCON cooldown. Required period between RCON invocations.
|
||||||
|
///
|
||||||
|
/// The Minecraft RCON implementation is very broken and brittle, this is used in the hopes to
|
||||||
|
/// improve reliability.
|
||||||
|
#[cfg(feature = "rcon")]
|
||||||
|
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
|
||||||
|
|
||||||
/// Server state.
|
/// Server state.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
@@ -56,6 +72,12 @@ pub struct Server {
|
|||||||
/// Matches `State`, utilzes AtomicU8 for better performance.
|
/// Matches `State`, utilzes AtomicU8 for better performance.
|
||||||
state: AtomicU8,
|
state: AtomicU8,
|
||||||
|
|
||||||
|
/// State watch sender, broadcast state changes.
|
||||||
|
state_watch_sender: watch::Sender<State>,
|
||||||
|
|
||||||
|
/// State watch receiver, subscribe to state changes.
|
||||||
|
state_watch_receiver: watch::Receiver<State>,
|
||||||
|
|
||||||
/// Server process PID.
|
/// Server process PID.
|
||||||
///
|
///
|
||||||
/// Set if a server process is running.
|
/// Set if a server process is running.
|
||||||
@@ -79,6 +101,14 @@ pub struct Server {
|
|||||||
///
|
///
|
||||||
/// Used as starting/stopping timeout.
|
/// Used as starting/stopping timeout.
|
||||||
kill_at: RwLock<Option<Instant>>,
|
kill_at: RwLock<Option<Instant>>,
|
||||||
|
|
||||||
|
/// Lock for exclusive RCON operations.
|
||||||
|
#[cfg(feature = "rcon")]
|
||||||
|
rcon_lock: Semaphore,
|
||||||
|
|
||||||
|
/// Last time server was stopped over RCON.
|
||||||
|
#[cfg(feature = "rcon")]
|
||||||
|
rcon_last_stop: Mutex<Option<Instant>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
@@ -87,13 +117,18 @@ impl Server {
|
|||||||
State::from_u8(self.state.load(Ordering::Relaxed))
|
State::from_u8(self.state.load(Ordering::Relaxed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get state receiver to subscribe on server state changes.
|
||||||
|
pub fn state_receiver(&self) -> watch::Receiver<State> {
|
||||||
|
self.state_watch_receiver.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Set a new state.
|
/// Set a new state.
|
||||||
///
|
///
|
||||||
/// This updates various other internal things depending on how the state changes.
|
/// This updates various other internal things depending on how the state changes.
|
||||||
///
|
///
|
||||||
/// Returns false if the state didn't change, in which case nothing happens.
|
/// Returns false if the state didn't change, in which case nothing happens.
|
||||||
fn update_state(&self, state: State, config: &Config) -> bool {
|
async fn update_state(&self, state: State, config: &Config) -> bool {
|
||||||
self.update_state_from(None, state, config)
|
self.update_state_from(None, state, config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set new state, from a current state.
|
/// Set new state, from a current state.
|
||||||
@@ -101,7 +136,7 @@ impl Server {
|
|||||||
/// This updates various other internal things depending on how the state changes.
|
/// This updates various other internal things depending on how the state changes.
|
||||||
///
|
///
|
||||||
/// Returns false if current state didn't match `from` or if nothing changed.
|
/// Returns false if current state didn't match `from` or if nothing changed.
|
||||||
fn update_state_from(&self, from: Option<State>, new: State, config: &Config) -> bool {
|
async fn update_state_from(&self, from: Option<State>, new: State, config: &Config) -> bool {
|
||||||
// Atomically swap state to new, return if from doesn't match
|
// Atomically swap state to new, return if from doesn't match
|
||||||
let old = State::from_u8(match from {
|
let old = State::from_u8(match from {
|
||||||
Some(from) => match self.state.compare_exchange(
|
Some(from) => match self.state.compare_exchange(
|
||||||
@@ -123,8 +158,11 @@ impl Server {
|
|||||||
|
|
||||||
trace!("Change server state from {:?} to {:?}", old, new);
|
trace!("Change server state from {:?} to {:?}", old, new);
|
||||||
|
|
||||||
|
// Broadcast change
|
||||||
|
let _ = self.state_watch_sender.send(new);
|
||||||
|
|
||||||
// Update kill at time for starting/stopping state
|
// Update kill at time for starting/stopping state
|
||||||
*self.kill_at.write().unwrap() = match new {
|
*self.kill_at.write().await = match new {
|
||||||
State::Starting if config.time.start_timeout > 0 => {
|
State::Starting if config.time.start_timeout > 0 => {
|
||||||
Some(Instant::now() + Duration::from_secs(config.time.start_timeout as u64))
|
Some(Instant::now() + Duration::from_secs(config.time.start_timeout as u64))
|
||||||
}
|
}
|
||||||
@@ -143,25 +181,26 @@ impl Server {
|
|||||||
|
|
||||||
// If Starting -> Started, update active time and keep it online for configured time
|
// If Starting -> Started, update active time and keep it online for configured time
|
||||||
if old == State::Starting && new == State::Started {
|
if old == State::Starting && new == State::Started {
|
||||||
self.update_last_active();
|
self.update_last_active().await;
|
||||||
self.keep_online_for(Some(config.time.min_online_time));
|
self.keep_online_for(Some(config.time.min_online_time))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update status as polled from the server.
|
/// Update status as obtained from the server.
|
||||||
///
|
///
|
||||||
/// This updates various other internal things depending on the current state and the given
|
/// This updates various other internal things depending on the current state and the given
|
||||||
/// status.
|
/// status.
|
||||||
pub fn update_status(&self, config: &Config, status: Option<ServerStatus>) {
|
pub async fn update_status(&self, config: &Config, status: Option<ServerStatus>) {
|
||||||
// Update state based on curren
|
// Update state based on curren
|
||||||
match (self.state(), &status) {
|
match (self.state(), &status) {
|
||||||
(State::Stopped | State::Starting, Some(_)) => {
|
(State::Stopped | State::Starting, Some(_)) => {
|
||||||
self.update_state(State::Started, config);
|
self.update_state(State::Started, config).await;
|
||||||
}
|
}
|
||||||
(State::Started, None) => {
|
(State::Started, None) => {
|
||||||
self.update_state(State::Stopped, config);
|
self.update_state(State::Stopped, config).await;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -170,19 +209,22 @@ impl Server {
|
|||||||
if let Some(status) = status {
|
if let Some(status) = status {
|
||||||
// Update last active time if there are online players
|
// Update last active time if there are online players
|
||||||
if status.players.online > 0 {
|
if status.players.online > 0 {
|
||||||
self.update_last_active();
|
self.update_last_active().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.status.write().unwrap().replace(status);
|
self.status.write().await.replace(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to start the server.
|
/// Try to start the server.
|
||||||
///
|
///
|
||||||
/// Does nothing if currently not in stopped state.
|
/// Does nothing if currently not in stopped state.
|
||||||
pub fn start(config: Arc<Config>, server: Arc<Server>, username: Option<String>) -> bool {
|
pub async fn start(config: Arc<Config>, server: Arc<Server>, username: Option<String>) -> bool {
|
||||||
// Must set state from stopped to starting
|
// Must set state from stopped to starting
|
||||||
if !server.update_state_from(Some(State::Stopped), State::Starting, &config) {
|
if !server
|
||||||
|
.update_state_from(Some(State::Stopped), State::Starting, &config)
|
||||||
|
.await
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,22 +234,24 @@ impl Server {
|
|||||||
None => info!(target: "lazymc", "Starting server..."),
|
None => info!(target: "lazymc", "Starting server..."),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke server command in separate task
|
// Spawn server in new task
|
||||||
tokio::spawn(invoke_server_cmd(config, server).map(|_| ()));
|
Self::spawn_server_task(config, server);
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawn the server task.
|
||||||
|
///
|
||||||
|
/// This should not be called directly.
|
||||||
|
fn spawn_server_task(config: Arc<Config>, server: Arc<Server>) {
|
||||||
|
tokio::spawn(invoke_server_cmd(config, server).map(|_| ()));
|
||||||
|
}
|
||||||
|
|
||||||
/// Stop running server.
|
/// Stop running server.
|
||||||
///
|
///
|
||||||
/// This requires the server PID to be known.
|
/// This will attempt to stop the server with all available methods.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub async fn stop(&self, config: &Config) -> bool {
|
pub async fn stop(&self, config: &Config) -> bool {
|
||||||
// We must have a running process
|
|
||||||
let has_process = self.pid.lock().unwrap().is_some();
|
|
||||||
if !has_process {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to stop through RCON if started
|
// Try to stop through RCON if started
|
||||||
#[cfg(feature = "rcon")]
|
#[cfg(feature = "rcon")]
|
||||||
if self.state() == State::Started && stop_server_rcon(config, self).await {
|
if self.state() == State::Started && stop_server_rcon(config, self).await {
|
||||||
@@ -216,10 +260,11 @@ impl Server {
|
|||||||
|
|
||||||
// Try to stop through signal
|
// Try to stop through signal
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if stop_server_signal(config, self) {
|
if stop_server_signal(config, self).await {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warn!(target: "lazymc", "Failed to stop server, no more suitable stopping method to use");
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +272,7 @@ impl Server {
|
|||||||
///
|
///
|
||||||
/// This requires the server PID to be known.
|
/// This requires the server PID to be known.
|
||||||
pub async fn force_kill(&self) -> bool {
|
pub async fn force_kill(&self) -> bool {
|
||||||
if let Some(pid) = *self.pid.lock().unwrap() {
|
if let Some(pid) = *self.pid.lock().await {
|
||||||
return os::force_kill(pid);
|
return os::force_kill(pid);
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -236,7 +281,7 @@ impl Server {
|
|||||||
/// Decide whether the server should sleep.
|
/// Decide whether the server should sleep.
|
||||||
///
|
///
|
||||||
/// Always returns false if it is currently not online.
|
/// Always returns false if it is currently not online.
|
||||||
pub fn should_sleep(&self, config: &Config) -> bool {
|
pub async fn should_sleep(&self, config: &Config) -> bool {
|
||||||
// Server must be online
|
// Server must be online
|
||||||
if self.state() != State::Started {
|
if self.state() != State::Started {
|
||||||
return false;
|
return false;
|
||||||
@@ -246,7 +291,7 @@ impl Server {
|
|||||||
let players_online = self
|
let players_online = self
|
||||||
.status
|
.status
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.await
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|status| status.players.online > 0)
|
.map(|status| status.players.online > 0)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@@ -259,7 +304,7 @@ impl Server {
|
|||||||
let keep_online = self
|
let keep_online = self
|
||||||
.keep_online_until
|
.keep_online_until
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.await
|
||||||
.map(|i| i >= Instant::now())
|
.map(|i| i >= Instant::now())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if keep_online {
|
if keep_online {
|
||||||
@@ -268,7 +313,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Last active time must have passed sleep threshold
|
// Last active time must have passed sleep threshold
|
||||||
if let Some(last_idle) = self.last_active.read().unwrap().as_ref() {
|
if let Some(last_idle) = self.last_active.read().await.as_ref() {
|
||||||
return last_idle.elapsed() >= Duration::from_secs(config.time.sleep_after as u64);
|
return last_idle.elapsed() >= Duration::from_secs(config.time.sleep_after as u64);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,27 +321,27 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decide whether to force kill the server process.
|
/// Decide whether to force kill the server process.
|
||||||
pub fn should_kill(&self) -> bool {
|
pub async fn should_kill(&self) -> bool {
|
||||||
self.kill_at
|
self.kill_at
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.await
|
||||||
.map(|t| t <= Instant::now())
|
.map(|t| t <= Instant::now())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read last known server status.
|
/// Read last known server status.
|
||||||
pub fn status(&self) -> RwLockReadGuard<Option<ServerStatus>> {
|
pub async fn status<'a>(&'a self) -> RwLockReadGuard<'a, Option<ServerStatus>> {
|
||||||
self.status.read().unwrap()
|
self.status.read().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the last active time.
|
/// Update the last active time.
|
||||||
fn update_last_active(&self) {
|
async fn update_last_active(&self) {
|
||||||
self.last_active.write().unwrap().replace(Instant::now());
|
self.last_active.write().await.replace(Instant::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force the server to be online for the given number of seconds.
|
/// Force the server to be online for the given number of seconds.
|
||||||
fn keep_online_for(&self, duration: Option<u32>) {
|
async fn keep_online_for(&self, duration: Option<u32>) {
|
||||||
*self.keep_online_until.write().unwrap() = duration
|
*self.keep_online_until.write().await = duration
|
||||||
.filter(|d| *d > 0)
|
.filter(|d| *d > 0)
|
||||||
.map(|d| Instant::now() + Duration::from_secs(d as u64));
|
.map(|d| Instant::now() + Duration::from_secs(d as u64));
|
||||||
}
|
}
|
||||||
@@ -304,13 +349,21 @@ impl Server {
|
|||||||
|
|
||||||
impl Default for Server {
|
impl Default for Server {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let (state_watch_sender, state_watch_receiver) = watch::channel(State::Stopped);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state: AtomicU8::new(State::Stopped.to_u8()),
|
state: AtomicU8::new(State::Stopped.to_u8()),
|
||||||
|
state_watch_sender,
|
||||||
|
state_watch_receiver,
|
||||||
pid: Default::default(),
|
pid: Default::default(),
|
||||||
status: Default::default(),
|
status: Default::default(),
|
||||||
last_active: Default::default(),
|
last_active: Default::default(),
|
||||||
keep_online_until: Default::default(),
|
keep_online_until: Default::default(),
|
||||||
kill_at: Default::default(),
|
kill_at: Default::default(),
|
||||||
|
#[cfg(feature = "rcon")]
|
||||||
|
rcon_lock: Semaphore::new(1),
|
||||||
|
#[cfg(feature = "rcon")]
|
||||||
|
rcon_last_stop: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,7 +373,7 @@ pub async fn invoke_server_cmd(
|
|||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
state: Arc<Server>,
|
state: Arc<Server>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Build command
|
// Configure command
|
||||||
let args = shlex::split(&config.server.command).expect("invalid server command");
|
let args = shlex::split(&config.server.command).expect("invalid server command");
|
||||||
let mut cmd = Command::new(&args[0]);
|
let mut cmd = Command::new(&args[0]);
|
||||||
cmd.args(args.iter().skip(1));
|
cmd.args(args.iter().skip(1));
|
||||||
@@ -344,7 +397,7 @@ pub async fn invoke_server_cmd(
|
|||||||
state
|
state
|
||||||
.pid
|
.pid
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.await
|
||||||
.replace(child.id().expect("unknown server PID"));
|
.replace(child.id().expect("unknown server PID"));
|
||||||
|
|
||||||
// Wait for process to exit, handle status
|
// Wait for process to exit, handle status
|
||||||
@@ -364,14 +417,19 @@ pub async fn invoke_server_cmd(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set state to stopped, update server PID
|
// Forget server PID
|
||||||
state.pid.lock().unwrap().take();
|
state.pid.lock().await.take();
|
||||||
state.update_state(State::Stopped, &config);
|
|
||||||
|
// Give server a little more time to quit forgotten threads
|
||||||
|
time::sleep(SERVER_QUIT_COOLDOWN).await;
|
||||||
|
|
||||||
|
// Set server state to stopped
|
||||||
|
state.update_state(State::Stopped, &config).await;
|
||||||
|
|
||||||
// Restart on crash
|
// Restart on crash
|
||||||
if crashed && config.server.wake_on_crash {
|
if crashed && config.server.wake_on_crash {
|
||||||
warn!(target: "lazymc", "Server crashed, restarting...");
|
warn!(target: "lazymc", "Server crashed, restarting...");
|
||||||
Server::start(config, state, None);
|
Server::start(config, state, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -384,6 +442,22 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
|
|||||||
|
|
||||||
// RCON must be enabled
|
// RCON must be enabled
|
||||||
if !config.rcon.enabled {
|
if !config.rcon.enabled {
|
||||||
|
trace!(target: "lazymc", "Not using RCON to stop server, disabled in config");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab RCON lock
|
||||||
|
let rcon_lock = server.rcon_lock.acquire().await.unwrap();
|
||||||
|
|
||||||
|
// Ensure RCON has cooled down
|
||||||
|
let rcon_cooled_down = server
|
||||||
|
.rcon_last_stop
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.map(|t| t.elapsed() >= RCON_COOLDOWN)
|
||||||
|
.unwrap_or(true);
|
||||||
|
if !rcon_cooled_down {
|
||||||
|
debug!(target: "lazymc", "Not using RCON to stop server, in cooldown, used too recently");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,11 +478,17 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
|
|||||||
// Invoke stop
|
// Invoke stop
|
||||||
if let Err(err) = rcon.cmd("stop").await {
|
if let Err(err) = rcon.cmd("stop").await {
|
||||||
error!(target: "lazymc", "Failed to invoke stop through RCON: {}", err);
|
error!(target: "lazymc", "Failed to invoke stop through RCON: {}", err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set server to stopping state
|
// Set server to stopping state, update last RCON time
|
||||||
// TODO: set before stop command, revert state on failure
|
server.rcon_last_stop.lock().await.replace(Instant::now());
|
||||||
server.update_state(State::Stopping, config);
|
server.update_state(State::Stopping, config).await;
|
||||||
|
|
||||||
|
drop(rcon_lock);
|
||||||
|
|
||||||
|
// Gracefully close connection
|
||||||
|
rcon.close().await;
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -417,17 +497,29 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
|
|||||||
///
|
///
|
||||||
/// Only available on Unix.
|
/// Only available on Unix.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn stop_server_signal(config: &Config, server: &Server) -> bool {
|
async fn stop_server_signal(config: &Config, server: &Server) -> bool {
|
||||||
// Grab PID
|
// Grab PID
|
||||||
let pid = match *server.pid.lock().unwrap() {
|
let pid = match *server.pid.lock().await {
|
||||||
Some(pid) => pid,
|
Some(pid) => pid,
|
||||||
None => return false,
|
None => {
|
||||||
|
debug!(target: "lazymc", "Could not send stop signal to server process, PID unknown");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set stopping state, send kill signal
|
// Send kill signal
|
||||||
// TODO: revert state on failure
|
if !crate::os::kill_gracefully(pid) {
|
||||||
server.update_state(State::Stopping, config);
|
error!(target: "lazymc", "Failed to send stop signal to server process");
|
||||||
crate::os::kill_gracefully(pid);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update from starting/started to stopping
|
||||||
|
server
|
||||||
|
.update_state_from(Some(State::Starting), State::Stopping, config)
|
||||||
|
.await;
|
||||||
|
server
|
||||||
|
.update_state_from(Some(State::Started), State::Stopping, config)
|
||||||
|
.await;
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
|
|||||||
|
|
||||||
// Initiate server start
|
// Initiate server start
|
||||||
if config.server.wake_on_start {
|
if config.server.wake_on_start {
|
||||||
Server::start(config.clone(), server.clone(), None);
|
Server::start(config.clone(), server.clone(), None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route all incomming connections
|
// Route all incomming connections
|
||||||
|
101
src/status.rs
101
src/status.rs
@@ -1,5 +1,6 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::server::State;
|
use crate::server::State;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
@@ -20,9 +21,6 @@ use crate::proto::{self, Client, ClientState, RawPacket};
|
|||||||
use crate::server::{self, Server};
|
use crate::server::{self, Server};
|
||||||
use crate::service;
|
use crate::service;
|
||||||
|
|
||||||
/// Client holding server state poll interval.
|
|
||||||
const HOLD_POLL_INTERVAL: Duration = Duration::from_millis(500);
|
|
||||||
|
|
||||||
/// Proxy the given inbound stream to a target address.
|
/// Proxy the given inbound stream to a target address.
|
||||||
// TODO: do not drop error here, return Box<dyn Error>
|
// TODO: do not drop error here, return Box<dyn Error>
|
||||||
pub async fn serve(
|
pub async fn serve(
|
||||||
@@ -81,7 +79,7 @@ pub async fn serve(
|
|||||||
|
|
||||||
// Hijack server status packet
|
// Hijack server status packet
|
||||||
if client_state == ClientState::Status && packet.id == proto::STATUS_PACKET_ID_STATUS {
|
if client_state == ClientState::Status && packet.id == proto::STATUS_PACKET_ID_STATUS {
|
||||||
let server_status = server_status(&config, &server);
|
let server_status = server_status(&config, &server).await;
|
||||||
let packet = StatusResponse { server_status };
|
let packet = StatusResponse { server_status };
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
@@ -107,7 +105,7 @@ pub async fn serve(
|
|||||||
.map(|p| p.name);
|
.map(|p| p.name);
|
||||||
|
|
||||||
// Start server if not starting yet
|
// Start server if not starting yet
|
||||||
Server::start(config.clone(), server.clone(), username);
|
Server::start(config.clone(), server.clone(), username).await;
|
||||||
|
|
||||||
// Hold client if enabled and starting
|
// Hold client if enabled and starting
|
||||||
if config.time.hold() && server.state() == State::Starting {
|
if config.time.hold() && server.state() == State::Starting {
|
||||||
@@ -159,54 +157,61 @@ pub async fn hold<'a>(
|
|||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
trace!(target: "lazymc", "Started holding client");
|
trace!(target: "lazymc", "Started holding client");
|
||||||
|
|
||||||
// Set up polling interval, get timeout
|
// A task to wait for suitable server state
|
||||||
let mut poll_interval = time::interval(HOLD_POLL_INTERVAL);
|
// Waits for started state, errors if stopping/stopped state is reached
|
||||||
let since = Instant::now();
|
let task_wait = async {
|
||||||
let timeout = config.time.hold_client_for as u64;
|
let mut state = server.state_receiver();
|
||||||
|
loop {
|
||||||
|
// Wait for state change
|
||||||
|
state.changed().await.unwrap();
|
||||||
|
|
||||||
loop {
|
match state.borrow().deref() {
|
||||||
// TODO: wait for start signal over channel instead of polling
|
// Still waiting on server start
|
||||||
poll_interval.tick().await;
|
State::Starting => {
|
||||||
|
trace!(target: "lazymc", "Server not ready, holding client for longer");
|
||||||
trace!("Polling server state for holding client...");
|
continue;
|
||||||
|
|
||||||
match server.state() {
|
|
||||||
// Still waiting on server start
|
|
||||||
State::Starting => {
|
|
||||||
trace!(target: "lazymc", "Server not ready, holding client for longer");
|
|
||||||
|
|
||||||
// If hold timeout is reached, kick client
|
|
||||||
if since.elapsed().as_secs() >= timeout {
|
|
||||||
warn!(target: "lazymc", "Holding client reached timeout of {}s, disconnecting", timeout);
|
|
||||||
kick(&config.messages.login_starting, &mut inbound.split().1).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
// Server started, start relaying and proxy
|
||||||
}
|
State::Started => {
|
||||||
|
break true;
|
||||||
|
}
|
||||||
|
|
||||||
// Server started, start relaying and proxy
|
// Server stopping, this shouldn't happen, kick
|
||||||
State::Started => {
|
State::Stopping => {
|
||||||
// TODO: drop client if already disconnected
|
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
|
||||||
|
break false;
|
||||||
|
}
|
||||||
|
|
||||||
// Relay client to proxy
|
// Server stopped, this shouldn't happen, disconnect
|
||||||
info!(target: "lazymc", "Server ready for held client, relaying to server");
|
State::Stopped => {
|
||||||
service::server::route_proxy_queue(inbound, config, hold_queue);
|
error!(target: "lazymc", "Server stopped for held client, disconnecting");
|
||||||
return Ok(());
|
break false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Server stopping, this shouldn't happen, kick
|
// Wait for server state with timeout
|
||||||
State::Stopping => {
|
let timeout = Duration::from_secs(config.time.hold_client_for as u64);
|
||||||
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
|
match time::timeout(timeout, task_wait).await {
|
||||||
kick(&config.messages.login_stopping, &mut inbound.split().1).await?;
|
// Relay client to proxy
|
||||||
break;
|
Ok(true) => {
|
||||||
}
|
info!(target: "lazymc", "Server ready for held client, relaying to server");
|
||||||
|
service::server::route_proxy_queue(inbound, config, hold_queue);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Server stopped, this shouldn't happen, disconnect
|
// Server stopping/stopped, this shouldn't happen, kick
|
||||||
State::Stopped => {
|
Ok(false) => {
|
||||||
error!(target: "lazymc", "Server stopped for held client, disconnecting");
|
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
|
||||||
break;
|
kick(&config.messages.login_stopping, &mut inbound.split().1).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timeout reached, kick with starting message
|
||||||
|
Err(_) => {
|
||||||
|
warn!(target: "lazymc", "Held client reached timeout of {}s, disconnecting", config.time.hold_client_for);
|
||||||
|
kick(&config.messages.login_starting, &mut inbound.split().1).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,8 +241,8 @@ async fn kick(msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build server status object to respond to client with.
|
/// Build server status object to respond to client with.
|
||||||
fn server_status(config: &Config, server: &Server) -> ServerStatus {
|
async fn server_status(config: &Config, server: &Server) -> ServerStatus {
|
||||||
let status = server.status();
|
let status = server.status().await;
|
||||||
|
|
||||||
// Select version and player max from last known server status
|
// Select version and player max from last known server status
|
||||||
let (version, max) = match status.as_ref() {
|
let (version, max) = match status.as_ref() {
|
||||||
|
Reference in New Issue
Block a user