├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── config.example ├── extras ├── .githooks │ ├── common │ └── pre-push ├── bash_autocomplete ├── docker │ ├── Dockerfile.centos │ └── Dockerfile.ubuntu ├── factorio.runit.example ├── factorio.service.example └── test │ ├── config-helper.bash │ ├── factorio.bats │ ├── http-mock-helper.bash │ ├── new-game.bats │ └── tmp-helper.bash └── factorio /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [ master, github-workflows-ci ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | shellchecks: 12 | runs-on: ubuntu-latest 13 | environment: "CI Tests" 14 | steps: 15 | #- name: Login to Docker Hub 16 | # uses: docker/login-action@v2 17 | # with: 18 | # username: ${{ secrets.DOCKERHUB_USERNAME }} 19 | # password: ${{ secrets.DOCKERHUB_TOKEN }} 20 | - name: Checkout repo 21 | uses: actions/checkout@master 22 | - name: Shellchecks 23 | run: | 24 | docker run --rm -v "$GITHUB_WORKSPACE:/mnt" koalaman/shellcheck:stable extras/.githooks/* 25 | docker run --rm -v "$GITHUB_WORKSPACE:/mnt" koalaman/shellcheck:stable factorio 26 | bats: 27 | runs-on: ubuntu-latest 28 | environment: "CI Tests" 29 | steps: 30 | #- name: Login to Docker Hub 31 | # uses: docker/login-action@v2 32 | # with: 33 | # username: ${{ secrets.DOCKERHUB_USERNAME }} 34 | # password: ${{ secrets.DOCKERHUB_TOKEN }} 35 | - name: Checkout repo 36 | uses: actions/checkout@master 37 | with: 38 | submodules: recursive 39 | - name: Prepare env 40 | run: | 41 | echo "factorio_version=1.1.61" >> $GITHUB_ENV 42 | echo "factorio_user=factorio" >> $GITHUB_ENV 43 | echo "factorio_group=factorio" >> $GITHUB_ENV 44 | - name: Add factorio user 45 | run: | 46 | sudo -E addgroup --system "${factorio_group}" 47 | sudo -E adduser --system --ingroup "${factorio_group}" "${factorio_user}" 48 | - name: Bats without resources 49 | run: sudo -E -u "${factorio_user}" extras/test/libs/bats-core/bin/bats extras/test --jobs 10 50 | - name: Fetch factorio 51 | run: | 52 | factorio_tar="/tmp/factorio_headless_x64_${factorio_version}.tar.xz" 53 | sudo -E wget -O "${factorio_tar}" "https://factorio.com/get-download/${factorio_version}/headless/linux64" 54 | echo "factorio_tar=${factorio_tar}" >> $GITHUB_ENV 55 | echo "FACTORIO_INIT_WITH_TEST_RESOURCES=1" >> $GITHUB_ENV 56 | - name: Bats with resources 57 | run: sudo -E -u "${factorio_user}" extras/test/libs/bats-core/bin/bats extras/test --jobs 10 58 | - name: Install factorio 59 | run: | 60 | sudo -E tar -xvf "${factorio_tar}" -C /opt 61 | sudo -E chown -R "${factorio_user}":"${factorio_group}" /opt/factorio 62 | sudo -E -u "${factorio_user}" /opt/factorio/bin/x64/factorio --create /opt/factorio/saves/server-save 63 | sudo -E -u "${factorio_user}" cp /opt/factorio/data/server-settings.example.json /opt/factorio/data/server-settings.json 64 | echo "FACTORIO_INIT_WITH_PRE_INSTALLED_GAME=1" >> $GITHUB_ENV 65 | - name: Bats with pre-installed game 66 | run: sudo -E -u "${factorio_user}" extras/test/libs/bats-core/bin/bats extras/test 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config 2 | **/*.swp 3 | *.code-workspace -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/libs/bats-support"] 2 | path = extras/test/libs/bats-support 3 | url = https://github.com/ztombol/bats-support 4 | [submodule "test/libs/bats-assert"] 5 | path = extras/test/libs/bats-assert 6 | url = https://github.com/ztombol/bats-assert 7 | [submodule "test/libs/bats-core"] 8 | path = extras/test/libs/bats-core 9 | url = https://github.com/bats-core/bats-core.git 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tobias Wallin - https://github.com/Bisa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Factorio Init 2 | 3 | A factorio init script in bash 4 | 5 | ## Requirements 6 | 7 | - [wget](https://www.gnu.org/software/wget/) 8 | - [curl](https://curl.se/) 9 | - [xz](https://tukaani.org/xz/) (wube uses tar.xz for tarballs, only needed if you install with this script) 10 | - [bash-completion](https://github.com/scop/bash-completion) (optional) 11 | 12 | ## Debugging 13 | 14 | If you find yourself wondering why stuff is not working the way you expect: 15 | 16 | - Check the logs, I suggest you `tail -f /opt/factorio/factorio-current.log` in a separate session 17 | - Enable debugging in the config and/or: 18 | - Try running the same commands as the factorio user (`/opt/factorio-init/factorio invocation` will tell you what the factorio user tries to run at start) 19 | 20 | ```bash 21 | /opt/factorio-init/factorio invocation 22 | # Run this as the factorio user, example: 23 | sudo -u factorio 'whatever invocation gave you' 24 | # You should see some output in your terminal here, hopefully giving 25 | # you a hint of what is going wrong 26 | ``` 27 | 28 | ## Install 29 | 30 | - Create a directory where you want to store this script along with configuration. (either copy-paste the files or clone from github): 31 | 32 | ```bash 33 | cd '/opt' 34 | git clone https://github.com/Bisa/factorio-init.git 35 | ``` 36 | 37 | - Rename or copy `config.example` to `config` and modify the values within according to your setup. 38 | 39 | - Unless you need files from the `extras` dir (check the below sections) you can safely remove it. 40 | 41 | ### Notes for users with an OS that has a older glibc version 42 | 43 | The config has options for declaring an alternate glibc root. The user millisa over on the factorio forums has created a wonderful [guide](https://forums.factorio.com/viewtopic.php?t=54654#p324493) to follow on creating this alternate glibc root (side by side). 44 | 45 | ### First-run 46 | 47 | - If you don't have Factorio installed already, use the `install` command: 48 | 49 | ```bash 50 | /opt/factorio-init/factorio install # see help for options 51 | ``` 52 | 53 | - The installation routine creates Factorio's `config.ini` automatically. 54 | 55 | - If you previously ran Factorio without this script, the existing `config.ini` should work fine. 56 | 57 | ### Autocompletion 58 | 59 | - Copy/symlink or source the bash_autocompletion file 60 | - Ensure the factorio script is in your path 61 | 62 | ```bash 63 | # either symlink: 64 | ln -s /opt/factorio-init/extras/bash_autocomplete /etc/bash_completion.d/factorio 65 | # or source: 66 | echo "source /opt/factorio-init/extras/bash_autocomplete" >> ~/.bashrc 67 | 68 | # then ensure factorio-init is added to your PATH, ie by: 69 | ln -s /opt/factorio-init/factorio /usr/local/bin/factorio 70 | 71 | # restart your shell to verify that it worked 72 | ``` 73 | 74 | ### Systemd 75 | 76 | - Copy the example service, adjust & reload 77 | 78 | ```bash 79 | cp /opt/factorio-init/extras/factorio.service.example /etc/systemd/system/factorio.service 80 | # Edit the service file to suit your environment then reload systemd 81 | systemctl daemon-reload 82 | ``` 83 | 84 | - Verify that the server starts 85 | 86 | ```bash 87 | systemctl start factorio 88 | systemctl status -l factorio 89 | # Remember to enable the service at startup if you want that: 90 | systemctl enable factorio 91 | ``` 92 | 93 | Note that systemd won't be able to keep track of the server process if you use this script to restart during updates. Use the config option ```UPDATE_PREVENT_RESTART=1``` and implement your own stop/start logic to work around this. 94 | 95 | ### SysvInit 96 | 97 | - Symlink the init script: 98 | 99 | ```bash 100 | ln -s /opt/factorio-init/factorio /etc/init.d/factorio 101 | # Make the script executable: 102 | chmod +x /opt/factorio-init/factorio 103 | # Try it out: 104 | service factorio help 105 | # Do not forget to enable the service at boot if you want that. 106 | ``` 107 | 108 | ## Contributing 109 | 110 | When contributing to this repo, please ensure your contribution is covered by at least one test in ```test/factorio.bats``` or the very least: do not create pull requests with failing tests, thank you. 111 | 112 | ### Test the code 113 | 114 | Testing is done using [bats-core](https://github.com/bats-core/bats-core), [bats-assert](https://github.com/ztombol/bats-assert) and [bats-support](https://github.com/ztombol/bats-support). 115 | 116 | - Write a test case, example: 117 | 118 | ```bash 119 | @test "DEBUG=1 produces output" { 120 | # To access functions within ./factorio, source it then use the run command: 121 | source ./factorio 122 | export DEBUG=1 123 | run debug "TEST" 124 | # use the various asserts from bats-assert 125 | assert_output "DEBUG: TEST" 126 | } 127 | ``` 128 | 129 | - init and update the submodules (if you did not already) 130 | 131 | ```bash 132 | git submodule init 133 | git submodule update 134 | ``` 135 | 136 | - Then run the tests, see the following sections: 137 | 138 | #### With .githooks 139 | 140 | - Set the hooks path to our .githooks directory 141 | 142 | ```bash 143 | git config --local core.hooksPath extras/.githooks 144 | ``` 145 | 146 | The ```extras/.githooks/pre-push``` will run shellcheck, local tests as well as docker tests with and without resources. 147 | 148 | #### With Docker 149 | 150 | - Build the docker image(s) (a slightly modified ubuntu/centos) 151 | 152 | ```bash 153 | docker build --build-arg ubuntu_version=20.04 \ 154 | --build-arg factorio_version=1.1.61 \ 155 | --tag ubuntu-finit:latest - < extras/docker/Dockerfile.ubuntu 156 | docker build --build-arg centos_version=centos8 \ 157 | --build-arg factorio_version=1.1.61 \ 158 | --tag centos-finit:latest - < extras/docker/Dockerfile.centos 159 | ``` 160 | 161 | Adding ```--target no-test-resources``` to the build command will avoid downloading test resources online but it will also skip tests that rely on the resources(!) 162 | 163 | - Then run the image, mounting the current directory and removing the container once it's done 164 | 165 | ```bash 166 | docker run -it --rm -v "$(pwd):/opt/factorio-init" --workdir /opt/factorio-init ubuntu-finit:latest extras/test 167 | docker run -it --rm -v "$(pwd):/opt/factorio-init" --workdir /opt/factorio-init centos-finit:latest extras/test 168 | ``` 169 | 170 | #### Manually 171 | 172 | Please note that some tests will be skipped unless you run them with the docker image or via .githooks, but running them manually is a quick way to get started. 173 | 174 | - run the tests 175 | 176 | ```bash 177 | ./extras/test/libs/bats-core/bin/bats extras/test 178 | ``` 179 | 180 | Using [parallel](https://www.gnu.org/software/parallel), adding ```--jobs 10``` to the above (adjust the number accordingly) will allow you to execute more tests in parallel and in turn possibly decrease the total time required to complete the run. 181 | 182 | ## Thank You 183 | 184 | - To all who find this script useful in one way or the other 185 | - A big thank you to [Wube](https://www.factorio.com/game/about) for making [Factorio](https://www.factorio.com/) 186 | - A special thanks to NoPantsMcDance, Oxyd, HanziQ, TheFactorioCube and all other frequent users of the [#factorio](irc://irc.esper.net/#factorio) channel @ esper.net 187 | - Thank you to Salzig for pointing me in the right direction when it comes to input redirection 188 | - At last, but not least; Thank you to all [contributors](https://github.com/Bisa/factorio-init/graphs/contributors) and users posting [issues](https://github.com/Bisa/factorio-init/issues) in my [github](https://github.com/Bisa/factorio-init/) project or on the [factorio forums](https://forums.factorio.com/viewtopic.php?f=133&t=13874) 189 | 190 | You are all a great source of motivation, thank you. 191 | 192 | ## License 193 | 194 | This code is realeased with the MIT license, see the LICENSE file. 195 | -------------------------------------------------------------------------------- /config.example: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Enable debugging, useful when you want to figure out why 4 | # this script is not behaving the way you expect it to do 5 | DEBUG=0 6 | 7 | # Enable alternative glibc directory for systems using older glibc versions ( ie RHEL CentOS and others ) 8 | ALT_GLIBC=0 9 | # Put the Absolute path to the side by side glibc root here 10 | ALT_GLIBC_DIR="/opt/glibc-2.18" 11 | #Version of alt glibc goes here (i.e 2.18) 12 | ALT_GLIBC_VER="2.18" 13 | 14 | # What do you want to call this service? 15 | SERVICE_NAME="Factorio" 16 | 17 | # Which user/group is running factorio? 18 | # Running any public service as "root" is generally not recommended 19 | USERNAME=factorio 20 | USERGROUP=factorio 21 | 22 | # The absolute path to the factorio directory 23 | FACTORIO_PATH=/opt/factorio 24 | 25 | # Server settings file, see data/server-settings.example.json 26 | SERVER_SETTINGS=${FACTORIO_PATH}/data/server-settings.json 27 | 28 | # Server admin settings 29 | # Server admin list file, must be in the following format: 30 | # [ 31 | # "admin1", 32 | # "admin2", 33 | # "adminX" 34 | # ] 35 | # If the file does not exist in ${FACTORIO_PATH}/data, a blank copy will be created. 36 | ADMINLIST=${FACTORIO_PATH}/data/server-adminlist.json 37 | 38 | # The whitelist file is in the same format as the admin list file. 39 | # If the file does not exist in ${FACTORIO_PATH}/data, it will not be used. 40 | WHITELIST=${FACTORIO_PATH}/data/server-whitelist.json 41 | 42 | # The banlist file is in the same format as the admin list file. 43 | # If the file does not exist in ${FACTORIO_PATH}/data, it will not be used. 44 | BANLIST=${FACTORIO_PATH}/data/server-banlist.json 45 | 46 | # Port on which you want to run the server 47 | PORT=34197 48 | 49 | # Save the command/chat/log on server start? Default location /opt/factorio/server.out 50 | # Setting this to 0 will cause the script to erase the log file on each start 51 | # If you set this to 1 you might want to apply logrotate on the log or it will eventually fill your disk 52 | SAVELOG=0 53 | 54 | # Delay starting the game server until the we confirm the pingpong servers are reachable. 55 | # This makes sure that game server detects the correct external IP in a NAT setup. 56 | # Useful for Cloud providers where VMs may start faster than their Internet connectivity is plumbed. 57 | WAIT_PINGPONG=0 58 | 59 | # Time after which the factorio server will be killed with force. 60 | # Adjust this to allow more time for map saving, e.g. on modded servers with multiple maps. 61 | # Increased values must be propagated to service files (systemd) as well. 62 | FORCED_SHUTDOWN=15 63 | 64 | # Using install, it's possible to cache the tarballs fetched from Wube 65 | INSTALL_CACHE_TAR=1 66 | # set this to a directory where you allow this script to create a 'factorio-install' directory 67 | INSTALL_CACHE_DIR=/tmp 68 | 69 | # 70 | # narcotiq made a sweet update script for factorio and by cloning into this 71 | # you can simply run "factorio update" provided you configure the below arguments 72 | # To install the updater: 73 | # 74 | # git clone https://github.com/narc0tiq/factorio-updater 75 | # 76 | 77 | # absolute path to the factorio-updater script 78 | UPDATE_SCRIPT=/path/to/update_factorio.py 79 | # Note that if you set HEADLESS=1 the username/token will not be used as the headless 80 | # download is provided free of charge 81 | HEADLESS=1 82 | UPDATE_USERNAME=you 83 | UPDATE_TOKEN=yourtoken 84 | UPDATE_EXPERIMENTAL=0 85 | # set this to a directory where you allow this script to create a 'factorio-update' directory. 86 | UPDATE_TMPDIR=/tmp 87 | # The directory ${UPDATE_TMPDIR}/factorio-update will be used by the updater to store patches for factorio. 88 | # If you set UPDATE_PERSIST_TMPDIR=1 this script will not try to clean up the directory. 89 | # This will effectively cache the patch files for later use but be aware that you need to handle eventual clean-up 90 | # of the ${UPDATE_TMPDIR}/factorio-update directory yourself. 91 | UPDATE_PERSIST_TMPDIR=0 92 | # prevent the script from restarting factorio during updates by setting this to 1 93 | # (note! This also means that you will be prevented from applying updates with the 94 | # script if the server is running) 95 | UPDATE_PREVENT_RESTART=0 96 | 97 | # 98 | # There is also integration with Tantrisse's Factorio-mods-manager 99 | # Uses UPDATE_USERNAME and UPDATE_TOKEN for mod management 100 | # Access is exposed under the mod subcommand 101 | # To install: 102 | # git clone https://github.com/Tantrisse/Factorio-mods-manager.git 103 | # 104 | 105 | # absolute path to the Factorio-mods-manager script 106 | MOD_SCRIPT_DIR=/path/to/mods_manager.py 107 | 108 | # Extras 109 | # Additional binary arguments, these will be sent to the binary when issuing the "start" command 110 | EXTRA_BINARGS="" 111 | -------------------------------------------------------------------------------- /extras/.githooks/common: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file should be sourced, not executed directly. 4 | 5 | root_dir="$(git rev-parse --show-toplevel)" 6 | dockerfiles="${root_dir}/extras/docker/Dockerfile" 7 | 8 | function dockertest() { 9 | dist="$1"; shift 10 | versions="$1"; shift 11 | fversion="$1"; shift 12 | targets="$1"; shift 13 | IFS=" " read -r -a extra_args <<< "${1:-}" 14 | 15 | for version in $versions; do 16 | image="${dist}-${version}" 17 | for target in $targets; do 18 | echo 19 | echo "Testing ${image}:${target} (factorio ${fversion})" 20 | echo 21 | docker build --build-arg "${dist}_version=${version}" \ 22 | --build-arg "factorio_version=${fversion}" \ 23 | --target "${target}" \ 24 | --tag "${image}-finit:${target}" - < "${dockerfiles}.${dist}" || return 1 25 | options=(-v "${root_dir}:/opt/factorio-init" "${image}-finit:${target}" "${extra_args[@]}" extras/test) 26 | docker run -t --rm "${options[@]}" || return 1 27 | done 28 | done 29 | } 30 | 31 | function run_shellchecks() { 32 | docker run --rm -v "${root_dir}:/mnt" koalaman/shellcheck:stable extras/.githooks/* && 33 | docker run --rm -v "${root_dir}:/mnt" koalaman/shellcheck:stable factorio 34 | } 35 | 36 | function run_localtests() { 37 | "${root_dir}/extras/test/libs/bats-core/bin/bats" "${root_dir}/extras/test" || exit 1 38 | } 39 | 40 | function run_dockertests() { 41 | factorio_version="1.1.61" 42 | targets="with-test-resources sans-test-resources" 43 | 44 | dockertest ubuntu latest "${factorio_version}" "${targets}" "--jobs 10" && 45 | dockertest "ubuntu" "latest" "${factorio_version}" "with-pre-installed-game" #TODO make pre-installed game tests run in parallel 46 | 47 | #dockertest "ubuntu" "20.04 18.04" "${factorio_version}" "${targets}" "--jobs 10" && 48 | #dockertest "ubuntu" "20.04 18.04" "${factorio_version}" "with-pre-installed-game" && #TODO make pre-installed game tests run in parallel 49 | #dockertest "centos" "centos8" "${factorio_version}" "with-pre-installed-game ${targets}" || exit 1 50 | #dockertest "centos" "centos7" "${factorio_version}" "with-pre-installed-game-glibc with-glibc-sidebyside" || exit 1 51 | } 52 | -------------------------------------------------------------------------------- /extras/.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # TODO: Break the running of tests out into a separate script to facilitate: 4 | # - only running selective tests/linting pre-commit, for sanity 5 | # - running all/some tests from commandline (to avoid stashing/doing git specific stuff) 6 | # - running all tests before pushing (ie what is happening now on every pre-commit) 7 | 8 | set -euo pipefail 9 | 10 | # find out where this repo is situated 11 | root_dir="$(git rev-parse --show-toplevel)" 12 | source "$root_dir/extras/.githooks/common" || exit 1 13 | 14 | z40=0000000000000000000000000000000000000000 15 | 16 | # shellcheck disable=SC2034 17 | while read -r local_ref local_sha remote_ref remote_sha; do 18 | if [ "$local_sha" = $z40 ]; then 19 | # Handle delete 20 | : 21 | else 22 | [[ "${SKIP_TESTS_PRE_PUSH:-False}" == "True" ]] && exit 0 23 | run_shellchecks && 24 | run_localtests && 25 | run_dockertests 26 | exit $? 27 | fi 28 | done 29 | -------------------------------------------------------------------------------- /extras/bash_autocomplete: -------------------------------------------------------------------------------- 1 | _factorio() 2 | { 3 | local cur prev opts base 4 | COMPREPLY=() 5 | cur="${COMP_WORDS[COMP_CWORD]}" 6 | prev="${COMP_WORDS[COMP_CWORD-1]}" 7 | 8 | # basic options 9 | opts=`factorio listcommands 2> /dev/null` 10 | 11 | # command arguments 12 | case "${prev}" in 13 | log) 14 | COMPREPLY=( $(compgen -W "-t --tail" -- ${cur}) ) 15 | return 0 16 | ;; 17 | chatlog) 18 | COMPREPLY=( $(compgen -W "-t --tail" -- ${cur}) ) 19 | return 0 20 | ;; 21 | update) 22 | COMPREPLY=( $(compgen -W "--dry-run" -- ${cur}) ) 23 | return 0 24 | ;; 25 | new-game|load-save|save-game) 26 | COMPREPLY=( $(compgen -W "`factorio listsaves 2> /dev/null`" -- ${cur}) ) 27 | return 0 28 | ;; 29 | esac 30 | 31 | COMPREPLY=($(compgen -W "${opts}" -- ${cur})) 32 | return 0 33 | } 34 | complete -F _factorio factorio 35 | -------------------------------------------------------------------------------- /extras/docker/Dockerfile.centos: -------------------------------------------------------------------------------- 1 | ARG centos_version=centos8 2 | ARG factorio_version=1.1.61 3 | ARG factorio_user=factorio 4 | 5 | ### A base image without test resources: 6 | FROM centos:$centos_version AS sans-test-resources 7 | ARG factorio_user 8 | RUN yum update -y && \ 9 | yum install -y \ 10 | wget && \ 11 | yum clean all && \ 12 | rm -rf /var/cache/yum 13 | RUN useradd $factorio_user 14 | USER $factorio_user 15 | WORKDIR /opt/factorio-init 16 | ENTRYPOINT ["bash", "/opt/factorio-init/extras/test/libs/bats-core/bin/bats"] 17 | 18 | ### Build onto the base, add test resources: 19 | FROM sans-test-resources AS with-test-resources 20 | ARG factorio_version 21 | ENV FACTORIO_INIT_WITH_TEST_RESOURCES=1 22 | RUN wget -O /tmp/factorio_headless_x64_${factorio_version}.tar.xz \ 23 | https://factorio.com/get-download/${factorio_version}/headless/linux64 24 | 25 | ### Build onto the with-test-resources for alternate glibc (for centos7 testing) 26 | FROM with-test-resources AS with-glibc-sidebyside 27 | ARG factorio_user 28 | USER root 29 | RUN yum groupinstall -y \ 30 | "Development tools" && \ 31 | yum install -y \ 32 | glibc-devel.i686 \ 33 | glibc.i686 && \ 34 | yum clean all && \ 35 | rm -rf /var/cache/yum 36 | WORKDIR /tmp 37 | RUN wget -q http://ftp.gnu.org/gnu/glibc/glibc-2.18.tar.gz -O - |tar -xvz 38 | WORKDIR /tmp/glibc-2.18/glibc-build 39 | RUN sed "s/3\.\[89\]/3\.\[89\]\* | 4/" -i ../configure 40 | RUN ../configure --prefix='/opt/glibc-2.18' && \ 41 | sed -i -e 's#if (/$ld_so_name/) {#if (/\Q$ld_so_name\E/) {#' \ 42 | ../scripts/test-installation.pl && \ 43 | make && \ 44 | make install # these take a while (~5 minutes on a i5 3.5GHz 32GB RAM WSL2 box) 45 | 46 | USER $factorio_user 47 | ENV FACTORIO_INIT_ALT_GLIBC=1 48 | WORKDIR /opt/factorio-init 49 | 50 | ### and pre-install the game for glibc 51 | FROM with-glibc-sidebyside AS with-pre-installed-game-glibc 52 | ARG factorio_user 53 | ARG factorio_group 54 | ARG factorio_version 55 | 56 | USER root 57 | RUN tar -xvf /tmp/factorio_headless_x64_${factorio_version}.tar.xz -C /opt && \ 58 | chown -R ${factorio_user}:${factorio_group} /opt/factorio 59 | USER ${factorio_user} 60 | ENV FACTORIO_INIT_WITH_PRE_INSTALLED_GAME=1 61 | RUN /opt/glibc-2.18/lib/ld-2.18.so --library-path /opt/glibc-2.18/lib \ 62 | /opt/factorio/bin/x64/factorio --create /opt/factorio/saves/server-save \ 63 | --executable-path /opt/factorio/bin/x64/factorio && \ 64 | cp /opt/factorio/data/server-settings.example.json /opt/factorio/data/server-settings.json 65 | 66 | ### and pre-install the game 67 | FROM with-test-resources AS with-pre-installed-game 68 | ARG factorio_user 69 | ARG factorio_group 70 | ARG factorio_version 71 | 72 | USER root 73 | RUN tar -xvf /tmp/factorio_headless_x64_${factorio_version}.tar.xz -C /opt && \ 74 | chown -R ${factorio_user}:${factorio_group} /opt/factorio 75 | USER ${factorio_user} 76 | ENV FACTORIO_INIT_WITH_PRE_INSTALLED_GAME=1 77 | RUN /opt/factorio/bin/x64/factorio --create /opt/factorio/saves/server-save && \ 78 | cp /opt/factorio/data/server-settings.example.json /opt/factorio/data/server-settings.json 79 | -------------------------------------------------------------------------------- /extras/docker/Dockerfile.ubuntu: -------------------------------------------------------------------------------- 1 | ARG ubuntu_version=20.04 2 | 3 | ### A base image without test resources: 4 | FROM ubuntu:$ubuntu_version AS sans-test-resources 5 | ARG factorio_user=factorio 6 | ARG factorio_group=factorio 7 | 8 | RUN apt-get update && apt-get install -y \ 9 | parallel \ 10 | wget \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | RUN addgroup --system $factorio_group 14 | RUN adduser --system --ingroup $factorio_group $factorio_user 15 | 16 | USER $factorio_user 17 | WORKDIR /opt/factorio-init 18 | ENTRYPOINT ["bash", "/opt/factorio-init/extras/test/libs/bats-core/bin/bats"] 19 | 20 | ### Build onto the base, add test resources: 21 | FROM sans-test-resources AS with-test-resources 22 | ENV FACTORIO_INIT_WITH_TEST_RESOURCES=1 23 | ARG factorio_version=1.1.61 24 | 25 | RUN wget -O /tmp/factorio_headless_x64_${factorio_version}.tar.xz \ 26 | https://factorio.com/get-download/${factorio_version}/headless/linux64 27 | 28 | ### and pre-install the game 29 | FROM with-test-resources AS with-pre-installed-game 30 | ARG factorio_version 31 | ARG factorio_user=factorio 32 | ARG factorio_group=factorio 33 | 34 | USER root 35 | RUN tar -xvf /tmp/factorio_headless_x64_${factorio_version}.tar.xz -C /opt && \ 36 | chown -R ${factorio_user}:${factorio_group} /opt/factorio 37 | USER ${factorio_user} 38 | ENV FACTORIO_INIT_WITH_PRE_INSTALLED_GAME=1 39 | RUN /opt/factorio/bin/x64/factorio --create /opt/factorio/saves/server-save && \ 40 | cp /opt/factorio/data/server-settings.example.json /opt/factorio/data/server-settings.json 41 | 42 | -------------------------------------------------------------------------------- /extras/factorio.runit.example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | FACTORIO_USER="factorio"; 3 | FACTORIO_GROUP="factorio"; 4 | 5 | exec chpst -u "${FACTORIO_USER}:${FACTORIO_GROUP}" factorio foreground 6 | -------------------------------------------------------------------------------- /extras/factorio.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Factorio Server 3 | Wants=network-online.target 4 | After=network.target network-online.target 5 | StartLimitIntervalSec=600 6 | 7 | [Service] 8 | User=factorio 9 | Group=factorio 10 | 11 | # We will store a pid file in your ${WRITE_DIR}/server.pid 12 | # Adjust if you change the write dir of your server 13 | PIDFile=/opt/factorio/server.pid 14 | 15 | Type=forking 16 | TimeoutStartSec=20 17 | ExecStart=/opt/factorio-init/factorio start 18 | TimeoutStopSec=20 19 | ExecStop=/opt/factorio-init/factorio stop 20 | RestartSec=20 21 | Restart=on-failure 22 | StartLimitBurst=5 23 | 24 | [Install] 25 | WantedBy=multi-user.target 26 | -------------------------------------------------------------------------------- /extras/test/config-helper.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | check_config_defaults_skip_command() { 4 | config_file="${BATS_TMPDIR}/readable" 5 | touch "${config_file}" 6 | 7 | source $factorio_script 8 | export DEBUG=1 9 | 10 | command=$1 11 | run load_config "${config_file}" ${command} 12 | assert_line "DEBUG: Skip check/loading defaults for command '${command}'" 13 | } 14 | -------------------------------------------------------------------------------- /extras/test/factorio.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ./test/libs/bats/bin/bats 2 | load 'libs/bats-support/load' 3 | load 'libs/bats-assert/load' 4 | 5 | factorio_script=./factorio 6 | 7 | @test ".debug DEBUG=1 produces output" { 8 | source $factorio_script 9 | export DEBUG=1 10 | run debug "TEST" # TODO: find a way to verify stdout vs stderr output 11 | 12 | assert_output "DEBUG: TEST" 13 | } 14 | 15 | @test ".debug DEBUG=0 produces no output" { 16 | source $factorio_script 17 | export DEBUG=0 18 | run debug "TEST" 19 | 20 | assert_output "" 21 | } 22 | 23 | @test ".debug is turned off by default" { 24 | source $factorio_script 25 | run debug "TEST" 26 | 27 | assert_output "" 28 | } 29 | 30 | @test ".error produces output" { 31 | source $factorio_script 32 | run error "An error occured!" # TODO: find a way to verify stdout vs stderr output 33 | 34 | assert_output 'An error occured!' 35 | } 36 | 37 | @test ".info produces output" { 38 | source $factorio_script 39 | run info "Informative text" # TODO: find a way to verify stdout vs stderr output 40 | 41 | assert_output 'Informative text' 42 | } 43 | 44 | @test ".load_config fails on missing config file" { 45 | source $factorio_script 46 | export DEBUG=0 47 | run load_config "nonexisting_config_file" 48 | 49 | assert_output "Config file 'nonexisting_config_file' does not exist!" 50 | assert_failure 1 51 | } 52 | 53 | @test ".load_config fails on non-readable config file" { 54 | [ $(id -u) -eq 0 ] && skip "We are running as root, we can not test non-readability" 55 | config_file="${BATS_TMPDIR}/non-readable" 56 | touch "${config_file}" 57 | chmod a-r "${config_file}" 58 | 59 | source $factorio_script 60 | export DEBUG=0 61 | run load_config "${config_file}" 62 | 63 | assert_output "Unable to read config file '"${config_file}"'!" 64 | assert_failure 1 65 | } 66 | 67 | @test ".load_config without args uses default file" { 68 | source $factorio_script 69 | export DEBUG=1 70 | run load_config 71 | 72 | assert_line --index 0 "DEBUG: Trying to load config file './config'." 73 | } 74 | 75 | @test ".load_config sets default FACTORIO_PATH" { 76 | source $factorio_script 77 | unset FACTORIO_PATH 78 | load_config ./config.example 79 | assert_equal "${FACTORIO_PATH}" '/opt/factorio' 80 | } 81 | 82 | @test ".load_config does not override FACTORIO_PATH" { 83 | source $factorio_script 84 | config_file="${BATS_TMPDIR}/readable" 85 | touch "${config_file}" 86 | export FACTORIO_PATH="-my-predefined-path-" 87 | load_config "${config_file}" 88 | assert_equal "${FACTORIO_PATH}" '-my-predefined-path-' 89 | } 90 | 91 | @test ".load_config passes command to .config_defaults" { 92 | config_file="${BATS_TMPDIR}/readable" 93 | touch "${config_file}" 94 | 95 | source $factorio_script 96 | export DEBUG=1 97 | 98 | run load_config "${config_file}" install 99 | assert_line "DEBUG: Check/Loading config defaults for command 'install'" 100 | } 101 | 102 | @test ".config_defualts skips '' (empty) command" { 103 | load 'config-helper' 104 | check_config_defaults_skip_command "" 105 | } 106 | 107 | @test ".config_defualts skips 'install' command" { 108 | load 'config-helper' 109 | check_config_defaults_skip_command "install" 110 | } 111 | 112 | @test ".config_defualts skips 'help' command" { 113 | load 'config-helper' 114 | check_config_defaults_skip_command "help" 115 | } 116 | 117 | @test ".config_defualts skips 'listcommands' command" { 118 | load 'config-helper' 119 | check_config_defaults_skip_command "listcommands" 120 | } 121 | 122 | @test ".config_defualts skips 'version' command" { 123 | load 'config-helper' 124 | check_config_defaults_skip_command "version" 125 | } 126 | 127 | @test ".run_main 'help' works without config" { 128 | source $factorio_script 129 | run run_main "" help 130 | 131 | assert_line --regexp '^Usage: .+ COMMAND$' 132 | assert_line --index 1 'Available commands:' 133 | assert_success 1 134 | } 135 | 136 | @test ".run_main 'listcommands' works without config" { 137 | source $factorio_script 138 | run run_main "" listcommands 139 | 140 | assert_line --index 0 'start' 141 | assert_line --index 19 'help' 142 | assert_success 1 143 | } 144 | 145 | @test ".install fails when FACTORIO_PATH is not empty" { 146 | load 'tmp-helper' 147 | load 'http-mock-helper' 148 | mock_curl_fail 149 | mock_wget_fail 150 | 151 | source $factorio_script 152 | load_config ./config.example 153 | FACTORIO_PATH="`create_tmp_nonempty_dir`" 154 | 155 | run install 156 | assert_output "Aborting install, '${FACTORIO_PATH}' is not empty!" 157 | assert_failure 1 158 | } 159 | 160 | @test ".install fails when FACTORIO_PATH is not writable" { 161 | [ $(id -u) -eq 0 ] && skip "We are running as root, we can not test non-writeability" 162 | load 'tmp-helper' 163 | load 'http-mock-helper' 164 | mock_curl_fail 165 | mock_wget_fail 166 | 167 | source $factorio_script 168 | load_config ./config.example 169 | USERNAME=$(id -un) 170 | USERGROUP=$(id -gn) 171 | FACTORIO_PATH="`create_tmp_empty_dir`" 172 | chmod a-w "${FACTORIO_PATH}" 173 | 174 | run install 175 | assert_output "Aborting install, unable to write to '${FACTORIO_PATH}'!" 176 | assert_failure 1 177 | } 178 | 179 | @test ".install fails on curl error" { 180 | load 'tmp-helper' 181 | load 'http-mock-helper' 182 | source $factorio_script 183 | 184 | mock_curl "${CURL_LATEST_STABLE_HEAD_CURLERR}" 1 185 | mock_wget_fail 186 | 187 | load_config ./config.example 188 | FACTORIO_PATH="`create_tmp_empty_dir`" 189 | DEBUG=1 190 | USERNAME=$(id -un) 191 | USERGROUP=$(id -gn) 192 | 193 | run install 194 | 195 | assert_line "DEBUG: Checking for latest headless version." 196 | assert_line "curl: (X) We ran into curl error X" 197 | assert_line "Aborting install, unable to curl '${LATEST_HEADLESS_URL}'" 198 | assert_failure 1 199 | } 200 | 201 | @test ".install fails on non HTTP 200 status" { 202 | load 'tmp-helper' 203 | load 'http-mock-helper' 204 | source $factorio_script 205 | 206 | mock_curl "${CURL_LATEST_STABLE_HEAD_302_503}" 0 207 | mock_wget_fail 208 | 209 | load_config ./config.example 210 | FACTORIO_PATH="`create_tmp_empty_dir`" 211 | BINARY="${FACTORIO_PATH}/bin/x64/factorio" 212 | INSTALL_CACHE_TAR=1 213 | USERNAME=`whoami` 214 | USERGROUP=`whoami` 215 | DEBUG=1 216 | 217 | run install 218 | 219 | assert_line "Aborting install, expected HTTP 200 from '${LATEST_HEADLESS_URL}', got '503'." 220 | assert_failure 1 221 | } 222 | 223 | @test ".install fails when tarball is missing" { 224 | load 'tmp-helper' 225 | load 'http-mock-helper' 226 | mock_curl_fail 227 | mock_wget_fail 228 | 229 | source $factorio_script 230 | load_config ./config.example 231 | FACTORIO_PATH="`create_tmp_empty_dir`" 232 | 233 | nofile="`create_nonexisting_file tarball`" 234 | run install "${nofile}" 235 | assert_output "Aborting install, '"${nofile}"' does not exist!" 236 | assert_failure 1 237 | } 238 | 239 | @test ".install uses cached tarball" { 240 | [ -z "${FACTORIO_INIT_WITH_TEST_RESOURCES}" ] && skip "We are not running tests with resources" 241 | 242 | load 'tmp-helper' 243 | load 'http-mock-helper' 244 | source $factorio_script 245 | 246 | mock_curl "${CURL_LATEST_STABLE_HEAD_302_200}" 0 247 | mock_wget_fail 248 | 249 | config_file="${BATS_TMPDIR}/config" 250 | cp ./config.example "${config_file}" 251 | if [ -n "${FACTORIO_INIT_ALT_GLIBC}" ]; then 252 | sed -i -e 's/ALT_GLIBC=0/ALT_GLIBC=1/' "${config_file}" 253 | fi 254 | sed -i -e 's#FACTORIO_PATH=/opt/factorio#FACTORIO_PATH="`create_tmp_empty_dir`"#' "${config_file}" 255 | load_config "${config_file}" 256 | 257 | INSTALL_CACHE_TAR=1 258 | USERNAME=$(id -un) 259 | USERGROUP=$(id -gn) 260 | DEBUG=1 261 | 262 | run install 263 | 264 | assert_line "Installation complete, edit '${FACTORIO_PATH}/data/server-settings.json' and start your server." 265 | assert_success 266 | } 267 | 268 | @test ".install uses provided tarball" { 269 | [ -z "${FACTORIO_INIT_WITH_TEST_RESOURCES}" ] && skip "We are not running tests with resources" 270 | load 'tmp-helper' 271 | load 'http-mock-helper' 272 | source $factorio_script 273 | 274 | mock_curl_fail 275 | mock_wget_fail 276 | 277 | config_file="${BATS_TMPDIR}/config" 278 | cp ./config.example "${config_file}" 279 | if [ -n "${FACTORIO_INIT_ALT_GLIBC}" ]; then 280 | sed -i -e 's/ALT_GLIBC=0/ALT_GLIBC=1/' "${config_file}" 281 | fi 282 | sed -i -e 's#FACTORIO_PATH=/opt/factorio#FACTORIO_PATH="`create_tmp_empty_dir`"#' "${config_file}" 283 | load_config "${config_file}" 284 | 285 | DEBUG=1 286 | tarball="/tmp/factorio_headless_x64_1.1.61.tar.xz" 287 | 288 | run install "${tarball}" 289 | 290 | refute_line "DEBUG: Found cached '${tarball}'." 291 | refute_line "DEBUG: No cache hit for '${filename}'." 292 | assert_line "Installation complete, edit '${FACTORIO_PATH}/data/server-settings.json' and start your server." 293 | assert_success 294 | } 295 | 296 | # 297 | #grep -P '(function [^\(]+\(.+)|^ +[^\(\)]+\)$' factorio 298 | # 299 | #function debug() { 300 | #function error() { 301 | #function info() { 302 | #function load_config() { 303 | #function config_defaults() { 304 | # install|help|listcommands|version|"") 305 | # *) 306 | #function usage() { 307 | #function as_user() { 308 | #function is_running() { 309 | #function wait_pingpong() { 310 | #function start_service() { 311 | #function stop_service() { 312 | #function send_cmd(){ 313 | #function cmd_players(){ 314 | #function check_permissions(){ 315 | #function test_deps(){ 316 | #function install(){ 317 | #function get_bin_version(){ 318 | #function get_bin_arch(){ 319 | #function update(){ 320 | #function mod() { 321 | # function mod_usage() { 322 | # update) 323 | # install) 324 | # enable) 325 | # disable) 326 | # remove) 327 | # list) 328 | # help) 329 | # *) 330 | #function run_main(){ 331 | # help|listcommands) 332 | # *) 333 | # start) 334 | # stop) 335 | # restart) 336 | # status) 337 | # cmd) 338 | # log) 339 | # --tail|-t) 340 | # *) 341 | # chatlog) 342 | # --tail|-t) 343 | # *) 344 | # players) 345 | # players-online|online) 346 | # new-game) 347 | # save-game) 348 | # load-save) 349 | # install) 350 | # update) 351 | # inv|invocation) 352 | # help) 353 | # listcommands) 354 | # listsaves) 355 | # version) 356 | # mod) 357 | # *) 358 | -------------------------------------------------------------------------------- /extras/test/http-mock-helper.bash: -------------------------------------------------------------------------------- 1 | 2 | mock_curl() { 3 | cresponse=${1}; shift 4 | cstatus=${1} 5 | 6 | function curl(){ 7 | echo "${cresponse}" 8 | return $cstatus 9 | } 10 | export -f curl 11 | return 0 12 | } 13 | 14 | mock_curl_fail() { 15 | function curl(){ 16 | echo "Calling curl here is a misstake!" 17 | return 1 18 | } 19 | export -f curl 20 | } 21 | 22 | mock_wget() { 23 | wresponse=${1}; shift 24 | wstatus=${1} 25 | 26 | function wget(){ 27 | echo "${wresponse}" 28 | return $wstatus 29 | } 30 | export -f wget 31 | return 0 32 | } 33 | 34 | mock_wget_fail() { 35 | function wget(){ 36 | echo "Calling wget here is a misstake!" 37 | return 1 38 | } 39 | export -f wget 40 | } 41 | 42 | export CURL_LATEST_STABLE_HEAD_302_503="\ 43 | HTTP/1.1 302 FOUND 44 | Connection: keep-alive 45 | Server: gunicorn/20.0.4 46 | Date: Thu, 12 Mar 2020 22:35:20 GMT 47 | Content-Type: text/html; charset=utf-8 48 | Content-Length: 449 49 | Location: https://dcdn.factorio.com/releases/factorio_headless_x64_1.1.61.tar.xz?key=FdR-M6lteBzs3F5Pbrq06A&expires=1584056120 50 | X-Frame-Options: SAMEORIGIN 51 | Strict-Transport-Security: max-age=31536000 52 | Via: 1.1 vegur 53 | 54 | HTTP/1.1 503 Service Temporarily Unavailable 55 | Server: nginx 56 | Date: Thu, 12 Mar 2020 22:35:20 GMT 57 | Content-Type: text/html 58 | Content-Length: 206 59 | Connection: keep-alive 60 | 61 | " 62 | 63 | export CURL_LATEST_STABLE_HEAD_302_200="\ 64 | HTTP/2 302 65 | date: Sun, 21 Aug 2022 20:17:48 GMT 66 | content-type: text/html; charset=utf-8 67 | content-length: 405 68 | location: https://dl.factorio.com/releases/factorio_headless_x64_1.1.61.tar.xz?secure=dqg-NnVSEe465BGdtEkz4A,1661116668 69 | 70 | HTTP/2 200 date: Sun, 21 Aug 2022 20:39:01 GMT content-type: application/octet-stream content-length: 57318572 71 | " 72 | 73 | export CURL_LATEST_STABLE_HEAD_CURLERR="curl: (X) We ran into curl error X" 74 | -------------------------------------------------------------------------------- /extras/test/new-game.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ./test/libs/bats/bin/bats 2 | load 'libs/bats-support/load' 3 | load 'libs/bats-assert/load' 4 | 5 | factorio_script=./factorio 6 | 7 | @test ".run_main new-game SAVENAME MAP_GEN_SETTINGS MAP_SETTINGS creates a new game" { 8 | [ -z "${FACTORIO_INIT_WITH_PRE_INSTALLED_GAME}" ] && skip "We are not running tests with pre-installed game" 9 | source $factorio_script 10 | config_file="${BATS_TMPDIR}/config" 11 | 12 | cp ./config.example "${config_file}" 13 | sed -i -e 's/DEBUG=0/DEBUG=1/' "${config_file}" 14 | if [ -n "${FACTORIO_INIT_ALT_GLIBC}" ]; then 15 | sed -i -e 's/ALT_GLIBC=0/ALT_GLIBC=1/' "${config_file}" 16 | fi 17 | 18 | factorio_path="/opt/factorio" 19 | save_name="new-game-test" 20 | map_gen="map-gen-settings.example.json" 21 | map_settings="map-settings.example.json" 22 | # TODO: make pre-installed game tests run in parallel 23 | run run_main "${config_file}" new-game "${save_name}" "${map_gen}" "${map_settings}" 24 | 25 | assert_output --partial "Program arguments: \"${factorio_path}/bin/x64/factorio\" \"--create\" \"${factorio_path}/bin/x64/../../saves/${save_name}\" \"--map-gen-settings=${factorio_path}/bin/x64/../../data/${map_gen}\"" 26 | assert_line "New game created: ${factorio_path}/bin/x64/../../saves/${save_name}.zip" 27 | assert_success 28 | } 29 | 30 | @test ".run_main new-game SAVENAME MAP_GEN_SETTINGS creates a new game" { 31 | [ -z "${FACTORIO_INIT_WITH_PRE_INSTALLED_GAME}" ] && skip "We are not running tests with pre-installed game" 32 | source $factorio_script 33 | config_file="${BATS_TMPDIR}/config" 34 | 35 | cp ./config.example "${config_file}" 36 | sed -i -e 's/DEBUG=0/DEBUG=1/' "${config_file}" 37 | if [ -n "${FACTORIO_INIT_ALT_GLIBC}" ]; then 38 | sed -i -e 's/ALT_GLIBC=0/ALT_GLIBC=1/' "${config_file}" 39 | fi 40 | 41 | factorio_path="/opt/factorio" 42 | save_name="new-game-test" 43 | map_gen="map-gen-settings.example.json" 44 | # TODO: make pre-installed game tests run in parallel 45 | run run_main "${config_file}" new-game "${save_name}" "${map_gen}" 46 | 47 | assert_output --partial "Program arguments: \"${factorio_path}/bin/x64/factorio\" \"--create\" \"${factorio_path}/bin/x64/../../saves/${save_name}\" \"--map-gen-settings=${factorio_path}/bin/x64/../../data/${map_gen}\"" 48 | assert_line "New game created: ${factorio_path}/bin/x64/../../saves/${save_name}.zip" 49 | assert_success 50 | } 51 | 52 | @test ".run_main new-game SAVENAME creates a new game" { 53 | [ -z "${FACTORIO_INIT_WITH_PRE_INSTALLED_GAME}" ] && skip "We are not running tests with pre-installed game" 54 | source $factorio_script 55 | config_file="${BATS_TMPDIR}/config" 56 | 57 | cp ./config.example "${config_file}" 58 | sed -i -e 's/DEBUG=0/DEBUG=1/' "${config_file}" 59 | if [ -n "${FACTORIO_INIT_ALT_GLIBC}" ]; then 60 | sed -i -e 's/ALT_GLIBC=0/ALT_GLIBC=1/' "${config_file}" 61 | fi 62 | 63 | factorio_path="/opt/factorio" 64 | save_name="new-game-test" 65 | # TODO: make pre-installed game tests run in parallel 66 | run run_main "${config_file}" new-game "${save_name}" 67 | 68 | assert_output --partial "Program arguments: \"${factorio_path}/bin/x64/factorio\" \"--create\" \"${factorio_path}/bin/x64/../../saves/${save_name}\"" 69 | assert_line "New game created: ${factorio_path}/bin/x64/../../saves/${save_name}.zip" 70 | assert_success 71 | } 72 | 73 | -------------------------------------------------------------------------------- /extras/test/tmp-helper.bash: -------------------------------------------------------------------------------- 1 | create_tmp_dir() { 2 | suffix="${1-dir}" 3 | mktemp -d "${BATS_TMPDIR}/factorio-init-${suffix}.XXXXXX" || fail 4 | } 5 | 6 | create_tmp_empty_dir() { 7 | create_tmp_dir "empty" || fail 8 | } 9 | 10 | create_tmp_nonempty_dir() { 11 | dir=`create_tmp_dir "nonempty"` 12 | touch "${dir}/a_file" || fail 13 | echo "${dir}" 14 | } 15 | 16 | create_tmp_file() { 17 | suffix="${1-file}" 18 | mktemp "${BATS_TMPDIR}/factorio-init-${suffix}.XXXXXX" || fail 19 | } 20 | 21 | create_nonexisting_file() { 22 | mktemp -u || fail 23 | } -------------------------------------------------------------------------------- /factorio: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: Factorio 5 | # Required-Start: $local_fs $network $remote_fs 6 | # Required-Stop: $local_fs $network $remote_fs 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: start and stop Factorio server 10 | # Description: A init script for Factorio, try the help command 11 | ### END INIT INFO 12 | 13 | function debug() { 14 | if [ "${DEBUG-0}" -gt 0 ]; then 15 | echo "DEBUG: $*" 16 | fi 17 | } 18 | 19 | function error() { 20 | echo "$*" 1>&2 21 | } 22 | 23 | function info() { 24 | echo "$*" 25 | } 26 | 27 | function load_config() { 28 | # unless a path is provided as the first argument, 29 | # assume there's a "config" file in our current directory. 30 | config_file="${1-./config}"; shift 31 | debug "Trying to load config file '${config_file}'." 32 | 33 | # check that the file exists 34 | [ -f "${config_file}" ] || { error "Config file '${config_file}' does not exist!"; return 1; } 35 | # and we can read it 36 | [ -r "${config_file}" ] || { error "Unable to read config file '${config_file}'!"; return 1; } 37 | # then try to source it 38 | # shellcheck disable=SC1090 39 | source "${config_file}" || { error "Unable to source config file '${config_file}'!"; return 1; } 40 | 41 | config_defaults "$1" 42 | return $? 43 | } 44 | 45 | function config_defaults() { 46 | command=$1 47 | debug "Check/Loading config defaults for command '${command}'" 48 | 49 | ME=$(whoami) 50 | 51 | if [ -z "${SERVICE_NAME}" ]; then 52 | SERVICE_NAME="Factorio" 53 | fi 54 | 55 | if [ -z "${USERGROUP}" ]; then 56 | USERGROUP=${USERNAME} 57 | fi 58 | 59 | if [ -z "${HEADLESS}" ]; then 60 | HEADLESS=1 61 | fi 62 | 63 | if [ -z "${UPDATE_EXPERIMENTAL}" ]; then 64 | UPDATE_EXPERIMENTAL=0 65 | fi 66 | 67 | if [ -z "${LATEST_HEADLESS_URL}" ]; then 68 | if [ "${UPDATE_EXPERIMENTAL}" -gt 0 ]; then 69 | LATEST_HEADLESS_URL="https://www.factorio.com/get-download/latest/headless/linux64" 70 | else 71 | LATEST_HEADLESS_URL="https://www.factorio.com/get-download/stable/headless/linux64" 72 | fi 73 | fi 74 | 75 | if [ -z "${UPDATE_PREVENT_RESTART}" ]; then 76 | UPDATE_PREVENT_RESTART=0 77 | fi 78 | 79 | if [ -z "${UPDATE_PERSIST_TMPDIR}" ]; then 80 | UPDATE_PERSIST_TMPDIR=0 81 | fi 82 | 83 | if [ -z "${NONCMDPATTERN}" ]; then 84 | NONCMDPATTERN='(^\s*(\s*[0-9]+\.[0-9]+|\)|\())|(Players:$)' 85 | fi 86 | 87 | if [ -z "${FACTORIO_PATH}" ]; then 88 | FACTORIO_PATH="/opt/factorio" 89 | fi 90 | 91 | if [ -z "${BINARY}" ]; then 92 | BINARY="${FACTORIO_PATH}/bin/x64/factorio" 93 | fi 94 | 95 | if [ -z "${BINARYB}" ]; then 96 | BINARYB="${BINARY}" 97 | fi 98 | 99 | if [ -z "${ALT_GLIBC}" ]; then 100 | ALT_GLIBC=0 101 | fi 102 | 103 | if [ -z "${WAIT_PINGPONG}" ]; then 104 | WAIT_PINGPONG=0 105 | fi 106 | 107 | if [ -z "${FORCED_SHUTDOWN}" ]; then 108 | FORCED_SHUTDOWN=15 109 | fi 110 | 111 | if [ -z "${ADMINLIST}" ]; then 112 | ADMINLIST="${FACTORIO_PATH}/data/server-adminlist.json" 113 | fi 114 | 115 | if [ "${ALT_GLIBC}" -gt 0 ]; then 116 | if [ -z "${ALT_GLIBC_DIR}" ]; then 117 | ALT_GLIBC_DIR="/opt/glibc-2.18" 118 | fi 119 | 120 | if [ -z "${ALT_GLIBC_VER}" ]; then 121 | ALT_GLIBC_VER="2.18" 122 | fi 123 | 124 | # flip BINARY to include alt glibc 125 | oldbinary="${BINARY}" 126 | BINARY="${ALT_GLIBC_DIR}/lib/ld-${ALT_GLIBC_VER}.so --library-path ${ALT_GLIBC_DIR}/lib ${oldbinary}" 127 | EXE_ARGS_GLIBC="--executable-path ${BINARYB}" 128 | fi 129 | 130 | if [ -z "${FCONF}" ]; then 131 | FCONF="${FACTORIO_PATH}/config/config.ini" 132 | fi 133 | 134 | if [ -z "${SERVER_SETTINGS}" ]; then 135 | SERVER_SETTINGS="${FACTORIO_PATH}/data/server-settings.json" 136 | fi 137 | 138 | if [ -z "${SAVELOG}" ]; then 139 | SAVELOG=0 140 | fi 141 | 142 | if [ -z "${PORT}" ]; then 143 | PORT=34197 144 | fi 145 | 146 | if [ -z "${INSTALL_CACHE_TAR}" ]; then 147 | INSTALL_CACHE_TAR=0 148 | fi 149 | 150 | if [ -z "${INSTALL_CACHE_DIR}" ]; then 151 | INSTALL_CACHE_DIR=/tmp/factorio-install.cache 152 | fi 153 | 154 | # perform some sanity checks in order to properly load the configuration 155 | case $command in 156 | install|help|listcommands|version|"") 157 | debug "Skip check/loading defaults for command '${command}'" 158 | ;; 159 | *) 160 | if ! [ -e "${BINARYB}" ]; then 161 | error "Could not find factorio binary! ${BINARYB}" 162 | error "(if you store your binary some place else, override BINARY='/your/path' in the config)" 163 | return 1 164 | fi 165 | 166 | if ! [ -e "${SERVER_SETTINGS}" ]; then 167 | error "Could not find factorio server settings file: ${SERVER_SETTINGS}" 168 | error "Update your config and point SERVER_SETTINGS to a modified version of data/server-settings.example.json" 169 | return 1 170 | fi 171 | 172 | if ! [ -e "${FCONF}" ]; then 173 | echo "Could not find factorio config file: ${FCONF}" 174 | echo "If this is the first time you run this script you need to generate the config.ini by starting the server manually." 175 | echo "(also make sure you have a save to run or the server will not start)" 176 | echo 177 | echo "Create save: sudo -u ${USERNAME} ${BINARY} --create ${FACTORIO_PATH}/saves/my_savegame ${EXE_ARGS_GLIBC}" 178 | echo "Start server: sudo -u ${USERNAME} ${BINARY} --start-server-load-latest ${EXE_ARGS_GLIBC}" 179 | echo 180 | echo "(If you rather store the config.ini in another location, set FCONF='/your/path' in this scripts config file)" 181 | return 1 182 | fi 183 | 184 | if [ -z "${WRITE_DIR}" ]; then 185 | # figure out the write-data path (where factorio looks for saves and mods) 186 | # Note - this is a hefty little operation, possible cause of head ache down the road 187 | # as it relies on the factorio write dir to live ../../ up from the binary if __PATH__executable__ 188 | # is used in the config file.. for now, that's the default so cross your fingers it will not change ;) 189 | debug "Determining WRITE_DIR based on ${FCONF}, IF you edited write-data from the default, this probably fails" 190 | WRITE_DIR=$(dirname "$(grep "^write-data=" "$FCONF" |cut -d'=' -f2 |sed -e 's#__PATH__executable__#'"$(dirname ${BINARYB})"/..'#g')") 191 | fi 192 | debug "write path: $WRITE_DIR" 193 | 194 | PIDFILE="${WRITE_DIR}/server.pid" 195 | 196 | if [ -z "${FIFO}" ];then 197 | FIFO="${WRITE_DIR}/server.fifo" 198 | fi 199 | 200 | if [ -z "${CMDOUT}" ];then 201 | CMDOUT="${WRITE_DIR}/server.out" 202 | fi 203 | 204 | # Finally, set up the invocation 205 | INVOCATION="${BINARY} --config ${FCONF} --port ${PORT} --start-server-load-latest --server-settings ${SERVER_SETTINGS} --server-adminlist ${ADMINLIST}" 206 | if [ -n "${WHITELIST}" ] && [ -e "${WHITELIST}" ]; then 207 | INVOCATION+=" --server-whitelist ${WHITELIST} --use-server-whitelist" 208 | fi 209 | if [ -n "${BANLIST}" ] && [ -e "${BANLIST}" ]; then 210 | INVOCATION+=" --server-banlist ${BANLIST}" 211 | fi 212 | INVOCATION+=" ${EXTRA_BINARGS}" 213 | ;; 214 | esac 215 | 216 | return 0 217 | } 218 | 219 | function usage() { 220 | echo -e "\ 221 | Usage: $0 COMMAND 222 | 223 | Available commands: 224 | start \t\t\t\t\t\t Starts the server 225 | stop \t\t\t\t\t\t\t Stops the server 226 | restart \t\t\t\t\t\t Restarts the server 227 | status \t\t\t\t\t\t Displays server status 228 | players-online \t\t\t\t\t Shows online players 229 | players \t\t\t\t\t\t Shows all players 230 | cmd [command/message] \t\t\t\t Open interactive commandline or send a single command to the server 231 | log [--tail|-t] \t\t\t\t\t Print the full server log, optionally tail the log to follow in real time 232 | chatlog [--tail|-t] \t\t\t\t\t Print the current chatlog, optionally tail the log to follow in real time 233 | new-game name [map-gen-settings] [map-settings] \t Stops the server and creates a new game with the specified 234 | \t\t\t\t\t\t\t name using the specified map gen settings and map settings json files 235 | save-game name \t\t\t\t\t Stops the server and saves game to specified save 236 | load-save name \t\t\t\t\t Stops the server and loads the specified save 237 | install [tarball] \t\t\t\t\t Installs the server with optional specified tarball 238 | \t\t\t\t\t\t\t (omit to download and use the latest headless server from Wube) 239 | update [--dry-run] \t\t\t\t\t Updates the server 240 | invocation \t\t\t\t\t\t Outputs the invocation for debugging purpose 241 | listcommands \t\t\t\t\t\t List all init-commands 242 | listsaves \t\t\t\t\t\t List all saves 243 | version \t\t\t\t\t\t Prints the binary version 244 | mod \t\t\t\t\t\t\t Manage mods (see $0 mod help for more information) 245 | help \t\t\t\t\t\t\t Shows this help message 246 | " 247 | } 248 | 249 | function as_user() { 250 | debug "as_user: $1" 251 | if [ "$ME" == "$USERNAME" ]; then # Are we the factorio user? 252 | bash -c "$1" 253 | elif [ "$(id -u)" == "0" ]; then # Are we root? 254 | su "$USERNAME" -s /bin/bash -c "$1" 255 | else 256 | # To prevent odd permission behaviour, either 257 | # run this script as the configured user or as root 258 | # (please do not run as root btw!) 259 | echo "Run this script as the $USERNAME user!" 260 | exit 1 261 | fi 262 | } 263 | 264 | function is_running() { 265 | if [ -e "${PIDFILE}" ]; then 266 | if kill -0 "$(cat "${PIDFILE}")" 2> /dev/null; then 267 | debug "${SERVICE_NAME} is running with pid $(cat "${PIDFILE}")" 268 | return 0 269 | else 270 | debug "Found ${PIDFILE}, but the server is not running. It's possible that your server has crashed" 271 | debug "Check the log for details" 272 | rm "${PIDFILE}" 2> /dev/null 273 | return 2 274 | fi 275 | fi 276 | return 1 277 | } 278 | 279 | function wait_pingpong() { 280 | until ping -c1 pingpong1.factorio.com &>/dev/null; do :; done 281 | until ping -c1 pingpong2.factorio.com &>/dev/null; do :; done 282 | } 283 | 284 | function start_service() { 285 | if is_running; then 286 | echo "${SERVICE_NAME} is already running!" 287 | return 1 288 | fi 289 | 290 | if ! check_permissions; then 291 | return $? 292 | fi 293 | 294 | # ensure we have a binary to start 295 | if ! [ -e "${BINARYB}" ]; then 296 | echo "Can't find ${BINARYB}. Please check your config!" 297 | return 1 298 | fi 299 | 300 | # ensure we have a fifo 301 | if ! [ -p "${FIFO}" ]; then 302 | if ! as_user "mkfifo ${FIFO}"; then 303 | echo "Failed to create pipe for stdin, if applicable, remove ${FIFO} and try again" 304 | return 1 305 | fi 306 | fi 307 | 308 | if [ ${SAVELOG} -eq 0 ]; then 309 | debug "Erasing log ${CMDOUT}" 310 | echo "" > "${CMDOUT}" 311 | fi 312 | 313 | if ! [ -e ${ADMINLIST} ]; then 314 | debug "${ADMINLIST} does not exist! Creating empty file." 315 | echo "[]" > ${ADMINLIST} 316 | chown "${USERNAME}:${USERGROUP}" ${ADMINLIST} 317 | fi 318 | 319 | if [ ${WAIT_PINGPONG} -gt 0 ]; then 320 | wait_pingpong 321 | fi 322 | 323 | as_user "tail -f ${FIFO} |${INVOCATION} ${EXE_ARGS_GLIBC}>> ${CMDOUT} 2>&1 & echo \$! > ${PIDFILE}" 324 | 325 | if ps -p "$(cat "${PIDFILE}")" > /dev/null 2>&1; then 326 | echo "Started ${SERVICE_NAME}, please see log for details" 327 | else 328 | as_user "cat ${CMDOUT} |grep -v -P '^$'" 329 | echo -e "\nUnable to start ${SERVICE_NAME}" 330 | return 1 331 | fi 332 | } 333 | 334 | function stop_service() { 335 | if [ -e "${PIDFILE}" ]; then 336 | echo -n "Stopping ${SERVICE_NAME}: " 337 | if kill -TERM "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null; then 338 | sec=1 339 | while [ "$sec" -le "${FORCED_SHUTDOWN}" ]; do 340 | if [ -e "${PIDFILE}" ]; then 341 | if kill -0 "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null; then 342 | echo -n ". " 343 | sleep 1 344 | else 345 | break 346 | fi 347 | else 348 | break 349 | fi 350 | sec=$((sec+1)) 351 | done 352 | fi 353 | 354 | if kill -0 "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null; then 355 | echo "Unable to shut down nicely, killing the process!" 356 | kill -KILL "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null 357 | else 358 | echo "complete!" 359 | fi 360 | 361 | # Open pipe for writing. 362 | exec 3> "${FIFO}" 363 | # Write a newline to the pipe, this triggers a SIGPIPE and causes tail to exit 364 | echo "" >&3 365 | # Close pipe. 366 | exec 3>&- 367 | 368 | rm "${PIDFILE}" 2> /dev/null 369 | return 0 # we've either shut down gracefully or killed the process 370 | else 371 | echo "${SERVICE_NAME} is not running (${PIDFILE} does not exist)" 372 | return 1 373 | fi 374 | } 375 | 376 | function send_cmd(){ 377 | NEED_OUTPUT=0 378 | if [ "$1" == "-o" ]; then 379 | NEED_OUTPUT=1 380 | shift 381 | fi 382 | if is_running; then 383 | if [ -p "${FIFO}" ]; then 384 | # Generate two unique log markers 385 | TIMESTAMP=$(date +"%s") 386 | START="FACTORIO_INIT_CMD_${TIMESTAMP}_START" 387 | END="FACTORIO_INIT_CMD_${TIMESTAMP}_END" 388 | 389 | # Whisper that unknown player to place start marker in log 390 | echo "/w $START" > "${FIFO}" 391 | # Run the actual command 392 | echo "$*" > "${FIFO}" 393 | # Whisper that unknown player again to place end marker in log after the command terminated 394 | echo "/w $END" > "${FIFO}" 395 | 396 | if [ ${NEED_OUTPUT} -eq 1 ]; then 397 | # search for the start marker in the log file, then follow and print the log output in real time until the end marker is found 398 | sleep 1 399 | awk "/Player $START doesn't exist./{flag=1;next}/Player $END doesn't exist./{exit}flag" < "${CMDOUT}" 400 | fi 401 | else 402 | echo "${FIFO} is not a pipe!" 403 | return 1 404 | fi 405 | else 406 | echo "Unable to send cmd to a stopped server!" 407 | return 1 408 | fi 409 | } 410 | 411 | function cmd_players(){ 412 | players=$(send_cmd -o "/p") 413 | if [ -z "${players}" ]; then 414 | echo "No players found!" 415 | return 1 416 | fi 417 | 418 | if [ "$1" == "online" ]; then 419 | echo "${players}" |grep -E '.+ \(online\)$' |sed -e 's/ (online)//g' 420 | else 421 | echo "${players}" 422 | fi 423 | } 424 | 425 | function check_permissions(){ 426 | if ! as_user "test -w ${WRITE_DIR}" ; then 427 | echo "Check Permissions. Cannot write to ${WRITE_DIR}" 428 | return 1 429 | fi 430 | 431 | if ! as_user "touch ${PIDFILE}" ; then 432 | echo "Check Permissions. Cannot touch pidfile ${PIDFILE}" 433 | return 1 434 | fi 435 | 436 | if ! as_user "touch ${CMDOUT}" ; then 437 | echo "Check Permissions. Cannot touch cmd output file ${CMDOUT}" 438 | return 1 439 | fi 440 | } 441 | 442 | function test_deps(){ 443 | return 0 # TODO: Implement ldd check on $BINARY 444 | } 445 | 446 | function install(){ 447 | # Prevent accidential overwrites 448 | if [ -e "${FACTORIO_PATH}" ]; then 449 | if [ -n "$(ls -A ${FACTORIO_PATH} 2>&1)" ]; then 450 | error "Aborting install, '${FACTORIO_PATH}' is not empty!" 451 | return 1 452 | fi 453 | fi 454 | 455 | tarball="$1" 456 | if [ -z "${tarball}" ]; then 457 | downloadlatest=1 458 | elif ! [ -e "${tarball}" ]; then 459 | error "Aborting install, '${tarball}' does not exist!" 460 | return 1 461 | fi 462 | 463 | target="${FACTORIO_PATH}" 464 | if ! as_user "test -w '${target}'"; then 465 | error "Aborting install, unable to write to '${target}'!" 466 | return 1 467 | fi 468 | 469 | if [ "${downloadlatest}" = 1 ]; then 470 | # fetch a http HEAD response, we need it to know what to download later 471 | debug "Checking for latest headless version." 472 | if ! httpresponse="$(curl -LIs "${LATEST_HEADLESS_URL}" 2>&1)"; then 473 | info "${httpresponse}" 474 | error "Aborting install, unable to curl '${LATEST_HEADLESS_URL}'" 475 | return 1 476 | else 477 | httpstatus="$(echo "${httpresponse}" |grep HTTP |tail -n -1 |grep -oP '(?<= )\d{3}(?= )')" 478 | if ! [ "${httpstatus}" = "200" ]; then 479 | info "${httpresponse}" 480 | error "Aborting install, expected HTTP 200 from '${LATEST_HEADLESS_URL}', got '${httpstatus}'." 481 | return 1 482 | fi 483 | fi 484 | 485 | # parse the response 486 | if filename=$(echo "${httpresponse}" |grep -oP '(?<=^location: )[^\?]+' |grep -oP 'factorio_headless.+'); then 487 | debug "Found, latest version: '${filename}'" 488 | else 489 | debug "${httpresponse}" 490 | error "Aborting install, unable to parse version - check the http response!" 491 | return 1 492 | fi 493 | 494 | if [ "${INSTALL_CACHE_TAR}" = 1 ]; then 495 | # we want to cache the tarballs, ensure we have somewhere to save them 496 | if ! [ -e "${INSTALL_CACHE_DIR}" ]; then 497 | if ! as_user "mkdir -p \"${INSTALL_CACHE_DIR}\""; then 498 | error "Aborting install, unable to create cache '${INSTALL_CACHE_DIR}'." 499 | return 1 500 | fi 501 | fi 502 | if ! as_user "test -w \"${INSTALL_CACHE_DIR}\""; then 503 | error "Aborting install, unable to write to cache '${INSTALL_CACHE_DIR}'." 504 | return 1 505 | fi 506 | # we have a usable cache dir, check if there's a hit for our wanted tarball 507 | tarball="${INSTALL_CACHE_DIR}/${filename}" 508 | if [ -f "${tarball}" ]; then 509 | debug "Found cached '${tarball}'." 510 | else 511 | debug "No cache hit for '${filename}'." 512 | tarball= 513 | fi 514 | fi 515 | fi 516 | 517 | if [ -z "${tarball}" ]; then 518 | if [ "${INSTALL_CACHE_TAR}" = 1 ]; then 519 | tarball="${INSTALL_CACHE_DIR}/${filename}" 520 | if ! as_user "wget -O \"${tarball}\" \"${LATEST_HEADLESS_URL}\""; then 521 | error "Aborting install, unable to download & cache '${tarball}'." 522 | return 1 523 | fi 524 | if ! as_user "tar --strip-components 1 -xf \"${tarball}\" -C \"${target}\""; then 525 | error "Aborting install, unable to extract '${tarball}'." 526 | return 1 527 | fi 528 | else 529 | if ! as_user "wget -O - \"${LATEST_HEADLESS_URL}\" |tar --strip-components 1 -xC \"${target}\""; then 530 | error "Aborting install, unable to download and extract tarball." 531 | return 1 532 | fi 533 | fi 534 | else 535 | if ! as_user "tar --strip-components 1 -xf \"${tarball}\" -C \"${target}\""; then 536 | error "Aborting install, unable to extract '${tarball}'." 537 | return 1 538 | fi 539 | fi 540 | 541 | # Generate default config & create a default save-game to play on 542 | debug "EXE_ARGS_GLIBC: ${EXE_ARGS_GLIBC}" 543 | if as_user "${BINARY} --create ${target}/saves/server-save ${EXE_ARGS_GLIBC}"; then 544 | if ! as_user "cp \"${target}/data/server-settings.example.json\" \"${SERVER_SETTINGS}\""; then 545 | error "WARNING! Unable to copy server settings, may need to be resolved manually." 546 | fi 547 | info "Installation complete, edit '${SERVER_SETTINGS}' and start your server." 548 | return 0 549 | else 550 | error "Failed to create save, review the output above to recover" 551 | return 1 552 | fi 553 | } 554 | 555 | function get_bin_version(){ 556 | as_user "$BINARY --version |egrep '^Version: [0-9\.]+' |egrep -o '[0-9\.]+' |head -n 1" 557 | } 558 | 559 | function get_bin_arch(){ 560 | as_user "$BINARY --version |egrep '^Binary version: ' |egrep -o '[0-9]{2}'" 561 | } 562 | 563 | function update(){ 564 | if ! [ -e "${UPDATE_SCRIPT}" ]; then 565 | echo "Failed to find update script, blatantly refusing to continue!" 566 | echo "Try cloning into git@github.com:narc0tiq/factorio-updater.git and set the UPDATE_SCRIPT config before you try again." 567 | return 1 568 | fi 569 | 570 | # Assume the user wants a dry run? (our only argument to this function) 571 | if [ -n "$1" ]; then 572 | echo "Running updater in --dry-run mode, no patches will be applied" 573 | dryrun=1 574 | else 575 | dryrun=0 576 | fi 577 | 578 | if [ ${HEADLESS} -gt 0 ]; then 579 | package="core-linux_headless$(get_bin_arch)" 580 | else 581 | package="core-linux$(get_bin_arch)" 582 | fi 583 | 584 | version=$(get_bin_version) 585 | if [ -z "${UPDATE_TMPDIR}" ]; then 586 | UPDATE_TMPDIR=/tmp 587 | fi 588 | 589 | if [ $UPDATE_PERSIST_TMPDIR -gt 0 ]; then 590 | # check/create tmpdir to ensure updater can download patches here 591 | tmpdir="${UPDATE_TMPDIR}/factorio-update" 592 | debug "Checking/creating update directory (persistant): ${tmpdir}" 593 | if ! [ -e "${tmpdir}" ]; then 594 | if ! as_user "mkdir -p ${tmpdir}"; then 595 | echo "Aborting update! Unable to create tmpdir: ${tmpdir}" 596 | return 1 597 | fi 598 | fi 599 | if ! as_user "test -w '${tmpdir}'"; then 600 | echo "Aborting update! Unable to write to tmpdir: ${tmpdir}" 601 | return 1 602 | fi 603 | else 604 | # Create tmpdir and ensure automatic cleanup 605 | debug "Creating update tmpdir: ${tmpdir}" 606 | if ! tmpdir=$(as_user "mktemp -d -p ${UPDATE_TMPDIR} factorio-update.XXXXXXXXXX"); then 607 | echo "Aborting update! Unable to create tmpdir: ${tmpdir}" 608 | return 1 609 | fi 610 | trap 'rm -rf "${tmpdir}"' EXIT 611 | fi 612 | 613 | invocation="python3 ${UPDATE_SCRIPT} --for-version ${version} --package ${package} --output-path ${tmpdir}" 614 | if [ ${UPDATE_EXPERIMENTAL} -gt 0 ]; then 615 | invocation="${invocation} --experimental" 616 | fi 617 | if [ "${DEBUG}" -gt 0 ]; then 618 | invocation="${invocation} --verbose" 619 | fi 620 | 621 | if [ ${HEADLESS} -eq 0 ]; then 622 | #GoodGuy Wube Software allows you to download the headless for free - yay! but you still have to 623 | #buy the game if you want to download the sound/gfx client 624 | invocation="${invocation} --user ${UPDATE_USERNAME} --token ${UPDATE_TOKEN}" 625 | fi 626 | 627 | echo "Checking for updates..." 628 | result=$(as_user "${invocation} --dry-run") 629 | exitcode=$? 630 | if [ ${exitcode} -eq 1 ] || [ ${exitcode} -gt 2 ]; then 631 | debug "Invocation: ${invocation}" 632 | debug "${result}" 633 | echo "Update check failed!" 634 | return 1 635 | else 636 | newversion=$(echo "${result}" |grep -E '^Dry run: ' |grep -E -o '[0-9\.]+' |tail -n 1) 637 | fi 638 | 639 | if [ -z "${newversion}" ]; then 640 | echo "No new updates for ${package} ${version}" 641 | return 0 642 | else 643 | echo "New version ${package} ${newversion}" 644 | fi 645 | 646 | # Go or no Go? 647 | if [ ${dryrun} -gt 0 ]; then 648 | debug "This is a dry-run, not taking any further actions." 649 | # allow scripts to read return code 0 for no updates and 2 if there are updates to apply 650 | if [ -n "${newversion}" ]; then 651 | return 2 652 | fi 653 | return 0 654 | fi 655 | 656 | # Prevent update restart, ie require the server to be stopped before we apply updates 657 | if [ $UPDATE_PREVENT_RESTART -gt 0 ]; then 658 | debug "Preventing update restarts ..." 659 | if is_running; then 660 | echo "Factorio is running, aborting update - stop the server and re-run the update command!" 661 | return 3 662 | else 663 | debug "Server wasn't running, continue with the update." 664 | fi 665 | else 666 | debug "Will not prevent update restarts." 667 | fi 668 | 669 | # Time to download the updates 670 | if ! as_user "${invocation}"; then 671 | echo "Aborting update!" 672 | return 1 673 | fi 674 | 675 | # Stop the server if it is running. 676 | is_running 677 | was_running=$? 678 | if [ ${was_running} -eq 0 ]; then 679 | send_cmd "Updating to new Factorio version, be right back" 680 | stop_service 681 | if is_running; then 682 | echo "Aborting update! The server is still running." 683 | return 1 684 | fi 685 | fi 686 | 687 | for patch in $(find "${tmpdir}" -type f -name "*.zip" | sort -V); do 688 | echo "Applying ${patch} ..." 689 | result=$(as_user "$BINARY --apply-update ${patch} ${EXE_ARGS_GLIBC}") 690 | exitcode=$? 691 | if [ $exitcode -gt 0 ]; then 692 | echo "${result}" 693 | echo 694 | echo "Error! Failed to apply update" 695 | if [ $UPDATE_PERSIST_TMPDIR -gt 0 ]; then 696 | echo "You can try to apply it manually with:" 697 | echo "su ${USERNAME} -c \"${BINARY} --apply-update ${patch} ${EXE_ARGS_GLIBC}\"" 698 | fi 699 | return 1 700 | fi 701 | done 702 | 703 | # Restarts the server if it was running 704 | if [ ${was_running} -eq 0 ]; then 705 | start_service 706 | fi 707 | 708 | echo "Successfully updated factorio" 709 | return 0 710 | } 711 | 712 | function mod() { 713 | function mod_usage() { 714 | cat < " cmd 868 | [ "${cmd}" == "exit" ] && return 0 869 | [ "${cmd}" == "clear" ] && clear && continue 870 | send_cmd -o "${cmd}" 871 | sleep 1 872 | done 873 | else 874 | send_cmd "${@:2}" 875 | fi 876 | ;; 877 | chatlog) 878 | case $2 in 879 | --tail|-t) 880 | tail -F -n +0 "${CMDOUT}" |grep -E -v "${NONCMDPATTERN}" 881 | ;; 882 | *) 883 | grep -E -v "${NONCMDPATTERN}" "${CMDOUT}" 884 | ;; 885 | esac 886 | ;; 887 | log) 888 | case $2 in 889 | --tail|-t) 890 | tail -F -n +0 "${CMDOUT}" 891 | ;; 892 | *) 893 | cat "${CMDOUT}" 894 | ;; 895 | esac 896 | ;; 897 | players) 898 | cmd_players 899 | ;; 900 | players-online|online) 901 | cmd_players online 902 | ;; 903 | new-game) 904 | if [ -z "$2" ]; then 905 | echo "You must specify a save name for your new game" 906 | return 1 907 | fi 908 | savename="${WRITE_DIR}/saves/$2" 909 | createsavecmd="$BINARY --create \"${savename}\"" 910 | 911 | # Check if user wants to use custom map-gen-settings 912 | if [ -n "$3" ]; then 913 | if [ -e "${WRITE_DIR}/data/$3" ]; then 914 | createsavecmd="$createsavecmd --map-gen-settings=${WRITE_DIR}/data/$3" 915 | else 916 | echo "Specified map-gen-settings json file does not exist in server's /data directory" 917 | return 1 918 | fi 919 | fi 920 | 921 | # Check if user wants to use custom map-settings 922 | if [ -n "$4" ]; then 923 | if [ -e "${WRITE_DIR}/data/$4" ]; then 924 | createsavecmd="$createsavecmd --map-settings=${WRITE_DIR}/data/$4" 925 | else 926 | echo "Specified map-settings json file does not exist in server's /data directory" 927 | return 1 928 | fi 929 | fi 930 | 931 | if ! as_user "$createsavecmd ${EXE_ARGS_GLIBC}"; then 932 | echo "Failed to create new game" 933 | return 1 934 | else 935 | echo "New game created: ${savename}.zip" 936 | fi 937 | ;; 938 | 939 | save-game) 940 | savename="${WRITE_DIR}/saves/$2.zip" 941 | 942 | # Stop Service 943 | if is_running; then 944 | send_cmd "Stopping server to save game" 945 | if ! stop_service; then 946 | echo "Failed to stop server, unable to save as \"$2\"" 947 | return 1 948 | fi 949 | fi 950 | 951 | lastsave=$(find "${WRITE_DIR}/saves" -type f -printf '%T@ %p\n' | sort -n | tail -1 | cut -f2- -d" ") 952 | if ! as_user "cp \"${lastsave}\" \"${savename}\""; then 953 | echo "Error! Failed to save game" 954 | return 1 955 | fi 956 | ;; 957 | 958 | load-save) 959 | # Ensure we get a new save file name 960 | newsave=${WRITE_DIR}/saves/$2.zip 961 | if [ ! -f "${newsave}" ]; then 962 | echo "Save \"${newsave}\" does not exist, aborting action!" 963 | return 1 964 | fi 965 | 966 | # Since stopping the server causes a save we have to stop the server to do this 967 | if is_running; then 968 | send_cmd "Stopping server to load a saved game" 969 | if ! stop_service; then 970 | echo "Aborting, unable to stop $SERVICE_NAME" 971 | return 1 972 | fi 973 | fi 974 | 975 | # Touch the new save file 976 | as_user "touch \"${newsave}\"" 977 | ;; 978 | install) 979 | install "$2" 980 | return $? 981 | ;; 982 | update) 983 | update "$2" 984 | return $? 985 | ;; 986 | inv|invocation) 987 | echo "${INVOCATION}" 988 | ;; 989 | help) 990 | usage 991 | ;; 992 | listcommands) 993 | usage |grep -oP '(?<= )\w+' 994 | ;; 995 | listsaves) 996 | find "${WRITE_DIR}"/saves -type f -name "*.zip" -exec basename {} \; |sed -e 's/.zip//' 997 | ;; 998 | version) 999 | get_bin_version 1000 | ;; 1001 | mod) 1002 | shift 1003 | mod "$@" 1004 | ;; 1005 | *) 1006 | echo "No such command!" 1007 | echo 1008 | usage 1009 | return 1 1010 | ;; 1011 | esac 1012 | } 1013 | 1014 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then 1015 | if [ -L "${0}" ]; then 1016 | config_file=$(readlink -e "$0" | sed "s:[^/]*$:config:") 1017 | else 1018 | # shellcheck disable=SC2001 1019 | config_file=$(echo "$0" | sed "s:[^/]*$:config:") 1020 | fi 1021 | 1022 | run_main "$config_file" "$@" 1023 | exit $? 1024 | fi 1025 | --------------------------------------------------------------------------------