Repository: kaimallea/csgo Branch: master Commit: 136572263fc0 Files: 19 Total size: 51.2 KB Directory structure: gitextract_deglkiou/ ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cloud-config.yaml ├── containerfs/ │ ├── README.md │ ├── manage_plugins.sh │ ├── manage_pugsetup_configs.sh │ └── start.sh ├── docker-compose.yaml └── test/ ├── bin/ │ └── bats ├── csgo/ │ └── cfg/ │ └── sourcemod/ │ └── pugsetup/ │ └── pugsetup.cfg ├── libexec/ │ └── bats-core/ │ ├── bats │ ├── bats-exec-suite │ ├── bats-exec-test │ ├── bats-format-tap-stream │ └── bats-preprocess ├── srcds_run └── tests.bats ================================================ FILE CONTENTS ================================================ ================================================ FILE: Dockerfile ================================================ FROM ubuntu:bionic ENV TERM xterm ENV STEAM_DIR /home/steam ENV STEAMCMD_DIR /home/steam/steamcmd ENV CSGO_APP_ID 740 ENV CSGO_DIR /home/steam/csgo SHELL ["/bin/bash", "-c"] ARG STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz RUN set -xo pipefail \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --no-install-suggests -y \ lib32gcc1 \ lib32stdc++6 \ lib32z1 \ ca-certificates \ net-tools \ locales \ curl \ unzip \ && locale-gen en_US.UTF-8 \ && adduser --disabled-password --gecos "" steam \ && mkdir ${STEAMCMD_DIR} \ && cd ${STEAMCMD_DIR} \ && curl -sSL ${STEAMCMD_URL} | tar -zx -C ${STEAMCMD_DIR} \ && mkdir -p ${STEAM_DIR}/.steam/sdk32 \ && ln -s ${STEAMCMD_DIR}/linux32/steamclient.so ${STEAM_DIR}/.steam/sdk32/steamclient.so \ && { \ echo '@ShutdownOnFailedCommand 1'; \ echo '@NoPromptForPassword 1'; \ echo 'login anonymous'; \ echo 'force_install_dir ${CSGO_DIR}'; \ echo 'app_update ${CSGO_APP_ID}'; \ echo 'quit'; \ } > ${STEAM_DIR}/autoupdate_script.txt \ && mkdir ${CSGO_DIR} \ && chown -R steam:steam ${STEAM_DIR} \ && rm -rf /var/lib/apt/lists/* ENV LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ LC_ALL=en_US.UTF-8 COPY --chown=steam:steam containerfs ${STEAM_DIR}/ USER steam WORKDIR ${CSGO_DIR} VOLUME ${CSGO_DIR} ENTRYPOINT exec ${STEAM_DIR}/start.sh ================================================ FILE: LICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: Makefile ================================================ SHELL := /bin/bash CONTAINER_NAME ?= csgo-dedicated-server IMAGE_NAME ?= kmallea/csgo:latest SERVER_HOSTNAME ?= Counter-Strike: Global Offensive Dedicated Server SERVER_PASSWORD ?= RCON_PASSWORD ?= changeme STEAM_ACCOUNT ?= changeme AUTHKEY ?= changeme IP ?= 0.0.0.0 PORT ?= 27015 TV_PORT ?= 27020 TICKRATE ?= 128 FPS_MAX ?= 400 GAME_TYPE ?= 0 GAME_MODE ?= 1 MAP ?= de_dust2 MAPGROUP ?= mg_active HOST_WORKSHOP_COLLECTION ?= WORKSHOP_START_MAP ?= MAXPLAYERS ?= 12 TV_ENABLE ?= 1 LAN ?= 1 SOURCEMOD_ADMINS ?= STEAM_1:0:123456,STEAM_1:0:654321 RETAKES ?= 0 NOMASTER ?= 0 .PHONY: all clean image test stop all: image clean: docker rmi $(IMAGE_NAME) image: Dockerfile docker build -t $(IMAGE_NAME) \ --build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \ . server: docker run \ -i \ -t \ -d \ --net=host \ --mount source=csgo-data,target=/home/steam/csgo \ -e "SERVER_HOSTNAME=$(SERVER_HOSTNAME)" \ -e "SERVER_PASSWORD=$(SERVER_PASSWORD)" \ -e "RCON_PASSWORD=$(RCON_PASSWORD)" \ -e "STEAM_ACCOUNT=$(STEAM_ACCOUNT)" \ -e "AUTHKEY=$(AUTHKEY)" \ -e "TICKRATE=$(TICKRATE)" \ -e "FPS_MAX=$(FPS_MAX)" \ -e "GAME_TYPE=$(GAME_TYPE)" \ -e "GAME_MODE=$(GAME_MODE)" \ -e "MAP=$(MAP)" \ -e "MAPGROUP=$(MAPGROUP)" \ -e "HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)" \ -e "WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)" \ -e "MAXPLAYERS=$(MAXPLAYERS)" \ -e "TV_ENABLE=$(TV_ENABLE)" \ -e "LAN=$(LAN)" \ -e "SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)" \ -e "RETAKES=$(RETAKES)" \ --name $(CONTAINER_NAME) \ $(IMAGE_NAME) test: docker run \ -i \ -t \ --rm \ --net=host \ --mount type=bind,source="$(PWD)/test",target=/home/steam/csgo \ -e "CI=true" \ -e "SERVER_HOSTNAME=$(SERVER_HOSTNAME)" \ -e "SERVER_PASSWORD=$(SERVER_PASSWORD)" \ -e "RCON_PASSWORD=$(RCON_PASSWORD)" \ -e "STEAM_ACCOUNT=$(STEAM_ACCOUNT)" \ -e "AUTHKEY=$(AUTHKEY)" \ -e "TICKRATE=$(TICKRATE)" \ -e "FPS_MAX=$(FPS_MAX)" \ -e "GAME_TYPE=$(GAME_TYPE)" \ -e "GAME_MODE=$(GAME_MODE)" \ -e "MAP=$(MAP)" \ -e "MAPGROUP=$(MAPGROUP)" \ -e "HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)" \ -e "WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)" \ -e "MAXPLAYERS=$(MAXPLAYERS)" \ -e "TV_ENABLE=$(TV_ENABLE)" \ -e "LAN=$(LAN)" \ -e "SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)" \ -e "RETAKES=$(RETAKES)" \ -e "SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2" \ --name $(CONTAINER_NAME) \ $(IMAGE_NAME) stop: docker stop $(CONTAINER_NAME) docker rm $(CONTAINER_NAME) ================================================ FILE: README.md ================================================ # CSGO containerized The Dockerfile will build an image for running a Counter-Strike: Global Offensive dedicated server in a container. The following addons and plugins are included by default: - [Metamod](https://www.sourcemm.net/) - [SourceMod](https://www.sourcemod.net/) - [SteamWorks](https://forums.alliedmods.net/showthread.php?t=229556) - [Updater](https://bitbucket.org/GoD_Tony/updater/downloads/updater.smx) - [PugSetup](https://github.com/splewis/csgo-pug-setup) - [Practice Mode](https://github.com/splewis/csgo-practice-mode) - [Retakes](https://github.com/splewis/csgo-retakes) (**disabled by default**) To get a 10man/gather going, simply connect and type `.setup` in chat. Practice Mode should also be available from the menu. Retakes is disabled by default. To enable it, set the environment variable `RETAKES=1` and restart the container. Use can later use the cvar `sm_retakes_enabled 0` to turn if off on-demand. ## How to Use ```bash docker pull kmallea/csgo:latest ``` To use the image as-is, run it with a few useful environment variables to configure the server: ```bash docker run \ --rm \ --interactive \ --tty \ --detach \ --mount source=csgo-data,target=/home/steam/csgo \ --network=host \ --env "SERVER_HOSTNAME=hostname" \ --env "SERVER_PASSWORD=password" \ --env "RCON_PASSWORD=rconpassword" \ --env "STEAM_ACCOUNT=gamelogintoken" \ --env "AUTHKEY=webapikey" \ --env "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321" \ kmallea/csgo ``` Would you rather use a bind volume so that you can access file contents directly? Use `--mount type=bind,source=$(pwd),target=/home/steam/csgo` instead of the one in the example above. If you plan on managing plugins manually with a bind volume, you might want pass an empty or reduced `INSTALL_PLUGINS` environment variable to prevent conflicts (see below for default value of `INSTALL_PLUGINS`). ### Required Game Login Token The `STEAM_ACCOUNT` is a "Game Login Token" required by Valve to run public servers. Confusingly, this token is also referred to as a steam account (it's set via `sv_setsteamaccount`). To get one, visit https://steamcommunity.com/dev/managegameservers. You'll need one for each server. Remember that if you DO NOT give a valid Game Login Token, your server will be restricted to LAN only ### Optional Steam Web API Key for Workshop Content To access maps and collections from the Workshop, you need to provide a Steam Web API key. You can provide this via the evironment variable `AUTHKEY` and it will be passed to the command-line as `-authkey `. If you don't have a key you can generate one at http://steamcommunity.com/dev/apikey. With a key set, you can also use the environment variables `HOST_WORKSHOP_COLLECTION` and `WORKSHOP_START_MAP` to specify a workshop collection and start the server with a workshop map, respectively. For more information check out the [Valve developer wiki page](https://developer.valvesoftware.com/wiki/CSGO_Workshop_For_Server_Operators#How_to_host_Workshop_Maps_with_a_CS:GO_Dedicated_Server). ### SourceMod admins The optional `SOURCEMOD_ADMINS` environment variable is a comma-delimited list of Steam IDs. These will be added to SourceMod's admin list before the server is started. ### Playing on LAN If you're on a LAN, add the environment variable `LAN=1` (e.g., `--env "LAN=1"`) to have `sv_lan 1` set for you in the server. ### Environment variable overrides Below are the default values for environment variables that control the server configuration. To override, pass one or more of these to docker using the `-e` or `--env` argument (example above). ```bash SERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server SERVER_PASSWORD= RCON_PASSWORD=changeme STEAM_ACCOUNT=changeme AUTHKEY=changeme IP=0.0.0.0 PORT=27015 TV_PORT=27020 TICKRATE=128 FPS_MAX=400 GAME_TYPE=0 GAME_MODE=1 MAP=de_dust2 MAPGROUP=mg_active HOST_WORKSHOP_COLLECTION= WORKSHOP_START_MAP= MAXPLAYERS=12 TV_ENABLE=1 LAN=0 SOURCEMOD_ADMINS= RETAKES=0 NOMASTER=0 ``` For compatibility with the [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) feature the following environment variables are also available as a '_FILE' variant. ```bash SERVER_PASSWORD_FILE RCON_PASSWORD_FILE STEAM_ACCOUNT_FILE AUTHKEY_FILE SOURCEMOD_ADMINS_FILE ``` If one of these is set the content of the referred file is used as content for the non-'_FILE" environment variable. If both environment variables are set, the content of the non-'_FILE' variable takes precedence. Usage of _FILE variables allows constructs like this in docker compose files: ```yml version: "3.7" services: app: image: kmallea/csgo secrets: - csgo_rcon_password environment: - RCON_PASSWORD_FILE=/run/secrets/csgo_rcon_password secrets: csgo_rcon_password: file: ${SECRETS_DIR}/csgo_rcon_password.txt ``` ### PugSetup ConVars PugSetup's default configuration can also be controlled via environment variables. Any environment variables prefixed with `SM_PUGSETUP_` will have its corresponding cvar updated inside of `$CSGODIR/csgo/cfg/sourcemod/pugsetup.cfg`. **NOTE: `pugsetup.cfg` is automatically generated the first time the plugin is loaded. So you may have to restart the container after the first run so that the file exists.** For example, if I wanted to enable set the cvars `sm_pugsetup_snake_captain_picks` and `sm_pugsetup_message_prefix`, I would set the following environment variables when starting the container: ```bash ... --env "SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2" \ --env "SM_PUGSETUP_MESSAGE_PREFIX=[{YELLOW}Sesame Street{NORMAL}]" \ ... ``` This would set these values in `$CSGODIR/csgo/cfg/sourcemod/pugsetup.cfg`: ```bash ... sm_pugsetup_snake_captain_picks "2" sm_pugsetup_message_prefix "[{YELLOW}Sesame Street{NORMAL}]" ... ``` ### Troubleshooting If you're unable to use [`--network=host`](https://docs.docker.com/network/host/), you'll need to publsh the ports instead, e.g.: ```bash docker run \ --rm \ --interactive \ --tty \ --detach \ --mount source=csgo-data,target=/home/steam/csgo \ --publish 27015:27015/tcp \ --publish 27015:27015/udp \ --publish 27020:27020/tcp \ --publish 27020:27020/udp \ --env "SERVER_HOSTNAME=hostname" \ --env "SERVER_PASSWORD=password" \ --env "RCON_PASSWORD=rconpassword" \ --env "STEAM_ACCOUNT=gamelogintoken" \ --env "AUTHKEY=webapikey" \ --env "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321" \ kmallea/csgo ``` ## Manually Building ```bash docker build -t csgo-dedicated-server . ``` _OR_ ```bash make ``` The game data is downloaded on first run (~26GB). Mount a volume to preserve game data if you need to recreate the container. The volume's target should be `/home/steam/csgo`. In these example I use a data volume, but you can use a bind volume as well since plugins are installed during container startup. ### Overriding versions of SteamCMD, Metamod, SourceMod, and/or PugSetup #### SteamCMD SteamCMD is installed directly into the image at build time. To override the URL it installs from, pass in a build arg named `STEAMCMD_URL`: ```bash docker build \ -t $(IMAGE_NAME) \ --build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \ . ``` #### Metamod, SourceMod, PugSetup, Retakes, etc All plugins and extensions are installed during the startup of the container. This allows plugins can be managed via an environment variable. The environment variable `INSTALL_PLUGINS` contains a space-delimited list of plugins to install. You can use newlines to delimit, they will be converted to spaces before processing. If you override this, make sure you include metamod and sourcemod or plugins that depend on them won't work. ```bash INSTALL_PLUGINS="${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.10/mmsource-1.10.7-git971-linux.tar.gz https://sm.alliedmods.net/smdrop/1.10/sourcemod-1.10.0-git6478-linux.tar.gz https://github.com/splewis/csgo-pug-setup/releases/download/2.0.5/pugsetup_2.0.5.zip https://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip https://github.com/b3none/retakes-instadefuse/releases/download/1.4.0/retakes-instadefuse.smx https://github.com/b3none/retakes-autoplant/releases/download/2.3.0/retakes_autoplant.smx https://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx }" ``` Lastly, a checksum is generated for each plugin's URL and is stored as `$CSGO_DIR/csgo/.marker` to prevent re-downloading plugins that have already been installed. ### Adding your own configs, other files etc. #### Build time The directory `containerfs` (container filesystem) is the equivalent of the steam user's home directory (`/home/steam`). The `csgo` game data lives in here. This means that any files you want to add, simply put them in the correct paths under `containerfs`, and they will appear in the Docker image relative to the steam user's home directory. It is recommended to use `INSTALL_PLUGINS` environment variable at run time to install plugins, so that they are decoupled from the image. #### Run time See `INSTALL_PLUGINS` above in the section above to learn about installing plugins. If you're using a data volume, you can use the `docker cp` command to copy files from your host machine into the data volume. If you're using a bind volume, you can copy files in directly. You may want to clear the `INSTALL_PLUGINS` variable if you want to manage everything manually. ### Test Locally After building: 1. Edit the exported environment variables in the `Makefile` to your liking 2. Run `make server` to start a local LAN server to test 3. Run `make test` to run tests ================================================ FILE: cloud-config.yaml ================================================ #cloud-config # This config is an example of of a one-liner to getting a CSGO server up-and-running # in just a few minutes on Google Compute Engine with the following one-liner that consumes # the uncommented parts of this file: # # gcloud compute instances create csgo-server \ # --project=$PROJECT \ # --zone=$ZONE \ # --image-family=cos-stable \ # --image-project=cos-cloud \ # --boot-disk-size=50GB \ # --machine-type=c2-standard-4 \ # --network=default \ # --metadata-from-file user-data=$(PWD)/cloud-config.yaml write_files: - path: /etc/systemd/system/dynamic-dns.service permissions: 0644 owner: root content: | # /etc/systemd/system/dynamic-dns.service [Unit] Description=Updates dynamic DNS record Wants=dynamic-dns.timer [Service] ExecStart=/bin/sh -c '(\ export PUBLIC_IP=$$(\ /usr/bin/curl \ -s \ -H "Metadata-Flavor: Google" \ https://domains.google.com/checkip \ ) && \ /usr/bin/curl \ -s \ --user : \ "https://domains.google.com/nic/update?hostname=&myip=$${PUBLIC_IP}" \ )' - path: /etc/systemd/system/dynamic-dns.timer permissions: 0644 owner: root content: | # /etc/systemd/system/dynamic-dns.timer [Unit] Description=Runs dynamic-dns.service every 15 minutes Requires=dynamic-dns.timer [Timer] Unit=dynamic-dns.service OnUnitInactiveSec=15m - path: /etc/systemd/system/csgods.service permissions: 0644 owner: root content: | [Unit] Description=CSGO Dedicated Server Container After=docker.service Requires=docker.service [Service] StandardInput=tty-force ExecStartPre=/usr/bin/docker pull kmallea/csgo ExecStart=/usr/bin/docker run --name %n \ --interactive \ --tty \ --rm \ --network host \ --cpuset-cpus 3 \ --mount source=csgo-data,target=/home/steam/csgo \ -e "SERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server" \ -e "SERVER_PASSWORD=" \ -e "RCON_PASSWORD=changeme" \ -e "STEAM_ACCOUNT=changeme" \ -e "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:1:654321" \ -e "AUTHKEY=changeme" \ -e "FPS_MAX=1000" \ kmallea/csgo ExecStop=-/usr/bin/docker stop %n ExecStopPost=-/usr/bin/docker rm %n runcmd: - iptables -w -A INPUT -p tcp --dport 27015 -j ACCEPT - iptables -w -A INPUT -p udp --dport 27015 -j ACCEPT - iptables -w -A INPUT -p tcp --dport 27020 -j ACCEPT - iptables -w -A INPUT -p udp --dport 27020 -j ACCEPT - iptables -w -A INPUT -p udp --dport 27005 -j ACCEPT - iptables -w -A INPUT -p udp --dport 51840 -j ACCEPT - iptables -w -A INPUT -p tcp --dport 26900 -j ACCEPT - iptables -w -A INPUT -p tcp --dport 80 -j ACCEPT - iptables -w -A INPUT -p tcp --dport 443 -j ACCEPT - systemctl daemon-reload - systemctl enable dynamic-dns.timer - systemctl start dynamic-dns.service - systemctl start csgods.service ================================================ FILE: containerfs/README.md ================================================ ## Adding your own files, plugins, etc. The directory `containerfs` (container filesystem) is the equivalent of the root CSGO directory (`/home/steam/csgo`). Any files or plugins you want to add to the image, simply put them in the correct paths under `containerfs`, and they will appear in the Docker image relative to the CSGO directory. For example, by default, CSGO is installed in the root path `/home/steam/csgo` within the docker image. If I want my `practice.cfg` file to live in the `cfg` directory, I would put that file in `containerfs/csgo/cfg/` and it will appear in the right place inside the docker image: `home/steam/csgo/csgo/cfg/practice.cfg` (Yes, `csgo` appears twice in the path because the CSGO installation has a sub-directory named `csgo`). ================================================ FILE: containerfs/manage_plugins.sh ================================================ #!/usr/bin/env bash set -ueo pipefail : "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}" export RETAKES="${RETAKES:-0}" INSTALL_PLUGINS="${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.11/mmsource-1.11.0-git1148-linux.tar.gz https://sm.alliedmods.net/smdrop/1.11/sourcemod-1.11.0-git6934-linux.tar.gz http://users.alliedmods.net/~kyles/builds/SteamWorks/SteamWorks-git131-linux.tar.gz https://bitbucket.org/GoD_Tony/updater/downloads/updater.smx https://github.com/splewis/csgo-practice-mode/releases/download/1.3.4/practicemode_1.3.4.zip https://github.com/splewis/csgo-pug-setup/releases/download/2.0.7/pugsetup_2.0.7.zip https://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip https://github.com/B3none/retakes-instadefuse/releases/download/1.5.0/retakes-instadefuse.smx https://github.com/B3none/retakes-autoplant/releases/download/2.3.3/retakes-autoplant.smx https://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx }" get_checksum_from_string () { local md5 md5=$(echo -n "$1" | md5sum | awk '{print $1}') echo "$md5" } is_plugin_installed() { local url_hash url_hash=$(get_checksum_from_string "$1") if [[ -f "$CSGO_DIR/csgo/${url_hash}.marker" ]]; then return 0 else return 1 fi } create_install_marker() { echo "$1" > "$CSGO_DIR/csgo/$(get_checksum_from_string "$1").marker" } file_url_exists() { if curl --output /dev/null --silent --head --fail "$1"; then return 0 fi return 1 } install_plugin() { filename=${1##*/} filename_ext=$(echo "${1##*.}" | awk '{print tolower($0)}') if ! file_url_exists "$1"; then echo "Plugin download check FAILED for $filename"; return 0 fi if ! is_plugin_installed "$1"; then echo "Downloading $1..." case "$filename_ext" in "gz") curl -sSL "$1" | tar -zx -C "$CSGO_DIR/csgo" echo "Extracting $filename..." create_install_marker "$1" ;; "zip") curl -sSL -o "$filename" "$1" echo "Extracting $filename..." unzip -oq "$filename" -d "$CSGO_DIR/csgo" rm "$filename" create_install_marker "$1" ;; "smx") (cd "$CSGO_DIR/csgo/addons/sourcemod/plugins/" && curl -sSLO "$1") create_install_marker "$1" ;; *) echo "Plugin $filename has an unknown file extension, skipping" ;; esac else echo "Plugin $filename is already installed, skipping" fi } echo "Installing plugins..." mkdir -p "$CSGO_DIR/csgo" IFS=' ' read -ra PLUGIN_URLS <<< "$(echo "$INSTALL_PLUGINS" | tr "\n" " ")" for URL in "${PLUGIN_URLS[@]}"; do install_plugin "$URL" done echo "Finished installing plugins." # Add steam ids to sourcemod admin file mkdir -p "$CSGO_DIR/csgo/addons/sourcemod/configs" IFS=',' read -ra STEAMIDS <<< "$SOURCEMOD_ADMINS" for id in "${STEAMIDS[@]}"; do echo "\"$id\" \"99:z\"" >> "$CSGO_DIR/csgo/addons/sourcemod/configs/admins_simple.ini" done PLUGINS_ENABLED_DIR="$CSGO_DIR/csgo/addons/sourcemod/plugins" PLUGINS_DISABLED_DIR="$CSGO_DIR/csgo/addons/sourcemod/plugins/disabled" RETAKES_PLUGINS="retakes.smx retakes-instadefuse.smx retakes-autoplant.smx retakes-hud.smx retakes_standardallocator.smx" PUGSETUP_PLUGINS="pugsetup.smx pugsetup_teamnames.smx pugsetup_damageprint.smx" # Disable Retakes by default so that we have a working and predictable state without plugins conflict if [[ -f "$PLUGINS_ENABLED_DIR"/retakes.smx ]]; then mv "$PLUGINS_ENABLED_DIR"/retakes*.smx "$PLUGINS_DISABLED_DIR"/ fi if [ "$RETAKES" = "1" ]; then if [[ -f "$PLUGINS_ENABLED_DIR"/pugsetup.smx ]]; then (cd "$PLUGINS_ENABLED_DIR" && mv pugsetup*.smx "$PLUGINS_DISABLED_DIR") echo "Disabled PugSetup plugins" fi # shellcheck disable=SC2086 (cd "$PLUGINS_DISABLED_DIR" && mv $RETAKES_PLUGINS "$PLUGINS_ENABLED_DIR") echo "Enabled Retakes plugins" else if [[ -f "$PLUGINS_DISABLED_DIR"/pugsetup.smx ]]; then # shellcheck disable=SC2086 (cd "$PLUGINS_DISABLED_DIR" && mv $PUGSETUP_PLUGINS "$PLUGINS_ENABLED_DIR") echo "Enabled PugSetup plugins" fi fi ================================================ FILE: containerfs/manage_pugsetup_configs.sh ================================================ #!/usr/bin/env bash set -ueo pipefail : "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}" PUGSETUP_CONFIG="$CSGO_DIR/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg" if [[ -f "$PUGSETUP_CONFIG" ]]; then # Update PugSetup cvars specified as envvars. # e.g., `SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2` will set sm_pugsetup_snake_captain_picks "2" inside of $PUGSETUP_CONFIG for var in "${!SM_PUGSETUP_@}"; do cvar=$(echo "$var" | tr '[:upper:]' '[:lower:]') value=${!var} sed -i "s/$cvar \"[^\]*\"/$cvar \"$value\"/g" "$PUGSETUP_CONFIG" done fi ================================================ FILE: containerfs/start.sh ================================================ #!/usr/bin/env bash # These envvars should've been set by the Dockerfile # If they're not set then something went wrong during the build : "${STEAM_DIR:?'ERROR: STEAM_DIR IS NOT SET!'}" : "${STEAMCMD_DIR:?'ERROR: STEAMCMD_DIR IS NOT SET!'}" : "${CSGO_APP_ID:?'ERROR: CSGO_APP_ID IS NOT SET!'}" : "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}" # set_env_from_file_or_def VAR [DEFAULT] # e.g. set_env_from_file_or_def 'RCON_PASSWORD' 'test' # Fills $VAR either with the content of the file with the name $VAR_FILE # or with DEFAULT. # If $VAR is already set nothing will be changed # If both $VAR and $VAR_FILE are set $VAR will keep its value and content # of $VAR_FILE will be ignored. function set_env_from_file_or_def() { local VAR="$1" local FILEVAR="${VAR}_FILE" local DEFAULTVAL="${2:-}" local RETURNVAL="$DEFAULTVAL" if [ "${!VAR:-}" ]; then RETURNVAL="${!VAR}" elif [ "${!FILEVAR:-}" ]; then RETURNVAL="$(< "${!FILEVAR}")" fi export "$VAR"="$RETURNVAL" unset "$FILEVAR" } export SERVER_HOSTNAME="${SERVER_HOSTNAME:-Counter-Strike: Global Offensive Dedicated Server}" set_env_from_file_or_def 'SERVER_PASSWORD' set_env_from_file_or_def 'RCON_PASSWORD' 'changeme' set_env_from_file_or_def 'STEAM_ACCOUNT' 'changeme' set_env_from_file_or_def 'AUTHKEY' 'changeme' set_env_from_file_or_def 'IP' '0.0.0.0' export PORT="${PORT:-27015}" export TV_PORT="${TV_PORT:-27020}" export TICKRATE="${TICKRATE:-128}" export FPS_MAX="${FPS_MAX:-400}" export GAME_TYPE="${GAME_TYPE:-0}" export GAME_MODE="${GAME_MODE:-1}" export MAP="${MAP:-de_dust2}" export MAPGROUP="${MAPGROUP:-mg_active}" export HOST_WORKSHOP_COLLECTION="${HOST_WORKSHOP_COLLECTION:-}" export WORKSHOP_START_MAP="${WORKSHOP_START_MAP:-}" export MAXPLAYERS="${MAXPLAYERS:-12}" export TV_ENABLE="${TV_ENABLE:-1}" export LAN="${LAN:-0}" set_env_from_file_or_def 'SOURCEMOD_ADMINS' export RETAKES="${RETAKES:-0}" export ANNOUNCEMENT_IP="${ANNOUNCEMENT_IP:-}" export NOMASTER="${NOMASTER:-}" # Create dynamic autoexec config mkdir -p "$CSGO_DIR/csgo/cfg" if [ ! -s "$CSGO_DIR/csgo/cfg/autoexec.cfg" ]; then cat << AUTOEXECCFG > "$CSGO_DIR/csgo/cfg/autoexec.cfg" log on hostname "$SERVER_HOSTNAME" rcon_password "$RCON_PASSWORD" sv_password "$SERVER_PASSWORD" sv_cheats 0 exec banned_user.cfg exec banned_ip.cfg AUTOEXECCFG else sed -i "s/^hostname.*/hostname \"$SERVER_HOSTNAME\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg sed -i "s/^rcon_password.*/rcon_password \"$RCON_PASSWORD\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg sed -i "s/^sv_password.*/sv_password \"$SERVER_PASSWORD\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg fi # Create dynamic server config if [ ! -s "$CSGO_DIR/csgo/cfg/server.cfg" ]; then cat << SERVERCFG > "$CSGO_DIR/csgo/cfg/server.cfg" tv_enable $TV_ENABLE tv_delaymapchange 1 tv_delay 30 tv_deltacache 2 tv_dispatchmode 1 tv_maxclients 10 tv_maxrate 0 tv_overridemaster 0 tv_relayvoice 1 tv_snapshotrate 64 tv_timeout 60 tv_transmitall 1 writeid writeip sv_mincmdrate $TICKRATE sv_maxupdaterate $TICKRATE sv_minupdaterate $TICKRATE SERVERCFG else sed -i "s/^tv_enable.*/tv_enable $TV_ENABLE/" $CSGO_DIR/csgo/cfg/server.cfg fi # Attempt to update CSGO before starting the server [[ -z ${CI+x} ]] && "$STEAMCMD_DIR/steamcmd.sh" +login anonymous +force_install_dir "$CSGO_DIR" +app_update "$CSGO_APP_ID" +quit # Install and configure plugins & extensions "$BASH" "$STEAM_DIR/manage_plugins.sh" # Update PugSetup configuration via environment variables "$BASH" "$STEAM_DIR/manage_pugsetup_configs.sh" SRCDS_ARGUMENTS=( "-console" "-usercon" "-game csgo" "-autoupdate" "-authkey $AUTHKEY" "-steam_dir $STEAMCMD_DIR" "-steamcmd_script $STEAM_DIR/autoupdate_script.txt" "-tickrate $TICKRATE" "-port $PORT" "-net_port_try 1" "-ip $IP" "-maxplayers_override $MAXPLAYERS" "+fps_max $FPS_MAX" "+game_type $GAME_TYPE" "+game_mode $GAME_MODE" "+mapgroup $MAPGROUP" "+map $MAP" "+sv_setsteamaccount" "$STEAM_ACCOUNT" "+sv_lan $LAN" "+tv_port $TV_PORT" ) if [[ -n $HOST_WORKSHOP_COLLECTION ]]; then SRCDS_ARGUMENTS+=("+host_workshop_collection $HOST_WORKSHOP_COLLECTION") fi if [[ -n $WORKSHOP_START_MAP ]]; then SRCDS_ARGUMENTS+=("+workshop_start_map $WORKSHOP_START_MAP") fi if [[ -n $ANNOUNCEMENT_IP ]]; then SRCDS_ARGUMENTS+=("+net_public_adr $ANNOUNCEMENT_IP") fi if [[ $NOMASTER == 1 ]]; then SRCDS_ARGUMENTS+=("-nomaster") fi SRCDS_RUN="$CSGO_DIR/srcds_run" # Patch srcds_run to fix autoupdates if grep -q 'steam.sh' "$SRCDS_RUN"; then sed -i 's/steam.sh/steamcmd.sh/' "$SRCDS_RUN" echo "Applied patch to srcds_run to fix autoupdates" fi # Start the server exec "$BASH" "$SRCDS_RUN" "${SRCDS_ARGUMENTS[@]}" ================================================ FILE: docker-compose.yaml ================================================ version: "3.7" volumes: csgo-data: name: csgo-data services: csgo: image: kmallea/csgo:latest container_name: csgo-ds environment: SERVER_HOSTNAME: "Counter-Strike: Global Offensive Dedicated Server" SERVER_PASSWORD: RCON_PASSWORD: changeme STEAM_ACCOUNT: changeme AUTHKEY: changeme SOURCEMOD_ADMINS: comma,delimited,list,of,steam,ids IP: 0.0.0.0 PORT: 27015 TV_PORT: 27020 TICKRATE: 128 FPS_MAX: 300 GAME_TYPE: 0 GAME_MODE: 1 MAP: de_dust2 MAPGROUP: mg_active MAXPLAYERS: 12 TV_ENABLE: 1 LAN: 0 RETAKES: 0 volumes: - type: volume source: csgo-data target: /home/steam/csgo network_mode: "host" restart: unless-stopped stdin_open: true tty: true ================================================ FILE: test/bin/bats ================================================ #!/usr/bin/env bash set -e BATS_READLINK='true' if command -v 'greadlink' >/dev/null; then BATS_READLINK='greadlink' elif command -v 'readlink' >/dev/null; then BATS_READLINK='readlink' fi bats_resolve_absolute_root_dir() { local cwd="$PWD" local path="$1" local result="$2" local target_dir local target_name local original_shell_options="$-" # Resolve the parent directory, e.g. /bin => /usr/bin on CentOS (#113). set -P while true; do target_dir="${path%/*}" target_name="${path##*/}" if [[ "$target_dir" != "$path" ]]; then cd "$target_dir" fi if [[ -L "$target_name" ]]; then path="$("$BATS_READLINK" "$target_name")" else printf -v "$result" -- '%s' "${PWD%/*}" set +P "-$original_shell_options" cd "$cwd" return fi done } export BATS_ROOT bats_resolve_absolute_root_dir "$0" 'BATS_ROOT' exec "$BATS_ROOT/libexec/bats-core/bats" "$@" ================================================ FILE: test/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg ================================================ sm_pugsetup_snake_captain_picks "0" ================================================ FILE: test/libexec/bats-core/bats ================================================ #!/usr/bin/env bash set -e export BATS_VERSION='1.2.0-dev' version() { printf 'Bats %s\n' "$BATS_VERSION" } abort() { printf 'Error: %s\n' "$1" >&2 usage >&2 exit 1 } usage() { local cmd="${0##*/}" local line while IFS= read -r line; do printf '%s\n' "$line" done <] [-j ] [-p | -t] ... $cmd [-h | -v] is the path to a Bats test file, or the path to a directory containing Bats test files (ending with ".bats"). -c, --count Count the number of test cases without running any tests -f, --filter Filter test cases by names matching the regular expression -h, --help Display this help message -j, --jobs Number of parallel jobs to run (requires GNU parallel) -p, --pretty Show results in pretty format (default for terminals) -r, --recursive Include tests in subdirectories -t, --tap Show results in TAP format -v, --version Display the version number For more information, see https://github.com/bats-core/bats-core END_OF_HELP_TEXT } expand_link() { readlink="$(type -p greadlink readlink | head -1)" "$readlink" -f "$1" } expand_path() { local path="${1%/}" local dirname="${path%/*}" local result="$2" if [[ "$dirname" == "$path" ]]; then dirname="$PWD" else cd "$dirname" dirname="$PWD" cd "$OLDPWD" fi printf -v "$result" '%s/%s' "$dirname" "${path##*/}" } BATS_LIBEXEC="$(dirname "$(expand_link "${BASH_SOURCE[0]}")")" export BATS_CWD="$PWD" export BATS_TEST_PATTERN="^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\{(.*)\$" export BATS_TEST_FILTER= export PATH="$BATS_LIBEXEC:$PATH" arguments=() # Unpack single-character options bundled together, e.g. -cr, -pr. for arg in "$@"; do if [[ "$arg" =~ ^-[^-]. ]]; then index=1 while option="${arg:$((index++)):1}"; do if [[ -z "$option" ]]; then break fi arguments+=("-$option") done else arguments+=("$arg") fi shift done set -- "${arguments[@]}" arguments=() unset flags pretty recursive flags=() pretty= recursive= if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then pretty=1 fi while [[ "$#" -ne 0 ]]; do case "$1" in -h|--help) version usage exit 0 ;; -v|--version) version exit 0 ;; -c|--count) flags+=('-c') ;; -f|--filter) shift flags+=('-f' "$1") ;; -j|--jobs) shift flags+=('-j' "$1") ;; -r|--recursive) recursive=1 ;; -t|--tap) pretty= ;; -p|--pretty) pretty=1 ;; -*) abort "Bad command line option '$1'" ;; *) arguments+=("$1") ;; esac shift done if [[ "${#arguments[@]}" -eq 0 ]]; then abort 'Must specify at least one ' fi filenames=() for filename in "${arguments[@]}"; do expand_path "$filename" 'filename' if [[ -d "$filename" ]]; then shopt -s nullglob if [[ "$recursive" -eq 1 ]]; then while IFS= read -r -d $'\0' file; do filenames+=("$file") done < <(find "$filename" -type f -name '*.bats' -print0 | sort -z) else for suite_filename in "$filename"/*.bats; do filenames+=("$suite_filename") done fi shopt -u nullglob else filenames+=("$filename") fi done formatter="cat" if [[ -n "$pretty" ]]; then flags+=("-x") formatter="bats-format-tap-stream" fi set -o pipefail execfail exec bats-exec-suite "${flags[@]}" "${filenames[@]}" | "$formatter" ================================================ FILE: test/libexec/bats-core/bats-exec-suite ================================================ #!/usr/bin/env bash set -e count_only_flag='' extended_syntax_flag='' filter='' num_jobs=1 have_gnu_parallel= flags=() while [[ "$#" -ne 0 ]]; do case "$1" in -c) count_only_flag=1 ;; -f) shift filter="$1" flags+=('-f' "$filter") ;; -j) shift num_jobs="$1" ;; -x) # shellcheck disable=SC2034 extended_syntax_flag='-x' flags+=('-x') ;; *) break ;; esac shift done if ( type -p parallel &>/dev/null ); then # shellcheck disable=SC2034 have_gnu_parallel=1 elif [[ "$num_jobs" != 1 ]]; then printf 'bats: cannot execute "%s" jobs without GNU parallel\n' "$num_jobs" >&2 exit 1 fi trap 'kill 0; exit 1' INT all_tests=() for filename in "$@"; do if [[ ! -f "$filename" ]]; then printf 'bats: %s does not exist\n' "$filename" >&2 exit 1 fi test_names=() test_dupes=() while read -r line; do if [[ ! "$line" =~ ^bats_test_function\ ]]; then continue fi line="${line%$'\r'}" line="${line#* }" all_tests+=( "$(printf "%s\t%s" "$filename" "$line")" ) if [[ " ${test_names[*]} " == *" $line "* ]]; then test_dupes+=("$line") continue fi test_names+=("$line") done < <(BATS_TEST_FILTER="$filter" bats-preprocess "$filename") if [[ "${#test_dupes[@]}" -ne 0 ]]; then printf 'bats warning: duplicate test name(s) in %s: %s\n' "$filename" "${test_dupes[*]}" >&2 fi done test_count="${#all_tests[@]}" if [[ -n "$count_only_flag" ]]; then printf '%d\n' "${test_count}" exit fi status=0 printf '1..%d\n' "${test_count}" # No point on continuing if there's no tests. if [[ "${test_count}" == 0 ]]; then exit fi if [[ "$num_jobs" != 1 ]]; then # Only use GNU parallel when we want parallel execution -- there is a small # amount of overhead using it over a simple loop in the serial case. set -o pipefail printf '%s\n' "${all_tests[@]}" | grep -v '^$' | \ parallel -qk -j "$num_jobs" --colsep="\t" -- bats-exec-test "${flags[@]}" '{1}' '{2}' '{#}' || status=1 else # Just do it serially. test_number=0 for test_line in "${all_tests[@]}"; do # Only handle non-empty lines if [[ $test_line ]]; then filename="${test_line%%$'\t'*}" test_name="${test_line##*$'\t'}" ((++test_number)) bats-exec-test "${flags[@]}" "$filename" "$test_name" "$test_number" || status=1 fi done if [[ "${test_number}" != "${test_count}" ]]; then printf '# bats warning: Only executed %s of %s tests\n' "$test_number" "$test_count" status=1 fi fi exit "$status" ================================================ FILE: test/libexec/bats-core/bats-exec-test ================================================ #!/usr/bin/env bash set -eET # Variables used in other scripts. BATS_COUNT_ONLY='' BATS_TEST_FILTER='' BATS_EXTENDED_SYNTAX='' while [[ "$#" -ne 0 ]]; do case "$1" in -c) # shellcheck disable=SC2034 BATS_COUNT_ONLY=1 ;; -f) shift # shellcheck disable=SC2034 BATS_TEST_FILTER="$1" ;; -x) BATS_EXTENDED_SYNTAX='-x' ;; *) break ;; esac shift done BATS_TEST_FILENAME="$1" shift if [[ -z "$BATS_TEST_FILENAME" ]]; then printf 'usage: bats-exec-test \n' >&2 exit 1 elif [[ ! -f "$BATS_TEST_FILENAME" ]]; then printf 'bats: %s does not exist\n' "$BATS_TEST_FILENAME" >&2 exit 1 fi BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}" BATS_TEST_NAMES=() load() { local name="$1" local filename if [[ "${name:0:1}" == '/' ]]; then filename="${name}" else filename="$BATS_TEST_DIRNAME/${name}.bash" fi if [[ ! -f "$filename" ]]; then printf 'bats: %s does not exist\n' "$filename" >&2 exit 1 fi # Dynamically loaded user files provided outside of Bats. # shellcheck disable=SC1090 source "${filename}" } run() { local origFlags="$-" set +eET local origIFS="$IFS" # 'output', 'status', 'lines' are global variables available to tests. # shellcheck disable=SC2034 output="$("$@" 2>&1)" # shellcheck disable=SC2034 status="$?" # shellcheck disable=SC2034,SC2206 IFS=$'\n' lines=($output) IFS="$origIFS" set "-$origFlags" } setup() { return 0 } teardown() { return 0 } skip() { BATS_TEST_SKIPPED="${1:-1}" BATS_TEST_COMPLETED=1 exit 0 } bats_test_begin() { BATS_TEST_DESCRIPTION="$1" if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then printf 'begin %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3 fi setup } bats_test_function() { local test_name="$1" BATS_TEST_NAMES+=("$test_name") } bats_capture_stack_trace() { local test_file local funcname local i BATS_STACK_TRACE=() for ((i=2; i != ${#FUNCNAME[@]}; ++i)); do # Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby # calling an exported function erases the test file's BASH_SOURCE entry. test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}" funcname="${FUNCNAME[$i]}" BATS_STACK_TRACE+=("${BASH_LINENO[$((i-1))]} $funcname $test_file") if [[ "$test_file" == "$BATS_TEST_SOURCE" ]]; then case "$funcname" in "$BATS_TEST_NAME"|setup|teardown) break ;; esac fi done } bats_print_stack_trace() { local frame local index=1 local count="${#@}" local filename local lineno for frame in "$@"; do bats_frame_filename "$frame" 'filename' bats_trim_filename "$filename" 'filename' bats_frame_lineno "$frame" 'lineno' if [[ $index -eq 1 ]]; then printf '# (' else printf '# ' fi local fn bats_frame_function "$frame" 'fn' if [[ "$fn" != "$BATS_TEST_NAME" ]]; then printf "from function \`%s' " "$fn" fi if [[ $index -eq $count ]]; then printf 'in test file %s, line %d)\n' "$filename" "$lineno" else printf 'in file %s, line %d,\n' "$filename" "$lineno" fi ((++index)) done } bats_print_failed_command() { local frame="${BATS_STACK_TRACE[${#BATS_STACK_TRACE[@]}-1]}" local filename local lineno local failed_line local failed_command bats_frame_filename "$frame" 'filename' bats_frame_lineno "$frame" 'lineno' bats_extract_line "$filename" "$lineno" 'failed_line' bats_strip_string "$failed_line" 'failed_command' printf '%s' "# \`${failed_command}' " if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then printf 'failed\n' else printf 'failed with status %d\n' "$BATS_ERROR_STATUS" fi } bats_frame_lineno() { printf -v "$2" '%s' "${1%% *}" } bats_frame_function() { local __bff_function="${1#* }" printf -v "$2" '%s' "${__bff_function%% *}" } bats_frame_filename() { local __bff_filename="${1#* }" __bff_filename="${__bff_filename#* }" if [[ "$__bff_filename" == "$BATS_TEST_SOURCE" ]]; then __bff_filename="$BATS_TEST_FILENAME" fi printf -v "$2" '%s' "$__bff_filename" } bats_extract_line() { local __bats_extract_line_line local __bats_extract_line_index=0 while IFS= read -r __bats_extract_line_line; do if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}" break fi done <"$1" } bats_strip_string() { [[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]] printf -v "$2" '%s' "${BASH_REMATCH[1]}" } bats_trim_filename() { printf -v "$2" '%s' "${1#$BATS_CWD/}" } bats_debug_trap() { if [[ "${BASH_SOURCE[0]}" != "$1" ]]; then # The last entry in the stack trace is not useful when en error occured: # It is either duplicated (kinda correct) or has wrong line number (Bash < 4.4) # Therefore we capture the stacktrace but use it only after the next debug # trap fired. # Expansion is required for empty arrays which otherwise error BATS_CURRENT_STACK_TRACE=( "${BATS_STACK_TRACE[@]+"${BATS_STACK_TRACE[@]}"}" ) bats_capture_stack_trace fi } # For some versions of Bash, the `ERR` trap may not always fire for every # command failure, but the `EXIT` trap will. Also, some command failures may not # set `$?` properly. See #72 and #81 for details. # # For this reason, we call `bats_error_trap` at the very beginning of # `bats_teardown_trap` (the `DEBUG` trap for the call will fix the stack trace) # and check the value of `$BATS_TEST_COMPLETED` before taking other actions. # We also adjust the exit status value if needed. # # See `bats_exit_trap` for an additional EXIT error handling case when `$?` # isn't set properly during `teardown()` errors. bats_error_trap() { local status="$?" if [[ -z "$BATS_TEST_COMPLETED" ]]; then BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}" if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then BATS_ERROR_STATUS=1 fi BATS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) trap - DEBUG fi } bats_teardown_trap() { bats_error_trap local status=0 teardown >>"$BATS_OUT" 2>&1 || status="$?" if [[ $status -eq 0 ]]; then BATS_TEARDOWN_COMPLETED=1 elif [[ -n "$BATS_TEST_COMPLETED" ]]; then BATS_ERROR_STATUS="$status" fi bats_exit_trap } bats_exit_trap() { local line local status local skipped='' trap - ERR EXIT if [[ -n "$BATS_TEST_SKIPPED" ]]; then skipped=' # skip' if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then skipped+=" $BATS_TEST_SKIPPED" fi fi if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" ]]; then if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then # For some versions of bash, `$?` may not be set properly for some error # conditions before triggering the EXIT trap directly (see #72 and #81). # Thanks to the `BATS_TEARDOWN_COMPLETED` signal, this will pinpoint such # errors if they happen during `teardown()` when `bats_perform_test` calls # `bats_teardown_trap` directly after the test itself passes. # # If instead the test fails, and the `teardown()` error happens while # `bats_teardown_trap` runs as the EXIT trap, the test will fail with no # output, since there's no way to reach the `bats_exit_trap` call. BATS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) BATS_ERROR_STATUS=1 fi printf 'not ok %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3 bats_print_stack_trace "${BATS_STACK_TRACE[@]}" >&3 bats_print_failed_command >&3 while IFS= read -r line; do printf '# %s\n' "$line" done <"$BATS_OUT" >&3 if [[ -n "$line" ]]; then printf '# %s\n' "$line" fi status=1 else printf 'ok %d %s%s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" \ "$skipped" >&3 status=0 fi rm -f "$BATS_OUT" bats_cleanup_preprocessed_source exit "$status" } bats_perform_test() { BATS_TEST_NAME="$1" BATS_TEST_NUMBER="$2" if ! declare -F "$BATS_TEST_NAME" &>/dev/null; then printf "bats: unknown test name \`%s'\n" "$BATS_TEST_NAME" >&2 exit 1 fi # Some versions of Bash will reset BASH_LINENO to the first line of the # function when the ERR trap fires. All versions of Bash appear to reset it # on an unbound variable access error. bats_debug_trap will fire both before # the offending line is executed, and when the error is triggered. # Consequently, we use `BATS_CURRENT_STACK_TRACE` recorded by the # first call to bats_debug_trap, _before_ the ERR trap or unbound variable # access fires. BATS_STACK_TRACE=() BATS_CURRENT_STACK_TRACE=() BATS_TEST_COMPLETED= BATS_TEST_SKIPPED= BATS_TEARDOWN_COMPLETED= BATS_ERROR_STATUS= trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG trap 'bats_error_trap' ERR trap 'bats_teardown_trap' EXIT "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 BATS_TEST_COMPLETED=1 trap 'bats_exit_trap' EXIT bats_teardown_trap } if [[ -z "$TMPDIR" ]]; then BATS_TMPDIR='/tmp' else BATS_TMPDIR="${TMPDIR%/}" fi BATS_TMPNAME="$BATS_TMPDIR/bats.$$" BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID" BATS_OUT="${BATS_TMPNAME}.out" bats_preprocess_source() { BATS_TEST_SOURCE="${BATS_TMPNAME}.src" bats-preprocess "$BATS_TEST_FILENAME" >"$BATS_TEST_SOURCE" trap 'bats_cleanup_preprocessed_source' ERR EXIT trap 'bats_cleanup_preprocessed_source; exit 1' INT } bats_cleanup_preprocessed_source() { rm -f "$BATS_TEST_SOURCE" } bats_evaluate_preprocessed_source() { if [[ -z "$BATS_TEST_SOURCE" ]]; then BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" fi # Dynamically loaded user files provided outside of Bats. # shellcheck disable=SC1090 source "$BATS_TEST_SOURCE" } exec 3<&1 # Run the given test. bats_preprocess_source bats_evaluate_preprocessed_source bats_perform_test "$@" ================================================ FILE: test/libexec/bats-core/bats-format-tap-stream ================================================ #!/usr/bin/env bash set -e header_pattern='[0-9]+\.\.[0-9]+' IFS= read -r header if [[ "$header" =~ $header_pattern ]]; then count="${header:3}" index=0 passed=0 failures=0 skipped=0 name= count_column_width=$(( ${#count} * 2 + 2 )) else # If the first line isn't a TAP plan, print it and pass the rest through printf '%s\n' "$header" exec cat fi update_screen_width() { screen_width="$(tput cols)" count_column_left=$(( screen_width - count_column_width )) } trap update_screen_width WINCH update_screen_width begin() { go_to_column 0 buffer_with_truncation $(( count_column_left - 1 )) ' %s' "$name" clear_to_end_of_line go_to_column $count_column_left buffer "%${#count}s/${count}" "$index" go_to_column 1 } pass() { go_to_column 0 buffer ' ✓ %s' "$name" advance } skip() { local reason="$1" if [[ -n "$reason" ]]; then reason=": $reason" fi go_to_column 0 buffer ' - %s (skipped%s)' "$name" "$reason" advance } fail() { go_to_column 0 set_color 1 bold buffer ' ✗ %s' "$name" advance } log() { set_color 1 buffer ' %s\n' "$1" clear_color } summary() { buffer '\n%d test' "$count" if [[ "$count" -ne 1 ]]; then buffer 's' fi buffer ', %d failure' "$failures" if [[ "$failures" -ne 1 ]]; then buffer 's' fi if [[ "$skipped" -gt 0 ]]; then buffer ', %d skipped' "$skipped" fi not_run=$((count - passed - failures - skipped)) if [[ "$not_run" -gt 0 ]]; then buffer ', %d not run' "$not_run" fi buffer '\n' } buffer_with_truncation() { local width="$1" shift local string # shellcheck disable=SC2059 printf -v 'string' -- "$@" if [[ "${#string}" -gt "$width" ]]; then buffer '%s...' "${string:0:$(( width - 4 ))}" else buffer '%s' "$string" fi } go_to_column() { local column="$1" buffer '\x1B[%dG' $(( column + 1 )) } clear_to_end_of_line() { buffer '\x1B[K' } advance() { clear_to_end_of_line buffer '\n' clear_color } set_color() { local color="$1" local weight=22 if [[ "$2" == 'bold' ]]; then weight=1 fi buffer '\x1B[%d;%dm' "$(( 30 + color ))" "$weight" } clear_color() { buffer '\x1B[0m' } _buffer= buffer() { local content # shellcheck disable=SC2059 printf -v content -- "$@" _buffer+="$content" } flush() { printf '%s' "$_buffer" _buffer= } finish() { flush printf '\n' } trap finish EXIT while IFS= read -r line; do case "$line" in 'begin '* ) ((++index)) name="${line#* $index }" begin flush ;; 'ok '* ) skip_expr="ok $index (.*) # skip ?(([[:print:]]*))?" if [[ "$line" =~ $skip_expr ]]; then ((++skipped)) skip "${BASH_REMATCH[2]}" else ((++passed)) pass fi ;; 'not ok '* ) ((++failures)) fail ;; '# '* ) log "${line:2}" ;; esac done summary ================================================ FILE: test/libexec/bats-core/bats-preprocess ================================================ #!/usr/bin/env bash set -e bats_encode_test_name() { local name="$1" local result='test_' local hex_code if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then name="${name//_/-5f}" name="${name//-/-2d}" name="${name// /_}" result+="$name" else local length="${#name}" local char i for ((i=0; i