diff options
-rw-r--r-- | .github/workflows/build.yml | 154 | ||||
-rwxr-xr-x | .github/workflows/ci-build.yml | 119 | ||||
-rw-r--r-- | .github/workflows/codeql-analysis.yml | 2 | ||||
-rw-r--r-- | .github/workflows/release.yml | 139 | ||||
-rwxr-xr-x | .gitignore | 7 | ||||
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | Dockerfile | 42 | ||||
-rwxr-xr-x[-rw-r--r--] | Makefile | 67 | ||||
-rwxr-xr-x | bin/rr | 312 | ||||
-rwxr-xr-x | bors.toml | 21 | ||||
-rw-r--r-- | cmd/rr/cmd/root.go | 158 | ||||
-rwxr-xr-x | composer.json | 9 | ||||
-rw-r--r-- | phpstan.neon.dist | 1 | ||||
-rw-r--r-- | src/Diactoros/ServerRequestFactory.php | 4 | ||||
-rw-r--r-- | src/Diactoros/StreamFactory.php | 2 | ||||
-rw-r--r-- | src/Diactoros/UploadedFileFactory.php | 2 | ||||
-rw-r--r-- | src/HttpClient.php | 9 |
17 files changed, 880 insertions, 172 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..92b55666 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,154 @@ +name: build + +on: + push: + pull_request: + +jobs: + php: + name: Build (PHP ${{ matrix.php }}, ${{ matrix.setup }} setup) + runs-on: ubuntu-20.04 + timeout-minutes: 6 + strategy: + fail-fast: false + matrix: + php: ['7.3', '7.4', '8.0'] + setup: [basic, lowest] + steps: + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 # action page: <https://github.com/shivammathur/setup-php> + with: + php-version: ${{ matrix.php }} + + - name: Check out code + uses: actions/checkout@v2 + + - name: Syntax check only (lint) + run: find ./src/ ./tests/ -name "*.php" -print0 | xargs -0 -n1 -P8 php -l + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Init Composer Cache # Docs: <https://git.io/JfAKn#php---composer> + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.setup }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install lowest Composer dependencies + if: matrix.setup == 'lowest' + run: composer update --prefer-dist --no-progress --prefer-lowest --ansi + + - name: Install basic Composer dependencies + if: matrix.setup == 'basic' + run: composer update --prefer-dist --no-progress --ansi + + - name: Analyze PHP sources + run: composer analyze + + # TODO write phpunit tests + #- name: Analyze PHP sources + # run: composer test + + golang: + name: Build (Go ${{ matrix.go }}, PHP ${{ matrix.php }}) + runs-on: ubuntu-20.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + php: ['7.3', '7.4', '8.0'] + go: ['1.14', '1.15'] + steps: + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v2 # action page: <https://github.com/actions/setup-go> + with: + go-version: ${{ matrix.go }} + + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 # action page: <https://github.com/shivammathur/setup-php> + with: + php-version: ${{ matrix.php }} + + - name: Check out code + uses: actions/checkout@v2 + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Init Composer Cache # Docs: <https://git.io/JfAKn#php---composer> + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer update --prefer-dist --no-progress --ansi + + - name: Init Go modules Cache # Docs: <https://git.io/JfAKn#go---modules> + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-go- + + - name: Install Go dependencies + run: go mod download + + - name: Run golang tests + run: | + mkdir ./coverage-ci + go test -race -v -covermode=atomic -coverprofile=./coverage-ci/lib.txt + go test ./util -race -v -covermode=atomic -coverprofile=./coverage-ci/util.txt + go test ./service -race -v -covermode=atomic -coverprofile=./coverage-ci/service.txt + go test ./service/env -race -v -covermode=atomic -coverprofile=./coverage-ci/env.txt + go test ./service/rpc -race -v -covermode=atomic -coverprofile=./coverage-ci/rpc.txt + go test ./service/http -race -v -covermode=atomic -coverprofile=./coverage-ci/http.txt + go test ./service/static -race -v -covermode=atomic -coverprofile=./coverage-ci/static.txt + go test ./service/limit -race -v -covermode=atomic -coverprofile=./coverage-ci/limit.txt + go test ./service/headers -race -v -covermode=atomic -coverprofile=./coverage-ci/headers.txt + go test ./service/metrics -race -v -covermode=atomic -coverprofile=./coverage-ci/metrics.txt + go test ./service/health -race -v -covermode=atomic -coverprofile=./coverage-ci/health.txt + go test ./service/gzip -race -v -covermode=atomic -coverprofile=./coverage-ci/gzip.txt + go test ./service/reload -race -v -covermode=atomic -coverprofile=./coverage-ci/reload.txt + cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt + + - uses: codecov/codecov-action@v1 # Docs: <https://github.com/codecov/codecov-action> + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage-ci/summary.txt + fail_ci_if_error: false + + golangci-check: + name: Golang-CI (lint) + runs-on: ubuntu-20.04 + steps: + - name: Check out code + uses: actions/checkout@v1 + + - name: golangci-lint + uses: reviewdog/action-golangci-lint@v1 # action page: <https://github.com/reviewdog/action-golangci-lint> + with: + github_token: ${{ secrets.github_token }} + + image: + name: Build docker image + runs-on: ubuntu-20.04 + timeout-minutes: 10 + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Build image + run: docker build -t roadrunner:local -f Dockerfile . + + - name: Scan image + uses: anchore/scan-action@v2 # action page: <https://github.com/anchore/scan-action> + with: + image: roadrunner:local + fail-build: true + severity-cutoff: low # negligible, low, medium, high or critical diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml deleted file mode 100755 index 8ec3eec9..00000000 --- a/.github/workflows/ci-build.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: CI - -on: [ push, pull_request ] - -jobs: - build: - name: Build (PHP ${{ matrix.php }}, Go ${{ matrix.go }}, OS ${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - php: [ 7.4, 8.0 ] - go: [ 1.14, 1.15 ] - os: [ ubuntu-20.04 ] - env: - GO111MODULE: on - steps: - - name: Set up Go ${{ matrix.go }} - uses: actions/setup-go@v1 - with: - go-version: ${{ matrix.go }} - - - name: Set up PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: dom - coverage: xdebug - - - name: Check out code - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - - name: Show versions - run: php -v ; composer -V ; go version - - - name: Debug if needed - env: - DEBUG: ${{ secrets.DEBUG }} - run: if [[ "$DEBUG" == "true" ]]; then env && go env; fi - - - name: Syntax check only (lint) - run: find ./src/ -name "*.php" -print0 | xargs -0 -n1 -P8 php -l - - - name: Get Composer Cache Directory # Docs: <https://github.com/actions/cache/blob/master/examples.md#php---composer> - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies # Docs: <https://github.com/actions/cache/blob/master/examples.md#php---composer> - uses: actions/cache@v1 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install Composer dependencies - run: composer install --prefer-dist --no-interaction - - # - name: Analyze PHP sources - # run: composer analyze - - - name: Install Go dependencies - run: go mod download - - - name: Run golang tests - run: | - mkdir ./coverage-ci - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/lib.txt -covermode=atomic . - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/rpc_config.txt -covermode=atomic ./plugins/rpc - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/rpc.txt -covermode=atomic ./plugins/rpc/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/plugin_config.txt -covermode=atomic ./plugins/config/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/logger.txt -covermode=atomic ./plugins/logger/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/server.txt -covermode=atomic ./plugins/server/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/metrics.txt -covermode=atomic ./plugins/metrics/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/informer.txt -covermode=atomic ./plugins/informer/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/informer.txt -covermode=atomic ./plugins/resetter/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/attributes.txt -covermode=atomic ./plugins/http/attributes - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/http_tests.txt -covermode=atomic ./plugins/http/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/gzip.txt -covermode=atomic ./plugins/gzip/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/static.txt -covermode=atomic ./plugins/static/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/static_root.txt -covermode=atomic ./plugins/static - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/headers.txt -covermode=atomic ./plugins/headers/tests - go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/checker.txt -covermode=atomic ./plugins/checker/tests - cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt - - - name: Run code coverage - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: summary.txt - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false - verbose: true - - - golangci-check: - name: runner / golangci-lint - runs-on: ubuntu-latest - steps: - - name: Check out code into the Go module directory - uses: actions/checkout@v1 - - name: golangci-lint - uses: reviewdog/action-golangci-lint@v1 - with: - github_token: ${{ secrets.github_token }} - -# image: -# name: Build docker image -# runs-on: ubuntu-latest -# steps: -# - name: Check out code -# uses: actions/checkout@v2 -# with: -# fetch-depth: 1 -# -# - name: Build image -# run: docker build -t rr:local -f Dockerfile . diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1c90e4a4..75e40110 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. + # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b1cd83ae --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,139 @@ +name: release + +on: + release: # Docs: <https://help.github.com/en/articles/events-that-trigger-workflows#release-event-release> + types: [published] + +jobs: + build: + name: Build for ${{ matrix.os }} (${{ matrix.arch }}, ${{ matrix.compiler }}) + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + os: [windows, freebsd, darwin] # linux, freebsd, darwin, windows + compiler: [gcc] # gcc, musl-gcc + archiver: [zip] # tar, zip + arch: [amd64] # amd64, 386 + include: + - os: linux + compiler: gcc + archiver: tar + arch: amd64 + - os: '' + compiler: musl-gcc # more info: <https://musl.libc.org/> + archiver: zip + arch: amd64 + steps: + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.5 + + - name: Check out code + uses: actions/checkout@v2 + + - name: Install musl + if: matrix.compiler == 'musl-gcc' + run: sudo apt-get install -y musl-tools + + - name: Download dependencies + run: go mod download # `-x` means "verbose" mode + + - name: Generate builder values + id: values + run: | + echo "::set-output name=version::`echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//'`" + echo "::set-output name=timestamp::`date +%FT%T%z`" + echo "::set-output name=binary-name::rr`[ ${{ matrix.os }} = 'windows' ] && echo '.exe'`" + + - name: Compile binary file + env: + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + CC: ${{ matrix.compiler }} + CGO_ENABLED: 0 + LDFLAGS: >- + -s + -X github.com/spiral/roadrunner/cmd/rr/cmd.Version=${{ steps.values.outputs.version }} + -X github.com/spiral/roadrunner/cmd/rr/cmd.BuildTime=${{ steps.values.outputs.timestamp }} + run: | + go build -trimpath -ldflags "$LDFLAGS" -o "./${{ steps.values.outputs.binary-name }}" ./cmd/rr/main.go + stat "./${{ steps.values.outputs.binary-name }}" + + - name: Generate distributive directory name + id: dist-dir + run: > + echo "::set-output name=name::roadrunner-${{ steps.values.outputs.version }}-$( + [ ${{ matrix.os }} != '' ] && echo '${{ matrix.os }}' || echo 'unknown' + )$( + [ ${{ matrix.compiler }} = 'musl-gcc' ] && echo '-musl' + )-${{ matrix.arch }}" + + - name: Generate distributive archive name + id: dist-arch + run: > + echo "::set-output name=name::${{ steps.dist-dir.outputs.name }}.$( + case ${{ matrix.archiver }} in + zip) echo 'zip';; + tar) echo 'tar.gz';; + *) exit 10; + esac + )" + + - name: Create distributive + run: | + mkdir ${{ steps.dist-dir.outputs.name }} + mv "./${{ steps.values.outputs.binary-name }}" ./${{ steps.dist-dir.outputs.name }}/ + cp ./README.md ./CHANGELOG.md ./LICENSE ./${{ steps.dist-dir.outputs.name }}/ + + - name: Pack distributive using tar + if: matrix.archiver == 'tar' + run: tar -zcf "${{ steps.dist-arch.outputs.name }}" "${{ steps.dist-dir.outputs.name }}" + + - name: Pack distributive using zip + if: matrix.archiver == 'zip' + run: zip -r -q "${{ steps.dist-arch.outputs.name }}" "${{ steps.dist-dir.outputs.name }}" + + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ steps.dist-dir.outputs.name }} + path: ${{ steps.dist-arch.outputs.name }} + if-no-files-found: error + retention-days: 30 + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.dist-arch.outputs.name }} + asset_name: ${{ steps.dist-arch.outputs.name }} + tag: ${{ github.ref }} + + docker: + name: Build docker image + runs-on: ubuntu-20.04 + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Make docker login + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_LOGIN }}" --password-stdin + + - name: Generate builder values + id: values + run: | + echo "::set-output name=version::`echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//'`" + echo "::set-output name=timestamp::`date +%FT%T%z`" + + - name: Build image + run: | + docker build \ + --tag "spiralscout/roadrunner:${{ steps.values.outputs.version }}" \ + --build-arg "APP_VERSION=${{ steps.values.outputs.version }}" \ + --build-arg "BUILD_TIME=${{ steps.values.outputs.timestamp }}" \ + . + + - name: Push image into registry + run: docker push "spiralscout/roadrunner:${{ steps.values.outputs.version }}" @@ -16,5 +16,10 @@ # Dependency directories (remove the comment below to include it) # vendor/ .idea +composer.lock +vendor vendor_php -builds
\ No newline at end of file +builds/ +tests/vendor/ +.rr-sample.yaml +psr-worker.php diff --git a/CHANGELOG.md b/CHANGELOG.md index db8ad552..030b8c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +UNRELEASED +---------- +- Add `rr --version` flag support + v1.9.0 (02.12.2020) ------------------- - Update PHP minimal supported version to 7.3 @@ -1,17 +1,39 @@ -FROM golang:latest as builder +# Image page: <https://hub.docker.com/_/golang> +FROM golang:1.15.5 as builder -COPY . /src +# app version and build date must be passed during image building (version without any prefix). +# e.g.: `docker build --build-arg "APP_VERSION=1.2.3" --build-arg "BUILD_TIME=$(date +%FT%T%z)" .` +ARG APP_VERSION="undefined" +ARG BUILD_TIME="undefined" + +# arguments to pass on each go tool link invocation +ENV LDFLAGS="-s \ +-X github.com/spiral/roadrunner/cmd/rr/cmd.Version=$APP_VERSION \ +-X github.com/spiral/roadrunner/cmd/rr/cmd.BuildTime=$BUILD_TIME" + +RUN mkdir /src WORKDIR /src +COPY ./go.mod ./go.sum ./ + +# Burn modules cache RUN set -x \ - && apt-get update -y \ - && apt-get install -y bash git \ && go version \ - && bash ./build.sh \ - && test -f ./.rr.yaml + && go mod download \ + && go mod verify + +COPY . . + +# compile binary file +RUN CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./rr ./cmd/rr/main.go + +# Image page: <https://hub.docker.com/_/alpine> +FROM alpine:3.12 -FROM alpine:latest +# use same build arguments for image labels +ARG APP_VERSION +ARG BUILD_TIME LABEL \ org.opencontainers.image.title="roadrunner" \ @@ -19,9 +41,13 @@ LABEL \ org.opencontainers.image.url="https://github.com/spiral/roadrunner" \ org.opencontainers.image.source="https://github.com/spiral/roadrunner" \ org.opencontainers.image.vendor="SpiralScout" \ + org.opencontainers.image.version="$APP_VERSION" \ + org.opencontainers.image.created="$BUILD_TIME" \ org.opencontainers.image.licenses="MIT" +# copy required files from builder image COPY --from=builder /src/rr /usr/bin/rr COPY --from=builder /src/.rr.yaml /etc/rr.yaml -ENTRYPOINT ["/usr/bin/rr"]
\ No newline at end of file +# use roadrunner binary as image entrypoint +ENTRYPOINT ["/usr/bin/rr"] @@ -1,23 +1,44 @@ -test: - go clean -testcache - go test -v -race -cover . -tags=debug - go test -v -race -cover ./plugins/rpc -tags=debug - go test -v -race -cover ./plugins/rpc/tests -tags=debug - go test -v -race -cover ./plugins/config/tests -tags=debug - go test -v -race -cover ./plugins/server/tests -tags=debug - go test -v -race -cover ./plugins/logger/tests -tags=debug - go test -v -race -cover ./plugins/metrics/tests -tags=debug - go test -v -race -cover ./plugins/informer/tests -tags=debug - go test -v -race -cover ./plugins/resetter/tests -tags=debug - go test -v -race -cover ./plugins/http/attributes -tags=debug - go test -v -race -cover ./plugins/http/tests -tags=debug - go test -v -race -cover ./plugins/gzip/tests -tags=debug - go test -v -race -cover ./plugins/static/tests -tags=debug - go test -v -race -cover ./plugins/static -tags=debug - go test -v -race -cover ./plugins/headers/tests -tags=debug - go test -v -race -cover ./plugins/checker/tests -tags=debug - -test_headers: - go test -v -race -cover ./plugins/headers/tests -tags=debug -test_checker: - go test -v -race -cover ./plugins/checker/tests -tags=debug
\ No newline at end of file +#!/usr/bin/make +# Makefile readme (ru): <http://linux.yaroslavl.ru/docs/prog/gnu_make_3-79_russian_manual.html> +# Makefile readme (en): <https://www.gnu.org/software/make/manual/html_node/index.html#SEC_Contents> + +SHELL = /bin/sh + +.DEFAULT_GOAL := build + +# This will output the help for each task. thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +help: ## Show this help + @printf "\033[33m%s:\033[0m\n" 'Available commands' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf " \033[32m%-14s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +build: ## Build RR binary file for local os/arch + CGO_ENABLED=0 go build -trimpath -ldflags "-s" -o ./rr ./cmd/rr/main.go + +clean: ## Make some clean + rm ./rr + +install: build ## Build and install RR locally + cp rr /usr/local/bin/rr + +uninstall: ## Uninstall locally installed RR + rm -f /usr/local/bin/rr + +test: ## Run application tests + test -d ./vendor_php || composer update --prefer-dist --ansi + go test -v -race -cover + go test -v -race -cover ./util + go test -v -race -cover ./service + go test -v -race -cover ./service/env + go test -v -race -cover ./service/rpc + go test -v -race -cover ./service/http + go test -v -race -cover ./service/static + go test -v -race -cover ./service/limit + go test -v -race -cover ./service/headers + go test -v -race -cover ./service/metrics + go test -v -race -cover ./service/health + go test -v -race -cover ./service/gzip + go test -v -race -cover ./service/reload + +lint: ## Run application linters + go fmt ./... + golint ./... @@ -0,0 +1,312 @@ +#!/usr/bin/env php +<?php +/** + * RoadRunner + * High-performance PHP process supervisor and load balancer written in Go + * + * This file responsive for cli commands + */ +declare(strict_types=1); + +foreach ([ + __DIR__ . '/../../../autoload.php', + __DIR__ . '/../vendor/autoload.php', + __DIR__ . '/vendor/autoload.php', + __DIR__ . '/../vendor_php/autoload.php' + ] as $file) { + if (file_exists($file)) { + define('RR_COMPOSER_INSTALL', $file); + + break; + } +} + +unset($file); + +if (!defined('RR_COMPOSER_INSTALL')) { + fwrite( + STDERR, + 'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL . + ' composer install' . PHP_EOL . PHP_EOL . + 'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL + ); + + die(1); +} + +if (RRHelper::getOSType() !== 'linux' && !class_exists('ZipArchive')) { + fwrite(STDERR, 'Extension `php-zip` is required.' . PHP_EOL); + die(1); +} + +if (!function_exists('curl_init')) { + fwrite(STDERR, 'Extension `php-curl` is required.' . PHP_EOL); + die(1); +} + +require RR_COMPOSER_INSTALL; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use function Couchbase\defaultDecoder; + +class RRHelper +{ + /** + * @var string + */ + public const SELF_PACKAGE_NAME = 'spiral/roadrunner'; + + /** + * Returns version of RoadRunner based on current package version. + * + * @return string Version of RoadRunner (eg.: `1.8.0`) + */ + public static function getVersion(): string + { + $version = \PackageVersions\Versions::getVersion(self::SELF_PACKAGE_NAME); + + if (\is_int($delimiter_position = \mb_strpos($version, '@'))) { + $version = \mb_substr($version, 0, (int) $delimiter_position); + } + + return \ltrim($version, 'vV'); + } + + /** + * Returns OS Type for filename + * + * @return string OS Type + */ + public static function getOSType(): string + { + switch (PHP_OS) { + case 'Darwin': + return 'darwin'; + case 'Linux': + return 'linux'; + case 'FreeBSD': + return 'freebsd'; + case 'WIN32': + case 'WINNT': + case 'Windows': + return 'windows'; + default: + return 'linux'; + } + } + + /** + * @return string + * @throws Exception + */ + public static function getSignature(): string + { + return 'roadrunner-' . self::getVersion() . '-' . self::getOSType() . '-amd64'; + } + + /** + * Returns generated URL to zip file on GitHub with binary file + * + * @return string URL + * @throws Exception + */ + public static function getBinaryDownloadUrl(): string + { + $ext = '.zip'; + if (self::getOSType() == 'linux') { + $ext = '.tar.gz'; + } + + return 'https://github.com/spiral/roadrunner/releases/download/v' + . static::getVersion() . '/' . self::getSignature() + . $ext; + } + + /** + * Extracts the roadrunner RR binary into given location. + * + * @param string $archive + * @param string $target + * @throws Exception + */ + public static function extractBinary(string $archive, string $target) + { + if (self::getOSType() !== 'linux') { + self::extractZIP($archive, $target); + } else { + self::extractTAR($archive, $target); + } + } + + /** + * @param string $archive + * @param string $target + * @throws Exception + */ + protected static function extractZIP(string $archive, string $target) + { + $zip = new ZipArchive(); + $zip->open($archive); + + $name = self::getSignature() . '/rr'; + if (self::getOSType() == 'windows') { + $name .= '.exe'; + } + + $stream = $zip->getStream($name); + if (!is_resource($stream)) { + return; + } + + $to = fopen($target, 'w'); + stream_copy_to_stream($stream, $to); + fclose($to); + + $zip->close(); + } + + /** + * @param string $archive + * @param string $target + * @throws Exception + */ + protected static function extractTAR(string $archive, string $target) + { + $arch = new PharData($archive); + $arch->extractTo('./', self::getSignature() . '/rr'); + + copy('./' . self::getSignature() . '/rr', $target); + unlink('./' . self::getSignature() . '/rr'); + rmdir('./' . self::getSignature()); + } +} + +(new Application('RoadRunner', RRHelper::getVersion())) + ->register('get-binary') + ->setDescription("Install or update RoadRunner binaries in specified folder (current folder by default)") + ->addOption('location', 'l', InputArgument::OPTIONAL, 'destination folder', '.') + ->setCode(function (InputInterface $input, OutputInterface $output) { + $output->writeln('<info>Updating binary file of RoadRunner</info>'); + + $finalFile = $input->getOption('location') . DIRECTORY_SEPARATOR . 'rr'; + if (RRHelper::getOSType() == 'windows') { + $finalFile .= '.exe'; + } + + if (is_file($finalFile)) { + $version = RRHelper::getVersion(); + + $previousVersion = preg_match( + '#Version:.+(\d+\.\d+\.\d+)#', + (string)shell_exec($finalFile), + $matches + ) ? $matches[1] : ""; + + $output->writeln('<error>RoadRunner binary file already exists!</error>'); + $helper = $this->getHelper('question'); + + if (version_compare($previousVersion, $version) === 0) { + $output->writeln(sprintf('<info>Current version: %s</info>', $previousVersion)); + $question = new ConfirmationQuestion( + sprintf('Skip update to the same version: %s ? [Y/n]', $version) + ); + if ($helper->ask($input, $output, $question)) { + return; + } + } else { + $question = new ConfirmationQuestion('Do you want overwrite it? [Y/n]'); + if (!$helper->ask($input, $output, $question)) { + return; + } + } + } + + $output->writeln('<info>Downloading RoadRunner archive for <fg=cyan>' . ucfirst(RRHelper::getOSType()) . '</fg=cyan></info>'); + + $progressBar = new ProgressBar($output); + $progressBar->setFormat('verbose'); + + $zipFileName = 'rr_zip_'.random_int(0, 10000); + if (RRHelper::getOSType() == 'linux') { + $zipFileName .= '.tar.gz'; + } + + $zipFile = fopen($zipFileName, "w+"); + $curlResource = curl_init(); + + curl_setopt($curlResource, CURLOPT_URL, RRHelper::getBinaryDownloadUrl()); + curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlResource, CURLOPT_BINARYTRANSFER, true); + curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curlResource, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curlResource, CURLOPT_FILE, $zipFile); + curl_setopt($curlResource, CURLOPT_PROGRESSFUNCTION, + function ($resource, $download_size, $downloaded, $upload_size, $uploaded) use (&$progressBar, $output) { + if ($download_size == 0) { + return; + } + + if ($progressBar->getStartTime() === 0) { + $progressBar->start(); + } + + if ($progressBar->getMaxSteps() != $download_size) { + /** + * Workaround for symfony < 4.1.x, for example PHP 7.0 will use 3.x + * feature #26449 Make ProgressBar::setMaxSteps public (ostrolucky) + */ + $progressBar = new ProgressBar($output, $download_size); + } + + $progressBar->setFormat('[%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% ' . intval($download_size / 1024) . 'KB'); + $progressBar->setProgress($downloaded); + }); + curl_setopt($curlResource, CURLOPT_NOPROGRESS, false); // needed to make progress function work + curl_setopt($curlResource, CURLOPT_HEADER, 0); + curl_exec($curlResource); + curl_close($curlResource); + fclose($zipFile); + + $progressBar->finish(); + $output->writeln(""); + + $output->writeln('<info>Unpacking <comment>' . basename(RRHelper::getBinaryDownloadUrl()) . '</comment></info>'); + + RRHelper::extractBinary($zipFileName, $finalFile); + unlink($zipFileName); + + if (!file_exists($finalFile) || filesize($finalFile) === 0) { + throw new Exception('Unable to extract the file.'); + } + + chmod($finalFile, 0755); + $output->writeln('<info>Binary file updated!</info>'); + }) + ->getApplication() + ->register("init-config") + ->setDescription("Inits default .rr.yaml config in specified folder (current folder by default)") + ->addOption('location', 'l', InputArgument::OPTIONAL, 'destination folder', '.') + ->setCode(function (InputInterface $input, OutputInterface $output) { + if (is_file($input->getOption('location') . DIRECTORY_SEPARATOR . '.rr.yaml')) { + $output->writeln('<error>Config file already exists!</error>'); + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Do you want overwrite it? [Y/n] '); + + if (!$helper->ask($input, $output, $question)) { + return; + } + } + + copy( + __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '.rr.yaml', + $input->getOption('location') . DIRECTORY_SEPARATOR . '.rr.yaml' + ); + $output->writeln('<info>Config file created!</info>'); + }) + ->getApplication() + ->run(); @@ -1,11 +1,18 @@ status = [ - 'Build (PHP 7.4, Go 1.15, OS ubuntu-latest)', - 'Build (PHP 7.4, Go 1.14, OS ubuntu-latest)', - 'Build (PHP 8, Go 1.15, OS ubuntu-latest)', - 'Build (PHP 8, Go 1.14, OS ubuntu-latest)', - 'runner / golangci-lint', ] - +'Build (PHP 7.3, basic setup)', +'Build (PHP 7.3, lowest setup)', +'Build (PHP 8.0, basic setup)', +'Build (PHP 8.0, lowest setup)', +'Build (Go 1.14, PHP 7.3)', +'Build (Go 1.15, PHP 7.3)', +'Build (Go 1.14, PHP 7.4)', +'Build (Go 1.15, PHP 7.4)', +'Build (Go 1.14, PHP 8.0)', +'Build (Go 1.15, PHP 8.0)', +'Golang-CI (lint)', +'Build docker image', +] required_approvals = 1 delete_merged_branches = true -timeout-sec = 600
\ No newline at end of file +timeout-sec = 1800 diff --git a/cmd/rr/cmd/root.go b/cmd/rr/cmd/root.go new file mode 100644 index 00000000..e67a4e62 --- /dev/null +++ b/cmd/rr/cmd/root.go @@ -0,0 +1,158 @@ +// Copyright (c) 2018 SpiralScout +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package cmd + +import ( + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spiral/roadrunner/cmd/util" + "github.com/spiral/roadrunner/service" + "github.com/spiral/roadrunner/service/limit" + "log" + "net/http" + "net/http/pprof" + "os" +) + +// Services bus for all the commands. +var ( + cfgFile, workDir, logFormat string + override []string + mergeJson string + + // Verbose enables verbosity mode (container specific). + Verbose bool + + // Debug enables debug mode (service specific). + Debug bool + + // Logger - shared logger. + Logger = logrus.New() + + // Container - shared service bus. + Container = service.NewContainer(Logger) + + // CLI is application endpoint. + CLI = &cobra.Command{ + Use: "rr", + SilenceErrors: true, + SilenceUsage: true, + Version: Version, // allows to use `--version` flag + Short: util.Sprintf( + "<green>RoadRunner</reset>, PHP Application Server\nVersion: <yellow+hb>%s</reset>, %s", + Version, + BuildTime, + ), + } +) + +// Execute adds all child commands to the CLI command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the CLI. +func Execute() { + if err := CLI.Execute(); err != nil { + util.ExitWithError(err) + } +} + +func init() { + CLI.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") + CLI.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "debug mode") + CLI.PersistentFlags().StringVarP(&logFormat, "logFormat", "l", "color", "select log formatter (color, json, plain)") + CLI.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is .rr.yaml)") + CLI.PersistentFlags().StringVarP(&workDir, "workDir", "w", "", "work directory") + CLI.PersistentFlags().StringVarP(&mergeJson, "jsonConfig", "j", "", "merge json configuration") + + CLI.PersistentFlags().StringArrayVarP( + &override, + "override", + "o", + nil, + "override config value (dot.notation=value)", + ) + + cobra.OnInitialize(func() { + if Verbose { + Logger.SetLevel(logrus.DebugLevel) + } + + configureLogger(logFormat) + + cfg, err := util.LoadConfig(cfgFile, []string{"."}, ".rr", override, mergeJson) + if err != nil { + Logger.Warnf("config: %s", err) + return + } + + if workDir != "" { + if err := os.Chdir(workDir); err != nil { + util.ExitWithError(err) + } + } + + if err := Container.Init(cfg); err != nil { + util.ExitWithError(err) + } + + // global watcher config + if Verbose { + wcv, _ := Container.Get(limit.ID) + if wcv, ok := wcv.(*limit.Service); ok { + wcv.AddListener(func(event int, ctx interface{}) { + util.LogEvent(Logger, event, ctx) + }) + } + } + + // if debug --> also run pprof service + if Debug { + go runDebugServer() + } + }) +} +func runDebugServer() { + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + srv := http.Server{ + Addr: ":6061", + Handler: mux, + } + + if err := srv.ListenAndServe(); err != nil { + log.Fatal(err) + } +} + +func configureLogger(format string) { + util.Colorize = false + switch format { + case "color", "default": + util.Colorize = true + Logger.Formatter = &logrus.TextFormatter{ForceColors: true} + case "plain": + Logger.Formatter = &logrus.TextFormatter{DisableColors: true} + case "json": + Logger.Formatter = &logrus.JSONFormatter{} + } +} diff --git a/composer.json b/composer.json index 4b68d498..e3017b97 100755 --- a/composer.json +++ b/composer.json @@ -18,16 +18,17 @@ "ext-json": "*", "ext-curl": "*", "spiral/goridge": "^2.4.2", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-factory": "^1.0.1", + "psr/http-message": "^1.0.1", "symfony/console": "^2.5.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", - "laminas/laminas-diactoros": "^1.3 || ^2.0" + "laminas/laminas-diactoros": "^1.3.6 || ^2.0", + "composer/package-versions-deprecated": "^1.8" }, "config": { "vendor-dir": "vendor_php" }, "require-dev": { - "phpstan/phpstan": "~0.12" + "phpstan/phpstan": "~0.12.34" }, "scripts": { "analyze": "phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi" diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 50c2a587..b5fec74d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,4 @@ parameters: level: 'max' - checkMissingIterableValueType: false paths: - src
\ No newline at end of file diff --git a/src/Diactoros/ServerRequestFactory.php b/src/Diactoros/ServerRequestFactory.php index 3fcf8e29..6a42f207 100644 --- a/src/Diactoros/ServerRequestFactory.php +++ b/src/Diactoros/ServerRequestFactory.php @@ -11,12 +11,14 @@ namespace Spiral\RoadRunner\Diactoros; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\ServerRequest; +use Laminas\Diactoros\ServerRequest; final class ServerRequestFactory implements ServerRequestFactoryInterface { /** * @inheritdoc + * + * @param array<mixed> $serverParams Array of SAPI parameters with which to seed the generated request instance. */ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface { diff --git a/src/Diactoros/StreamFactory.php b/src/Diactoros/StreamFactory.php index cc0a5306..68a77e92 100644 --- a/src/Diactoros/StreamFactory.php +++ b/src/Diactoros/StreamFactory.php @@ -12,7 +12,7 @@ namespace Spiral\RoadRunner\Diactoros; use RuntimeException; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; -use Zend\Diactoros\Stream; +use Laminas\Diactoros\Stream; final class StreamFactory implements StreamFactoryInterface { diff --git a/src/Diactoros/UploadedFileFactory.php b/src/Diactoros/UploadedFileFactory.php index 45773287..daa475c1 100644 --- a/src/Diactoros/UploadedFileFactory.php +++ b/src/Diactoros/UploadedFileFactory.php @@ -12,7 +12,7 @@ namespace Spiral\RoadRunner\Diactoros; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UploadedFileFactoryInterface; use Psr\Http\Message\UploadedFileInterface; -use Zend\Diactoros\UploadedFile; +use Laminas\Diactoros\UploadedFile; final class UploadedFileFactory implements UploadedFileFactoryInterface { diff --git a/src/HttpClient.php b/src/HttpClient.php index 4ca152c8..9b9048ca 100644 --- a/src/HttpClient.php +++ b/src/HttpClient.php @@ -62,14 +62,13 @@ final class HttpClient */ public function respond(int $status, string $body, array $headers = []): void { - if (empty($headers)) { - // this is required to represent empty header set as map and not as array - $headers = new \stdClass(); - } + $sendHeaders = empty($headers) + ? new \stdClass() // this is required to represent empty header set as map and not as array + : $headers; $this->getWorker()->send( $body, - (string) json_encode(['status' => $status, 'headers' => $headers]) + (string) json_encode(['status' => $status, 'headers' => $sendHeaders]) ); } } |