mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-11 05:06:51 +00:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84adbbc461 | |||
| 75811d22e8 | |||
| e26c7683d2 | |||
| d7b71aceda | |||
| 25a787529b | |||
| f82965b825 | |||
| f1cf4ee194 | |||
| 5136919c36 | |||
| 4f8ecd598f | |||
| 65365281eb | |||
| c05dfb36d3 | |||
| b2830b39e0 | |||
| 7997ad2b93 | |||
| 80d743afec | |||
| ac9857a965 | |||
| 0e9fd46a5c | |||
| 305d180a5f | |||
| c4f80103b6 | |||
| 0d57e3645a | |||
| 0afdc31f96 | |||
| 91de6e001e | |||
| 908ed3e723 | |||
| 7411073c08 | |||
| 7d838375bb | |||
| f545f30ec0 | |||
| 6579cdfc7f | |||
| 40c48ba804 | |||
| 1c2cb4f439 | |||
| 650dac37a7 | |||
| 55275b23ee | |||
| bce69e1a1b | |||
| f1917ad0de | |||
| 6ea6d54af6 | |||
| c1b486a7eb | |||
| 454c9e1534 | |||
| eaa673c4e4 | |||
| 9c389e3007 | |||
| 7e9a5934c5 | |||
| cc17366c1c | |||
| 62f6db35db | |||
| be422001e8 | |||
| b2eb35592f | |||
| a3b26431ce | |||
| f460323cc5 | |||
| 26694d3bd8 | |||
| e96713a998 | |||
| 6359b8639e | |||
| 6c9d5ccd4a | |||
| 234554b459 | |||
| 6df5a82364 | |||
| ac75410657 | |||
| 238929c3ec | |||
| 1f79e3b0a7 | |||
| f5af2afce5 | |||
| 9482bea8af | |||
| a55572e5b3 | |||
| 098eb7cb7a | |||
| 68334e3bb8 | |||
| 124231c3c7 | |||
| f591af2cbd | |||
| ff11d20d9c | |||
| 72911235c5 | |||
| 60de00c73f | |||
| 4e080b59d3 | |||
| bac4b90c1d | |||
| ea47c9aa1c | |||
| a91d9762db | |||
| 6fb11d69f9 | |||
| 1eab3296d1 | |||
| 4352154b84 | |||
| 03cf601921 | |||
| 9f13301613 | |||
| 650158ea8a | |||
| 552fc2700f | |||
| 582afa1451 | |||
| c43739a7ef | |||
| 720f662afe | |||
| e9488d12ee | |||
| 9a87b155ba | |||
| f6a1cd9b64 | |||
| 7b7c9093ce | |||
| fa4c0ec823 | |||
| 08af1da966 | |||
| ed8475dacf | |||
| e91cdca6f3 | |||
| c267168cb7 | |||
| 58b45d8458 | |||
| b77512dfd9 | |||
| bdc6542970 | |||
| c942a44f6a | |||
| 55081fa59b | |||
| cc1d0e803b | |||
| b7a2371220 | |||
| ae65385c38 | |||
| ab70e85f1c | |||
| ff57eb2b43 | |||
| e78cb8089b | |||
| 09eef64514 | |||
| a2c2710760 | |||
| 38a0cdb4ab | |||
| c587dfc0dc | |||
| 7a090ffcc9 | |||
| 6ab49367ba | |||
| 12f9fb03c3 | |||
| ac00ef1b64 | |||
| 1e9a77c7b2 | |||
| a8dd52800e | |||
| fab063f194 | |||
| 4902898880 | |||
| c46b3245b8 | |||
| 3afd94c61d | |||
| 89632b7acd | |||
| b8cf193911 | |||
| fb4d34ef5b | |||
| 7f7e360cd7 | |||
| fc06665d2b | |||
| bfaf9765ae | |||
| a702e19dff |
4
.gitignore
vendored
4
.gitignore
vendored
@ -41,3 +41,7 @@ cmake-build-*/
|
||||
|
||||
# Doxygen doc files
|
||||
_doc/
|
||||
|
||||
# gRPC auto-generated C++ source files
|
||||
*.pb.cc
|
||||
*.pb.h
|
||||
|
||||
352
.gitlab-ci.yml
352
.gitlab-ci.yml
@ -30,13 +30,6 @@ stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
.rules-branch-and-MR-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- when: never
|
||||
|
||||
.rules-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -44,16 +37,6 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-always:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -64,173 +47,10 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.after-script-code-coverage:
|
||||
after_script:
|
||||
- go get github.com/boumenot/gocover-cobertura
|
||||
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
|
||||
- "go tool cover -func=/tmp/coverage.out | grep total:"
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-and-MR-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
|
||||
.test-base:
|
||||
stage: test
|
||||
script:
|
||||
- make test
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .test-base
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .after-script-code-coverage
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
|
||||
.windows-base:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.20
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go1.20
|
||||
- export GO111MODULE=on
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export MSYSTEM=
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
- .windows-base
|
||||
stage: test
|
||||
script:
|
||||
- make test
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.build-base:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
rules:
|
||||
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
|
||||
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
|
||||
.linux-build-setup:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .build-base
|
||||
- .linux-build-setup
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
|
||||
.darwin-build-setup:
|
||||
before_script:
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .build-base
|
||||
- .darwin-build-setup
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
.windows-build-setup:
|
||||
# ENV
|
||||
.env-windows:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export GOROOT=/c/Go1.20/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
@ -249,10 +69,172 @@ build-darwin-qa:
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
.env-darwin:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOROOT=~/local/opt/go@1.20
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH="${GOPATH}/bin:$PATH"
|
||||
- export QT6DIR=/opt/Qt/6.3.2/macos
|
||||
- export PATH="${QT6DIR}/bin:$PATH"
|
||||
- uname -a
|
||||
cache: {}
|
||||
tags:
|
||||
- macos-m1-bridge
|
||||
|
||||
.env-linux-build:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
.script-test:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make test
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/**
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .script-test
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .env-windows
|
||||
- .script-test
|
||||
- .rules-branch-and-MR-manual
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
- .env-darwin
|
||||
- .script-test
|
||||
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
#- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
tags:
|
||||
- small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
- coverage/**
|
||||
when: 'always'
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.script-build:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-linux-build
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-darwin
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-windows:
|
||||
extends:
|
||||
- .build-base
|
||||
- .windows-build-setup
|
||||
- .script-build
|
||||
- .env-windows
|
||||
|
||||
build-windows-qa:
|
||||
extends:
|
||||
|
||||
@ -36,6 +36,14 @@ issues:
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
- path: utils/smtp-send
|
||||
linters:
|
||||
- dupl
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
|
||||
@ -58,7 +58,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
|
||||
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
|
||||
* [goleak](https://go.uber.org/goleak)
|
||||
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
|
||||
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
||||
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
||||
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||
@ -66,16 +66,12 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
||||
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
||||
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
||||
* [atlas](https://ariga.io/atlas)
|
||||
* [ent](https://entgo.io/ent)
|
||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
||||
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
|
||||
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
|
||||
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
|
||||
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
|
||||
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
|
||||
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||
@ -93,7 +89,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
||||
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
|
||||
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
|
||||
* [inflect](https://github.com/go-openapi/inflect) available under [license](https://github.com/go-openapi/inflect/blob/master/LICENSE)
|
||||
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
|
||||
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
|
||||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||
@ -105,7 +100,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
|
||||
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
|
||||
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
|
||||
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
|
||||
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
|
||||
@ -114,7 +108,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
||||
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
|
||||
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
|
||||
* [go-wordwrap](https://github.com/mitchellh/go-wordwrap) available under [license](https://github.com/mitchellh/go-wordwrap/blob/master/LICENSE)
|
||||
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
|
||||
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
|
||||
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||
@ -130,14 +123,13 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/master/LICENSE)
|
||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||
* [genproto](https://google.golang.org/genproto)
|
||||
gopkg.in/yaml.v3
|
||||
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
|
||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||
|
||||
82
Changelog.md
82
Changelog.md
@ -3,6 +3,88 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Trift Bridge 3.4.0
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
* Test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient.
|
||||
* Test: Add smtp-send utility.
|
||||
* GODT-2759: Check for oprhan messages.
|
||||
* GODT-2759: Add prompt to download missing messages for analysis.
|
||||
* GODT-2759: CLI debug commands.
|
||||
* Remove gRPC auto-generated C++ source files.
|
||||
* Test: Force all unit test to use minimum sync spec.
|
||||
* Test: Force sync limits to minimum with env variable.
|
||||
* GODT-2691: Close logrus output file on exit.
|
||||
* GODT-2522: New Gluon database layout.
|
||||
* GODT-2678: When internet is off, do not display status dot icon for the user in the context menu.
|
||||
* GODT-2686: Change the orientation of the expand/collapse arrow for Advanced settings.
|
||||
* Test(GODT-2636): Add step for sending from EML.
|
||||
* Log failed message ids during sync.
|
||||
* GODT-2510: Remove Ent.
|
||||
* Test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests.
|
||||
* GODT-2703: Got rid of account details dialog with Apple Mail autoconf.
|
||||
* GODT-2685: Update to bug report log attachment logic.
|
||||
* GODT-2690: Update sentry reporting in GUI for new log file naming.
|
||||
* GODT-2668: Implemented new log retention policy.
|
||||
* Test(GODT-2683): Save Draft without "Date" & "From" in headers.
|
||||
* GODT-2666: Feat(GODT-2667): introduce sessionID in bridge.
|
||||
* GODT-2660: Calculate bridge coverage and refactor CI yaml file.
|
||||
* Fix dependency_license script to handle dot formated version.
|
||||
|
||||
### Fixed
|
||||
* GODT-2812: Fix rare sync deadlock.
|
||||
* GODT-2822: Better handling 429 during sync and event loop.
|
||||
* GODT-2763: Missing Answered flag on Sync and Message Create.
|
||||
* GODT-2758: Fix panic in SetFlagsOnMessages.
|
||||
* GODT-2578: Refresh literals appended to Sent folder.
|
||||
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
||||
* GODT-2522: Handle migration with unreferenced db values.
|
||||
* GODT-2693: Allow missing whitespace after header field colon.
|
||||
* GODT-2653: Only log when err is not nil.
|
||||
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
|
||||
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
|
||||
|
||||
|
||||
## Stone Bridge 3.3.2
|
||||
|
||||
### Fixed
|
||||
* GODT-2782: Filter all labels when doing perma delete check.
|
||||
|
||||
|
||||
## Stone Bridge 3.3.1
|
||||
|
||||
### Changed
|
||||
* GODT-2707: Set bridge-gui default log level to 'debug'.
|
||||
* GODT-2674: Add more logs during update failed.
|
||||
* GODT-2750: Disable raise on main window when a notification is clicked on Linux.
|
||||
* GODT-2709: Remove the config status file when user is removed.
|
||||
* GODT-2748: Log calls that cause main window to show, with reason.
|
||||
* GODT-2705: Added log entries for focus service on client and server sides.
|
||||
* GODT-2712: Feed config_status with user action while pending.
|
||||
* GODT-2728: Remove the sentry report for gRPC event stream interruptions in bridge-gui.
|
||||
* GODT-2715: Add Unitary test for configStatus event.
|
||||
* GODT-2715: Add Functional test for configStatus telemetry event.
|
||||
* Disable windows runner.
|
||||
* GODT-2714: Apply PR comments.
|
||||
* GODT-2714: Set Configuration Status to Failure and send Recovery event when issue is solved.
|
||||
* GODT-2713: Send config_progress event once a day if the configuration is stucked in pending for more than a day.
|
||||
* GODT-2711: Send config_abort event on User removal.
|
||||
* GODT-2710: Send config success on IMAP/SMTP connection..
|
||||
* GODT-2716: Make Configuration Statistics persistent.
|
||||
* GODT-2709: Init Configuration status.
|
||||
* Log errors on failed message Downloads.
|
||||
|
||||
### Fixed
|
||||
* GODT-2774: Only check telemetry availability for the current user.
|
||||
* GODT-2774: Add external context to telemetry tasks.
|
||||
* GODT-2774: Add context to Authorize in `gluon.Connector`.
|
||||
* GODT-2726: Fix Parsing of Details field in GPA error message.
|
||||
* GODT-2708: Fix dimensions event format + handling of ReportClicked event.
|
||||
* GODT-2756: Fix for 'Settings' context menu opening the 'Help' page.
|
||||
|
||||
|
||||
## Stone Bridge 3.3.0
|
||||
|
||||
### Changed
|
||||
|
||||
21
Makefile
21
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.3.0+git
|
||||
BRIDGE_APP_VERSION?=3.4.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -229,14 +229,28 @@ add-license:
|
||||
change-copyright-year:
|
||||
./utils/missing_license.sh change-year
|
||||
|
||||
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
|
||||
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
|
||||
|
||||
test: gofiles
|
||||
go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
mkdir -p coverage/unit-${GOOS}
|
||||
go test \
|
||||
-v -timeout=20m -p=1 -count=1 \
|
||||
${GOCOVERAGE} \
|
||||
-run=${TESTRUN} ./internal/... ./pkg/... \
|
||||
${GOCOVERDIR}/unit-${GOOS}
|
||||
|
||||
test-race: gofiles
|
||||
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-integration: gofiles
|
||||
go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
mkdir -p coverage/integration
|
||||
go test \
|
||||
-v -timeout=60m -p=1 -count=1 \
|
||||
${GOCOVERAGE} \
|
||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||
${GOCOVERDIR}/integration
|
||||
|
||||
|
||||
test-integration-debug: gofiles
|
||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||
@ -260,6 +274,7 @@ mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
|
||||
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/user MessageDownloader > internal/user/mocks/mocks.go
|
||||
|
||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@ -43,9 +44,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
launcherName = "launcher"
|
||||
|
||||
FlagCLI = "cli"
|
||||
FlagCLIShort = "c"
|
||||
@ -53,6 +55,7 @@ const (
|
||||
FlagNonInteractiveShort = "n"
|
||||
FlagLauncher = "--launcher"
|
||||
FlagWait = "--wait"
|
||||
FlagSessionID = "--session-id"
|
||||
)
|
||||
|
||||
func main() { //nolint:funlen
|
||||
@ -75,12 +78,26 @@ func main() { //nolint:funlen
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get logs path")
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
|
||||
sessionID := logging.NewSessionID()
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
|
||||
|
||||
var closer io.Closer
|
||||
if closer, err = logging.Init(
|
||||
logsPath,
|
||||
sessionID,
|
||||
logging.LauncherShortAppName,
|
||||
logging.DefaultMaxLogFileSize,
|
||||
logging.NoPruning,
|
||||
os.Getenv("VERBOSITY"),
|
||||
); err != nil {
|
||||
l.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = logging.Close(closer)
|
||||
}()
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get updates path")
|
||||
@ -134,7 +151,7 @@ func main() { //nolint:funlen
|
||||
}
|
||||
}
|
||||
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
14
go.mod
14
go.mod
@ -5,9 +5,9 @@ go 1.20
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
@ -51,16 +51,12 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
|
||||
entgo.io/ent v0.11.8 // indirect
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
@ -78,7 +74,6 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
@ -90,7 +85,6 @@ require (
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
@ -98,8 +92,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
@ -115,7 +108,6 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
|
||||
34
go.sum
34
go.sum
@ -1,5 +1,3 @@
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@ -13,13 +11,10 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
|
||||
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@ -28,10 +23,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d h1:+1BKm++zxmfGwj81q3jFkiDpgVwg529qznGbI//uXpk=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3 h1:VMbbJD3dcGPPIgbdQTS5Z4nX0QU/SsVZWdmsMVVBBsI=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611 h1:QVydPr/+pgz5xihc2ujNNV+qnq3oTidIXvF0PgkcY6U=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
@ -42,8 +35,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317 h1:Mo293UO7CfX1mL+jrBoSrftvhEyq6hyRDia2d6+Kx9U=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
||||
@ -54,8 +47,6 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||
@ -63,8 +54,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.m
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
@ -159,8 +148,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
@ -171,7 +158,6 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
@ -248,8 +234,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
@ -282,7 +266,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@ -299,8 +282,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
@ -309,8 +292,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@ -365,7 +346,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
||||
@ -419,8 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY=
|
||||
github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
|
||||
@ -19,6 +19,7 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
@ -76,10 +77,12 @@ const (
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
flagSessionID = "session-id"
|
||||
)
|
||||
|
||||
const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
appShortName = "bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App {
|
||||
@ -150,6 +153,10 @@ func New() *cli.App {
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagSessionID,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
@ -183,6 +190,11 @@ func run(c *cli.Context) error {
|
||||
exe = os.Args[0]
|
||||
}
|
||||
|
||||
var logCloser io.Closer
|
||||
defer func() {
|
||||
_ = logging.Close(logCloser)
|
||||
}()
|
||||
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
@ -199,7 +211,9 @@ func run(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// Initialize logging.
|
||||
return withLogging(c, crashHandler, locations, func() error {
|
||||
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
|
||||
logCloser = closer
|
||||
|
||||
// If there was an error during migration, log it now.
|
||||
if migrationErr != nil {
|
||||
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
||||
@ -298,7 +312,7 @@ func withSingleInstance(settingPath, lockFile string, version *semver.Version, f
|
||||
}
|
||||
|
||||
// Initialize our logging system.
|
||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
|
||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
|
||||
logrus.Debug("Initializing logging")
|
||||
defer logrus.Debug("Logging stopped")
|
||||
|
||||
@ -311,12 +325,21 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||
|
||||
// Initialize logging.
|
||||
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
|
||||
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
|
||||
var closer io.Closer
|
||||
if closer, err = logging.Init(
|
||||
logsPath,
|
||||
sessionID,
|
||||
logging.BridgeShortAppName,
|
||||
logging.DefaultMaxLogFileSize,
|
||||
logging.DefaultPruningSize,
|
||||
c.String(flagLogLevel),
|
||||
); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we dump a stack trace if we crash.
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
|
||||
|
||||
logrus.
|
||||
WithField("appName", constants.FullAppName).
|
||||
@ -329,7 +352,7 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
||||
WithField("SentryID", sentry.GetProtectedHostname()).
|
||||
Info("Run app")
|
||||
|
||||
return fn()
|
||||
return fn(closer)
|
||||
}
|
||||
|
||||
// WithLocations provides access to locations where we store our files.
|
||||
|
||||
@ -44,7 +44,7 @@ import (
|
||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||
|
||||
// withBridge creates creates and tears down the bridge.
|
||||
// withBridge creates and tears down the bridge.
|
||||
func withBridge(
|
||||
c *cli.Context,
|
||||
exe string,
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@ -300,8 +301,11 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
|
||||
string(info.BridgePass)),
|
||||
))
|
||||
|
||||
currentUserAgent = b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
|
||||
require.Eventually(t, func() bool {
|
||||
currentUserAgent = b.GetCurrentUserAgent()
|
||||
|
||||
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
|
||||
}, time.Minute, 5*time.Second)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -18,23 +18,19 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxTotalAttachmentSize = 7 * (1 << 20)
|
||||
MaxCompressedFilesCount = 6
|
||||
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
|
||||
DefaultMaxSessionCountForBugReport = 10
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||
@ -50,54 +46,25 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
}
|
||||
}
|
||||
|
||||
var atts []proton.ReportBugAttachment
|
||||
var attachment []proton.ReportBugAttachment
|
||||
|
||||
if attachLogs {
|
||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
})
|
||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
body, err := io.ReadAll(buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
|
||||
// Include bridge logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// Include crash logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// bridge-gui keeps just one small (~ 1kb) log file; we always include it.
|
||||
if len(guiLogs) > 0 {
|
||||
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
|
||||
}
|
||||
|
||||
archive, err := zipFiles(matchFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atts = append(atts, proton.ReportBugAttachment{
|
||||
attachment = append(attachment, proton.ReportBugAttachment{
|
||||
Name: "logs.zip",
|
||||
Filename: "logs.zip",
|
||||
MIMEType: "application/zip",
|
||||
@ -105,6 +72,12 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
})
|
||||
}
|
||||
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.ReportBugSent()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
@ -118,116 +91,5 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
|
||||
Username: account,
|
||||
Email: email,
|
||||
}, atts...)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
||||
logsPath, err := locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(logsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
|
||||
for _, file := range files {
|
||||
if filenameMatchFunc(file.Name()) {
|
||||
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
|
||||
|
||||
return matchFiles, nil
|
||||
}
|
||||
|
||||
type limitedBuffer struct {
|
||||
capacity int
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func newLimitedBuffer(capacity int) *limitedBuffer {
|
||||
return &limitedBuffer{
|
||||
capacity: capacity,
|
||||
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
||||
if len(p)+b.buf.Len() > b.capacity {
|
||||
return 0, ErrSizeTooLarge
|
||||
}
|
||||
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
|
||||
return b.buf.Read(p)
|
||||
}
|
||||
|
||||
func zipFiles(filenames []string) (io.Reader, error) {
|
||||
if len(filenames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := newLimitedBuffer(MaxTotalAttachmentSize)
|
||||
|
||||
w := zip.NewWriter(buf)
|
||||
defer w.Close() //nolint:errcheck
|
||||
|
||||
for _, file := range filenames {
|
||||
if err := addFileToZip(file, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func addFileToZip(filename string, writer *zip.Writer) error {
|
||||
fileReader, err := os.Open(filepath.Clean(filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileReader.Close() //nolint:errcheck,gosec
|
||||
|
||||
fileInfo, err := fileReader.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Method = zip.Deflate
|
||||
header.Name = filepath.Base(filename)
|
||||
|
||||
fileWriter, err := writer.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(fileWriter, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fileReader.Close()
|
||||
}, attachment...)
|
||||
}
|
||||
|
||||
46
internal/bridge/config_status.go
Normal file
46
internal/bridge/config_status.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBugClicked() {
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.ReportBugClicked()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.AutoconfigUsed(client)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.KBArticleOpened(article)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
297
internal/bridge/debug.go
Normal file
297
internal/bridge/debug.go
Normal file
@ -0,0 +1,297 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
goimap "github.com/emersion/go-imap"
|
||||
goimapclient "github.com/emersion/go-imap/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type CheckClientStateResult struct {
|
||||
MissingMessages map[string]map[string]user.DiagMailboxMessage
|
||||
}
|
||||
|
||||
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
|
||||
v, ok := c.MissingMessages[userID]
|
||||
if !ok {
|
||||
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
|
||||
} else {
|
||||
v[message.ID] = message
|
||||
}
|
||||
}
|
||||
|
||||
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
|
||||
// anything that is out of place.
|
||||
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) (CheckClientStateResult, error) {
|
||||
bridge.usersLock.RLock()
|
||||
defer bridge.usersLock.RUnlock()
|
||||
|
||||
users := maps.Values(bridge.users)
|
||||
|
||||
result := CheckClientStateResult{
|
||||
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
|
||||
}
|
||||
|
||||
for _, usr := range users {
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
|
||||
}
|
||||
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check")
|
||||
log.Debug("Retrieving all server metadata")
|
||||
meta, err := usr.GetDiagnosticMetadata(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
success := true
|
||||
|
||||
if len(meta.Metadata) != len(meta.MessageIDs) {
|
||||
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
|
||||
}
|
||||
|
||||
log.Debug("Building state")
|
||||
state, err := meta.BuildMailboxToMessageMap(usr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to build state")
|
||||
return result, err
|
||||
}
|
||||
|
||||
info, err := bridge.GetUserInfo(usr.ID())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to get user info")
|
||||
return result, err
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
|
||||
|
||||
for account, mboxMap := range state {
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
|
||||
}
|
||||
if err := func(account string, mboxMap user.AccountMailboxMap) error {
|
||||
client, err := goimapclient.Dial(addr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to connect to imap client")
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = client.Logout()
|
||||
}()
|
||||
|
||||
if err := client.Login(account, string(info.BridgePass)); err != nil {
|
||||
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
|
||||
}
|
||||
|
||||
log := log.WithField("account", account)
|
||||
for mboxName, messageList := range mboxMap {
|
||||
log := log.WithField("mbox", mboxName)
|
||||
status, err := client.Select(mboxName, true)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
|
||||
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
|
||||
}
|
||||
|
||||
log.Debug("Checking message count")
|
||||
|
||||
if int(status.Messages) != len(messageList) {
|
||||
success = false
|
||||
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
|
||||
}
|
||||
|
||||
ids, err := clientGetMessageIDs(client, mboxName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
|
||||
}
|
||||
|
||||
for _, msg := range messageList {
|
||||
imapFlags, ok := ids[msg.ID]
|
||||
if !ok {
|
||||
if meta.FailedMessageIDs.Contains(msg.ID) {
|
||||
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
|
||||
} else {
|
||||
log.Errorf("Missing message '%v'", msg.ID)
|
||||
}
|
||||
|
||||
result.AddMissingMessage(msg.UserID, msg)
|
||||
continue
|
||||
}
|
||||
|
||||
if checkFlags {
|
||||
if !imapFlags.Equals(msg.Flags) {
|
||||
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
|
||||
msg.ID,
|
||||
imapFlags.ToSlice(),
|
||||
msg.Flags.ToSlice(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
log.Errorf("State does not match")
|
||||
} else {
|
||||
log.Info("State matches")
|
||||
}
|
||||
|
||||
return nil
|
||||
}(account, mboxMap); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned messages (only present in All Mail)
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
|
||||
}
|
||||
log.Debugf("Checking for orphans")
|
||||
|
||||
for _, m := range meta.Metadata {
|
||||
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
|
||||
switch t {
|
||||
case proton.AllMailLabel:
|
||||
return false
|
||||
case proton.AllSentLabel:
|
||||
return false
|
||||
case proton.AllDraftsLabel:
|
||||
return false
|
||||
case proton.OutboxLabel:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if len(filteredLabels) == 0 {
|
||||
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) DebugDownloadFailedMessages(
|
||||
ctx context.Context,
|
||||
result CheckClientStateResult,
|
||||
exportPath string,
|
||||
progressCB func(string, int, int),
|
||||
) error {
|
||||
bridge.usersLock.RLock()
|
||||
defer bridge.usersLock.RUnlock()
|
||||
|
||||
for userID, messages := range result.MissingMessages {
|
||||
usr, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to find user with id %v", userID)
|
||||
}
|
||||
|
||||
userDir := filepath.Join(exportPath, userID)
|
||||
if err := os.MkdirAll(userDir, 0o700); err != nil {
|
||||
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
|
||||
}
|
||||
|
||||
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
||||
status, err := client.Select(mailbox, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.Messages == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resCh := make(chan *goimap.Message)
|
||||
|
||||
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
|
||||
|
||||
seq, err := goimap.ParseSeqSet("1:*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := client.Fetch(
|
||||
seq,
|
||||
fetchItems,
|
||||
resCh,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
messages := iterator.Collect(iterator.Chan(resCh))
|
||||
|
||||
ids := make(map[string]imap.FlagSet, len(messages))
|
||||
|
||||
for i, m := range messages {
|
||||
literal, err := io.ReadAll(m.GetBody(section))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := rfc822.NewHeader(literal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
|
||||
}
|
||||
|
||||
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
|
||||
if !ok {
|
||||
logrus.Errorf("Message %v does not have internal id", internalID)
|
||||
continue
|
||||
}
|
||||
|
||||
messageFlags := imap.NewFlagSet(m.Flags...)
|
||||
|
||||
// Recent and Deleted are not part of the proton flag set.
|
||||
messageFlags.RemoveFromSelf("\\Recent")
|
||||
messageFlags.RemoveFromSelf("\\Deleted")
|
||||
|
||||
ids[internalID] = messageFlags
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
175
internal/bridge/draft_test.go
Normal file
175
internal/bridge/draft_test.go
Normal file
@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
go_imap "github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
|
||||
getGluonHeaderID := func(literal []byte) (string, string) {
|
||||
h, err := rfc822.NewHeader(literal)
|
||||
require.NoError(t, err)
|
||||
|
||||
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
|
||||
require.True(t, ok)
|
||||
|
||||
externalID, ok := h.GetChecked("Message-Id")
|
||||
require.True(t, ok)
|
||||
|
||||
return gluonID, externalID
|
||||
}
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = s.CreateUser("bar", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The initial user should be fully synced.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
waiter := waitForIMAPServerReady(b)
|
||||
defer waiter.Done()
|
||||
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
waiter.Wait()
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
// Create first draft in client.
|
||||
literal := fmt.Sprintf(`From: %v
|
||||
To: %v
|
||||
Date: Fri, 3 Feb 2023 01:04:32 +0100
|
||||
Subject: Foo
|
||||
|
||||
Hello
|
||||
`, info.Addresses[0], "bar@proton.local")
|
||||
|
||||
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
|
||||
// Verify the draft is available in client.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
|
||||
require.NoError(t, err)
|
||||
return status.Messages == 1
|
||||
}, 2*time.Second, time.Second)
|
||||
|
||||
// Retrieve the new literal so we can have the Proton Message ID.
|
||||
messages, err := clientFetch(client, "Drafts")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(messages))
|
||||
|
||||
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||
require.NoError(t, err)
|
||||
logrus.Info(string(newLiteral))
|
||||
|
||||
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
|
||||
|
||||
// Modify new literal.
|
||||
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
|
||||
|
||||
func() {
|
||||
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = smtpClient.Close() }()
|
||||
|
||||
// Upgrade to TLS.
|
||||
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||
|
||||
// Authorize with SASL PLAIN.
|
||||
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||
info.Addresses[0],
|
||||
info.Addresses[0],
|
||||
string(info.BridgePass)),
|
||||
))
|
||||
|
||||
// Send the message.
|
||||
require.NoError(t, smtpClient.SendMail(
|
||||
info.Addresses[0],
|
||||
[]string{"bar@proton.local"},
|
||||
bytes.NewReader(newLiteralModified),
|
||||
))
|
||||
}()
|
||||
|
||||
// Append message to Sent as the imap client would.
|
||||
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
|
||||
|
||||
// Verify the sent message gets updated with the new literal.
|
||||
require.Eventually(t, func() bool {
|
||||
// Check if sent message matches the latest draft.
|
||||
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(messagesClient1) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||
require.NoError(t, err)
|
||||
|
||||
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
|
||||
|
||||
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
|
||||
require.NoError(t, err)
|
||||
|
||||
sentLiteralStr := string(sentLiteralText)
|
||||
|
||||
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
|
||||
|
||||
idIsDifferent := sentLiteralID != newLiteralID
|
||||
|
||||
externIDMatches := sentLiteralExternID == newLiteralExternID
|
||||
|
||||
return literalMatches && idIsDifferent && externIDMatches
|
||||
}, 2*time.Second, time.Second)
|
||||
})
|
||||
}, server.WithMessageDedup())
|
||||
}
|
||||
@ -32,7 +32,7 @@ import (
|
||||
|
||||
const HeartbeatCheckInterval = time.Hour
|
||||
|
||||
func (bridge *Bridge) IsTelemetryAvailable() bool {
|
||||
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
|
||||
var flag = true
|
||||
if bridge.GetTelemetryDisabled() {
|
||||
return false
|
||||
@ -40,14 +40,14 @@ func (bridge *Bridge) IsTelemetryAvailable() bool {
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
flag = flag && user.IsTelemetryEnabled(context.Background())
|
||||
flag = flag && user.IsTelemetryEnabled(ctx)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
return flag
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
|
||||
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
|
||||
data, err := json.Marshal(heartbeat)
|
||||
if err != nil {
|
||||
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
|
||||
@ -62,7 +62,7 @@ func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
if err := user.SendTelemetry(context.Background(), data); err == nil {
|
||||
if err := user.SendTelemetry(ctx, data); err == nil {
|
||||
sent = true
|
||||
break
|
||||
}
|
||||
@ -87,7 +87,7 @@ func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
|
||||
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
|
||||
logrus.Debug("Checking for heartbeat")
|
||||
|
||||
bridge.heartbeat.TrySending()
|
||||
bridge.heartbeat.TrySending(ctx)
|
||||
})
|
||||
|
||||
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
|
||||
|
||||
@ -50,7 +50,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
|
||||
|
||||
// this is called at start of heartbeat process.
|
||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable().AnyTimes()
|
||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||
|
||||
return mocks
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
@ -50,31 +51,31 @@ func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call
|
||||
}
|
||||
|
||||
// IsTelemetryAvailable mocks base method.
|
||||
func (m *MockHeartbeatManager) IsTelemetryAvailable() bool {
|
||||
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsTelemetryAvailable")
|
||||
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
|
||||
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call {
|
||||
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable))
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
|
||||
}
|
||||
|
||||
// SendHeartbeat mocks base method.
|
||||
func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool {
|
||||
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendHeartbeat", arg0)
|
||||
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SendHeartbeat indicates an expected call of SendHeartbeat.
|
||||
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call {
|
||||
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
|
||||
}
|
||||
|
||||
// SetLastHeartbeatSent mocks base method.
|
||||
|
||||
@ -297,10 +297,11 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
||||
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
|
||||
// which we need at next startup to decrypt the vault.
|
||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
useTelemetry := !bridge.GetTelemetryDisabled()
|
||||
// Delete all the users.
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
bridge.logoutUser(ctx, user, true, true, useTelemetry)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@ -60,6 +61,9 @@ func (s *smtpSession) AuthPlain(username, password string) error {
|
||||
if strings.Contains(s.Bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
|
||||
s.Bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
|
||||
}
|
||||
|
||||
user.SendConfigStatusSuccess(context.Background())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -67,8 +71,16 @@ func (s *smtpSession) AuthPlain(username, password string) error {
|
||||
"username": username,
|
||||
"pkg": "smtp",
|
||||
}).Error("Incorrect login credentials.")
|
||||
|
||||
return fmt.Errorf("invalid username or password")
|
||||
err := fmt.Errorf("invalid username or password")
|
||||
for _, user := range s.users {
|
||||
for _, mail := range user.Emails() {
|
||||
if mail == username {
|
||||
user.ReportConfigStatusFailure(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}, s.usersLock)
|
||||
}
|
||||
|
||||
|
||||
@ -399,6 +399,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
|
||||
}
|
||||
|
||||
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
|
||||
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
|
||||
}
|
||||
|
||||
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -417,6 +421,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
var msgFlags proton.MessageFlag
|
||||
if flags == 0 {
|
||||
msgFlags = proton.MessageFlagReceived
|
||||
} else {
|
||||
msgFlags = flags
|
||||
}
|
||||
|
||||
str, err := c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
@ -427,7 +438,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrID,
|
||||
LabelIDs: []string{labelID},
|
||||
Flags: proton.MessageFlagReceived,
|
||||
Flags: msgFlags,
|
||||
},
|
||||
Message: message,
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ type Locator interface {
|
||||
ProvideLogsPath() (string, error)
|
||||
ProvideGluonCachePath() (string, error)
|
||||
ProvideGluonDataPath() (string, error)
|
||||
ProvideStatsPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear(...string) error
|
||||
|
||||
@ -229,7 +229,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -249,7 +249,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
}
|
||||
|
||||
if user, ok := bridge.users[userID]; ok {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
||||
}
|
||||
|
||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||
@ -351,7 +351,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -519,6 +519,11 @@ func (bridge *Bridge) addUserWithVault(
|
||||
apiUser proton.User,
|
||||
vault *vault.User,
|
||||
) error {
|
||||
statsPath, err := bridge.locator.ProvideStatsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
||||
}
|
||||
|
||||
user, err := user.New(
|
||||
ctx,
|
||||
vault,
|
||||
@ -528,6 +533,8 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.panicHandler,
|
||||
bridge.vault.GetShowAllMail(),
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
statsPath,
|
||||
bridge,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
@ -588,9 +595,14 @@ func (bridge *Bridge) newVaultUser(
|
||||
}
|
||||
|
||||
// logout logs out the given user, optionally logging them out from the API too.
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
|
||||
defer delete(bridge.users, user.ID())
|
||||
|
||||
// if this is actually a remove account
|
||||
if withData && withAPI {
|
||||
user.SendConfigStatusAbort(ctx, withTelemetry)
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withAPI": withAPI,
|
||||
|
||||
@ -166,7 +166,8 @@ func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User,
|
||||
|
||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
safe.Lock(func() {
|
||||
bridge.logoutUser(ctx, user, false, false)
|
||||
bridge.logoutUser(ctx, user, false, false, false)
|
||||
user.ReportConfigStatusFailure("User deauth.")
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
|
||||
@ -70,9 +70,11 @@ func prepareMobileConfig(
|
||||
password []byte,
|
||||
) *mobileconfig.Config {
|
||||
return &mobileconfig.Config{
|
||||
DisplayName: username,
|
||||
EmailAddress: addresses,
|
||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
DisplayName: username,
|
||||
EmailAddress: addresses,
|
||||
AccountName: username,
|
||||
AccountDescription: username,
|
||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
IMAP: &mobileconfig.IMAP{
|
||||
Hostname: hostname,
|
||||
Port: imapPort,
|
||||
|
||||
221
internal/configstatus/config_status.go
Normal file
221
internal/configstatus/config_status.go
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const version = "1.0.0"
|
||||
|
||||
func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
|
||||
status := ConfigurationStatus{
|
||||
FilePath: filepath,
|
||||
DataLock: safe.NewRWMutex(),
|
||||
Data: &ConfigurationStatusData{},
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath); err == nil {
|
||||
if err := status.Load(); err == nil {
|
||||
return &status, nil
|
||||
}
|
||||
logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.")
|
||||
}
|
||||
|
||||
status.Data.init()
|
||||
if err := status.Save(); err != nil {
|
||||
return &status, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Load() error {
|
||||
bytes, err := os.ReadFile(status.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var metadata MetadataOnly
|
||||
if err := json.Unmarshal(bytes, &metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if metadata.Metadata.Version != version {
|
||||
return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version)
|
||||
}
|
||||
|
||||
return json.Unmarshal(bytes, status.Data)
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Save() error {
|
||||
temp := status.FilePath + "_temp"
|
||||
f, err := os.Create(temp) //nolint:gosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
err = enc.Encode(status.Data)
|
||||
if err := f.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Error while closing configstatus file.")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(temp, status.FilePath)
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) IsPending() bool {
|
||||
status.DataLock.RLock()
|
||||
defer status.DataLock.RUnlock()
|
||||
|
||||
return !status.Data.DataV1.PendingSince.IsZero()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) IsFromFailure() bool {
|
||||
status.DataLock.RLock()
|
||||
defer status.DataLock.RUnlock()
|
||||
|
||||
return status.Data.DataV1.FailureDetails != ""
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ApplySuccess() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
status.Data.init()
|
||||
status.Data.DataV1.PendingSince = time.Time{}
|
||||
return status.Save()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ApplyFailure(err string) error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
status.Data.init()
|
||||
status.Data.DataV1.FailureDetails = err
|
||||
return status.Save()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ApplyProgress() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
status.Data.DataV1.LastProgress = time.Now()
|
||||
return status.Save()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if !status.Data.hasLinkClicked(link) {
|
||||
status.Data.setClickedLink(link)
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ReportClicked() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if !status.Data.DataV1.ReportClick {
|
||||
status.Data.DataV1.ReportClick = true
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ReportSent() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if !status.Data.DataV1.ReportSent {
|
||||
status.Data.DataV1.ReportSent = true
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) AutoconfigUsed(client string) error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if client != status.Data.DataV1.Autoconf {
|
||||
status.Data.DataV1.Autoconf = client
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Remove() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
return os.Remove(status.FilePath)
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) init() {
|
||||
data.Metadata = Metadata{
|
||||
Version: version,
|
||||
}
|
||||
data.DataV1.PendingSince = time.Now()
|
||||
data.DataV1.LastProgress = time.Time{}
|
||||
data.DataV1.Autoconf = ""
|
||||
data.DataV1.ClickedLink = 0
|
||||
data.DataV1.ReportSent = false
|
||||
data.DataV1.ReportClick = false
|
||||
data.DataV1.FailureDetails = ""
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) setClickedLink(pos uint) {
|
||||
data.DataV1.ClickedLink |= 1 << pos
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
|
||||
val := data.DataV1.ClickedLink & (1 << pos)
|
||||
return val > 0
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) clickedLinkToString() string {
|
||||
var str = ""
|
||||
var first = true
|
||||
for i := 0; i < 64; i++ {
|
||||
if data.hasLinkClicked(uint(i)) {
|
||||
if !first {
|
||||
str += ","
|
||||
} else {
|
||||
first = false
|
||||
str += "["
|
||||
}
|
||||
str += strconv.Itoa(i)
|
||||
}
|
||||
}
|
||||
if str != "" {
|
||||
str += "]"
|
||||
}
|
||||
return str
|
||||
}
|
||||
252
internal/configstatus/config_status_test.go
Normal file
252
internal/configstatus/config_status_test.go
Normal file
@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigStatus_init_virgin(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
|
||||
|
||||
require.Equal(t, "", config.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_init_existing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
require.Equal(t, "Mr TBird", config.Data.DataV1.Autoconf)
|
||||
}
|
||||
|
||||
func TestConfigStatus_init_bad_version(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "2.0.0"},
|
||||
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
require.Equal(t, "", config.Data.DataV1.Autoconf)
|
||||
}
|
||||
|
||||
func TestConfigStatus_IsPending(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
config.Data.DataV1.PendingSince = time.Time{}
|
||||
require.Equal(t, false, config.IsPending())
|
||||
}
|
||||
|
||||
func TestConfigStatus_IsFromFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.IsFromFailure())
|
||||
config.Data.DataV1.FailureDetails = "test"
|
||||
require.Equal(t, true, config.IsFromFailure())
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplySuccess(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
require.NoError(t, config.ApplySuccess())
|
||||
require.Equal(t, false, config.IsPending())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, true, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplyFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, config.ApplySuccess())
|
||||
|
||||
require.NoError(t, config.ApplyFailure("Big Failure"))
|
||||
require.Equal(t, true, config.IsFromFailure())
|
||||
require.Equal(t, true, config.IsPending())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "Big Failure", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplyProgress(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
|
||||
|
||||
require.NoError(t, config.ApplyProgress())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, false, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_RecordLinkClicked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
|
||||
require.NoError(t, config.RecordLinkClicked(0))
|
||||
require.Equal(t, uint64(1), config.Data.DataV1.ClickedLink)
|
||||
require.NoError(t, config.RecordLinkClicked(1))
|
||||
require.Equal(t, uint64(3), config.Data.DataV1.ClickedLink)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(3), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ReportClicked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.ReportClick)
|
||||
require.NoError(t, config.ReportClicked())
|
||||
require.Equal(t, true, config.Data.DataV1.ReportClick)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, true, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ReportSent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.ReportSent)
|
||||
require.NoError(t, config.ReportSent())
|
||||
require.Equal(t, true, config.Data.DataV1.ReportSent)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, true, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func dumpConfigStatusInFile(data *configstatus.ConfigurationStatusData, file string) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
return json.NewEncoder(f).Encode(data)
|
||||
}
|
||||
57
internal/configstatus/configuration_abort.go
Normal file
57
internal/configstatus/configuration_abort.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigAbortValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigAbortDimensions struct {
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
}
|
||||
|
||||
type ConfigAbortData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigSuccessValues
|
||||
Dimensions ConfigSuccessDimensions
|
||||
}
|
||||
|
||||
type ConfigAbortBuilder struct{}
|
||||
|
||||
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
||||
return ConfigAbortData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_abort",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
75
internal/configstatus/configuration_abort_test.go
Normal file
75
internal/configstatus/configuration_abort_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationAbort_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
}
|
||||
|
||||
func TestConfigurationAbort_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
}
|
||||
60
internal/configstatus/configuration_progress.go
Normal file
60
internal/configstatus/configuration_progress.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import "time"
|
||||
|
||||
type ConfigProgressValues struct {
|
||||
NbDay int `json:"nb_day"`
|
||||
NbDaySinceLast int `json:"nb_day_since_last"`
|
||||
}
|
||||
|
||||
type ConfigProgressData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigProgressValues
|
||||
Dimensions struct{}
|
||||
}
|
||||
|
||||
type ConfigProgressBuilder struct{}
|
||||
|
||||
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
|
||||
return ConfigProgressData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_progress",
|
||||
Values: ConfigProgressValues{
|
||||
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
||||
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfDay(now, prev time.Time) int {
|
||||
if now.IsZero() || prev.IsZero() {
|
||||
return 1
|
||||
}
|
||||
if now.Year() > prev.Year() {
|
||||
if now.YearDay() > prev.YearDay() {
|
||||
return 365 + (now.YearDay() - prev.YearDay())
|
||||
}
|
||||
return (prev.YearDay() + now.YearDay()) - 365
|
||||
} else if now.YearDay() > prev.YearDay() {
|
||||
return now.YearDay() - prev.YearDay()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
71
internal/configstatus/configuration_progress_test.go
Normal file
71
internal/configstatus/configuration_progress_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationProgress_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
require.Equal(t, 0, req.Values.NbDay)
|
||||
require.Equal(t, 1, req.Values.NbDaySinceLast)
|
||||
}
|
||||
|
||||
func TestConfigurationProgress_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().AddDate(0, 0, -5),
|
||||
LastProgress: time.Now().AddDate(0, 0, -2),
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
require.Equal(t, 5, req.Values.NbDay)
|
||||
require.Equal(t, 2, req.Values.NbDaySinceLast)
|
||||
}
|
||||
61
internal/configstatus/configuration_recovery.go
Normal file
61
internal/configstatus/configuration_recovery.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigRecoveryValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigRecoveryDimensions struct {
|
||||
Autoconf string `json:"autoconf"`
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
FailureDetails string `json:"failure_details"`
|
||||
}
|
||||
|
||||
type ConfigRecoveryData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigRecoveryValues
|
||||
Dimensions ConfigRecoveryDimensions
|
||||
}
|
||||
|
||||
type ConfigRecoveryBuilder struct{}
|
||||
|
||||
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
|
||||
return ConfigRecoveryData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_recovery",
|
||||
Values: ConfigRecoveryValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
},
|
||||
Dimensions: ConfigRecoveryDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
FailureDetails: data.DataV1.FailureDetails,
|
||||
},
|
||||
}
|
||||
}
|
||||
79
internal/configstatus/configuration_recovery_test.go
Normal file
79
internal/configstatus/configuration_recovery_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationRecovery_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
require.Equal(t, "", req.Dimensions.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigurationRecovery_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
|
||||
}
|
||||
59
internal/configstatus/configuration_success.go
Normal file
59
internal/configstatus/configuration_success.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigSuccessValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigSuccessDimensions struct {
|
||||
Autoconf string `json:"autoconf"`
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
}
|
||||
|
||||
type ConfigSuccessData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigSuccessValues
|
||||
Dimensions ConfigSuccessDimensions
|
||||
}
|
||||
|
||||
type ConfigSuccessBuilder struct{}
|
||||
|
||||
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
|
||||
return ConfigSuccessData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_success",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
77
internal/configstatus/configuration_success_test.go
Normal file
77
internal/configstatus/configuration_success_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationSuccess_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
}
|
||||
|
||||
func TestConfigurationSuccess_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
}
|
||||
56
internal/configstatus/types_config_status.go
Normal file
56
internal/configstatus/types_config_status.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
)
|
||||
|
||||
const ProgressCheckInterval = time.Hour
|
||||
|
||||
type Metadata struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type MetadataOnly struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type DataV1 struct {
|
||||
PendingSince time.Time `json:"pending_since"`
|
||||
LastProgress time.Time `json:"last_progress"`
|
||||
Autoconf string `json:"auto_conf"`
|
||||
ClickedLink uint64 `json:"clicked_link"`
|
||||
ReportSent bool `json:"report_sent"`
|
||||
ReportClick bool `json:"report_click"`
|
||||
FailureDetails string `json:"failure_details"`
|
||||
}
|
||||
|
||||
type ConfigurationStatusData struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
DataV1 DataV1 `json:"dataV1"`
|
||||
}
|
||||
|
||||
type ConfigurationStatus struct {
|
||||
FilePath string
|
||||
DataLock safe.RWMutex
|
||||
|
||||
Data *ConfigurationStatusData
|
||||
}
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
// TryRaise tries to raise the application by dialing the focus service.
|
||||
@ -38,7 +39,7 @@ func TryRaise(settingsPath string) bool {
|
||||
var raised bool
|
||||
|
||||
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
||||
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
|
||||
if _, err := client.Raise(ctx, &wrapperspb.StringValue{Value: "TryRaise"}); err != nil {
|
||||
return fmt.Errorf("failed to call client.Raise: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.21.3
|
||||
// protoc v3.21.12
|
||||
// source: focus.proto
|
||||
|
||||
package proto
|
||||
@ -27,6 +27,7 @@ import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
@ -91,22 +92,24 @@ var file_focus_proto_rawDesc = []byte{
|
||||
0x0a, 0x0b, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x66,
|
||||
0x6f, 0x63, 0x75, 0x73, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||
0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x22, 0x2b, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x7b,
|
||||
0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73, 0x65,
|
||||
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
|
||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
|
||||
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
|
||||
0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
|
||||
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x81,
|
||||
0x01, 0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73,
|
||||
0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a,
|
||||
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63,
|
||||
0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@ -123,13 +126,14 @@ func file_focus_proto_rawDescGZIP() []byte {
|
||||
|
||||
var file_focus_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_focus_proto_goTypes = []interface{}{
|
||||
(*VersionResponse)(nil), // 0: focus.VersionResponse
|
||||
(*emptypb.Empty)(nil), // 1: google.protobuf.Empty
|
||||
(*VersionResponse)(nil), // 0: focus.VersionResponse
|
||||
(*wrapperspb.StringValue)(nil), // 1: google.protobuf.StringValue
|
||||
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
|
||||
}
|
||||
var file_focus_proto_depIdxs = []int32{
|
||||
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.Empty
|
||||
1, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
|
||||
1, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
|
||||
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.StringValue
|
||||
2, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
|
||||
2, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
|
||||
0, // 3: focus.Focus.Version:output_type -> focus.VersionResponse
|
||||
2, // [2:4] is the sub-list for method output_type
|
||||
0, // [0:2] is the sub-list for method input_type
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
option go_package = "github.com/ProtonMail/proton-bridge/v3/internal/focus/proto";
|
||||
|
||||
@ -27,7 +28,7 @@ package focus; // ignored by Go, used as namespace name in C++.
|
||||
// Service Declaration
|
||||
//**********************************************************************************************************************≠––
|
||||
service Focus {
|
||||
rpc Raise(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
rpc Raise(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
rpc Version(google.protobuf.Empty) returns (VersionResponse);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.3
|
||||
// - protoc v3.21.12
|
||||
// source: focus.proto
|
||||
|
||||
package proto
|
||||
@ -12,6 +12,7 @@ import (
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
@ -23,7 +24,7 @@ const _ = grpc.SupportPackageIsVersion7
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type FocusClient interface {
|
||||
Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error)
|
||||
}
|
||||
|
||||
@ -35,7 +36,7 @@ func NewFocusClient(cc grpc.ClientConnInterface) FocusClient {
|
||||
return &focusClient{cc}
|
||||
}
|
||||
|
||||
func (c *focusClient) Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
|
||||
if err != nil {
|
||||
@ -57,7 +58,7 @@ func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...gr
|
||||
// All implementations must embed UnimplementedFocusServer
|
||||
// for forward compatibility
|
||||
type FocusServer interface {
|
||||
Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
|
||||
Version(context.Context, *emptypb.Empty) (*VersionResponse, error)
|
||||
mustEmbedUnimplementedFocusServer()
|
||||
}
|
||||
@ -66,7 +67,7 @@ type FocusServer interface {
|
||||
type UnimplementedFocusServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedFocusServer) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
func (UnimplementedFocusServer) Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Raise not implemented")
|
||||
}
|
||||
func (UnimplementedFocusServer) Version(context.Context, *emptypb.Empty) (*VersionResponse, error) {
|
||||
@ -86,7 +87,7 @@ func RegisterFocusServer(s grpc.ServiceRegistrar, srv FocusServer) {
|
||||
}
|
||||
|
||||
func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
in := new(wrapperspb.StringValue)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -98,7 +99,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
FullMethod: "/focus.Focus/Raise",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(FocusServer).Raise(ctx, req.(*emptypb.Empty))
|
||||
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -45,6 +46,7 @@ type Service struct {
|
||||
raiseCh chan struct{}
|
||||
version *semver.Version
|
||||
|
||||
log *logrus.Entry
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
@ -55,13 +57,14 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
|
||||
server: grpc.NewServer(),
|
||||
raiseCh: make(chan struct{}, 1),
|
||||
version: version,
|
||||
log: logrus.WithField("pkg", "focus/service"),
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
|
||||
proto.RegisterFocusServer(serv.server, serv)
|
||||
|
||||
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to start focus serv")
|
||||
serv.log.WithError(err).Warn("Failed to start focus service")
|
||||
} else {
|
||||
config := service.Config{}
|
||||
// retrieve the port assigned by the system, so that we can put it in the config file.
|
||||
@ -71,9 +74,9 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
|
||||
}
|
||||
config.Port = address.Port
|
||||
if path, err := service.SaveGRPCServerConfigFile(locator, &config, serverConfigFileName); err != nil {
|
||||
logrus.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
|
||||
serv.log.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
|
||||
} else {
|
||||
logrus.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
|
||||
serv.log.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
|
||||
}
|
||||
|
||||
go func() {
|
||||
@ -89,13 +92,15 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
|
||||
}
|
||||
|
||||
// Raise implements the gRPC FocusService interface; it raises the application.
|
||||
func (service *Service) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
func (service *Service) Raise(_ context.Context, reason *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
service.log.WithField("Reason", reason.Value).Debug("Raise")
|
||||
service.raiseCh <- struct{}{}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// Version implements the gRPC FocusService interface; it returns the version of the service.
|
||||
func (service *Service) Version(context.Context, *emptypb.Empty) (*proto.VersionResponse, error) {
|
||||
service.log.Debug("Version")
|
||||
return &proto.VersionResponse{
|
||||
Version: service.version.Original(),
|
||||
}, nil
|
||||
|
||||
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
@ -117,7 +117,27 @@ void AppController::restart(bool isCrashing) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] launcher The launcher.
|
||||
/// \param[in] args The launcher arguments.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
||||
launcher_ = launcher;
|
||||
launcherArgs_ = args;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] sessionID The sessionID.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::setSessionID(const QString &sessionID) {
|
||||
sessionID_ = sessionID;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The sessionID.
|
||||
//****************************************************************************************************************************************************
|
||||
QString AppController::sessionID() {
|
||||
return sessionID_;
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ class Exception;
|
||||
/// \brief App controller class.
|
||||
//****************************************************************************************************************************************************
|
||||
class AppController : public QObject {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
friend AppController &app();
|
||||
|
||||
public: // member functions.
|
||||
@ -52,10 +52,12 @@ public: // member functions.
|
||||
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||
Settings &settings();; ///< Return the application settings.
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args);
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args); ///< Set the launcher arguments.
|
||||
void setSessionID(QString const &sessionID); ///< Set the sessionID.
|
||||
QString sessionID(); ///< Get the sessionID.
|
||||
|
||||
public slots:
|
||||
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
|
||||
void onFatalError(bridgepp::Exception const &e); ///< Handle fatal errors.
|
||||
|
||||
private: // member functions
|
||||
AppController(); ///< Default constructor.
|
||||
@ -67,8 +69,9 @@ private: // data members
|
||||
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
||||
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
||||
std::unique_ptr<Settings> settings_; ///< The application settings.
|
||||
QString launcher_;
|
||||
QStringList launcherArgs_;
|
||||
QString launcher_; ///< The launcher.
|
||||
QStringList launcherArgs_; ///< The launcher arguments.
|
||||
QString sessionID_; ///< The sessionID.
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
#include "Pch.h"
|
||||
#include "CommandLine.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/SessionID/SessionID.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -142,5 +143,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
||||
|
||||
options.logLevel = parseLogLevel(argc, argv);
|
||||
|
||||
QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" });
|
||||
if (sessionID.isEmpty()) {
|
||||
// The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge
|
||||
sessionID = newSessionID();
|
||||
options.bridgeArgs.append("--session-id");
|
||||
options.bridgeArgs.append(sessionID);
|
||||
}
|
||||
app().setSessionID(sessionID);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -43,12 +43,9 @@ EventStreamReader::EventStreamReader(QObject *parent)
|
||||
void EventStreamReader::run() {
|
||||
try {
|
||||
emit started();
|
||||
|
||||
grpc::Status const status = app().grpc().runEventStreamReader();
|
||||
if (!status.ok()) {
|
||||
throw Exception(QString::fromStdString(status.error_message()));
|
||||
}
|
||||
|
||||
// Status code for the call below is ignored. The event stream may have interrupted by system shutdown or OS user sign-out, and we do not
|
||||
// want this to generate a sentry report.
|
||||
app().grpc().runEventStreamReader();
|
||||
emit finished();
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
#include "LogUtils.h"
|
||||
#include "BuildConfig.h"
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -33,15 +32,10 @@ Log &initLog() {
|
||||
log.registerAsQtMessageHandler();
|
||||
log.setEchoInConsole(true);
|
||||
|
||||
// remove old gui log files
|
||||
QDir const logsDir(userLogsDir());
|
||||
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
|
||||
QFile(fileInfo.absoluteFilePath()).remove();
|
||||
}
|
||||
|
||||
// create new GUI log file
|
||||
QString error;
|
||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
|
||||
if (!log.startWritingToFile(QDir(userLogsDir()).absoluteFilePath(QString("%1_gui_000_v%2_%3.log").arg(app().sessionID(),
|
||||
PROJECT_VER, PROJECT_TAG)), &error)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ using namespace bridgepp;
|
||||
/// \brief handle notification of attempt to re-open the application.
|
||||
//****************************************************************************************************************************************************
|
||||
void applicationShouldHandleReopen(id, SEL) {
|
||||
app().backend().showMainWindow();
|
||||
app().backend().showMainWindow("macOS applicationShouldHandleReopen notification");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
||||
app().grpc().setLog(&log);
|
||||
this->connectGrpcEvents();
|
||||
|
||||
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().grpc().connectToServer(app().sessionID(), bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
|
||||
QString bridgeVer;
|
||||
@ -109,6 +109,52 @@ UserList const &QMLBackend::users() const {
|
||||
return *users_;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the if bridge considers internet is on.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::isInternetOn() const {
|
||||
return isInternetOn_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::showMainWindow(QString const&reason) {
|
||||
app().log().debug(QString("main window show requested: %1").arg(reason));
|
||||
emit showMainWindow();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::showHelp(QString const&reason) {
|
||||
app().log().debug(QString("main window show requested (help page): %1").arg(reason));
|
||||
emit showHelp();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::showSettings(QString const&reason) {
|
||||
app().log().debug(QString("main window show requested (settings page): %1").arg(reason));
|
||||
emit showSettings();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] forceShowWindow Should the window be force to display.
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::selectUser(QString const &userID, bool forceShowWindow, QString const &reason) {
|
||||
if (forceShowWindow) {
|
||||
app().log().debug(QString("main window show requested (user page): %1").arg(reason));
|
||||
}
|
||||
emit selectUser(userID, forceShowWindow);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The build year as a string (e.g. 2023)
|
||||
@ -640,7 +686,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
|
||||
HANDLE_EXCEPTION(
|
||||
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
||||
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
||||
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
|
||||
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
|
||||
}
|
||||
app().grpc().login(username, password);
|
||||
)
|
||||
@ -874,11 +920,36 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
|
||||
if (!badEventDisplayQueue_.isEmpty()) {
|
||||
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
|
||||
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
///
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyReportBugClicked() const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().reportBugClicked();
|
||||
)
|
||||
}
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] client The selected Mail client for autoconfig.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().autoconfigClicked(client);
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] article The url of the KB article.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().KBArticleClicked(article);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
@ -923,6 +994,25 @@ void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &st
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isOn Does bridge consider internet as on.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::internetStatusChanged(bool isOn) {
|
||||
HANDLE_EXCEPTION(
|
||||
if (isInternetOn_ == isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
isInternetOn_ = isOn;
|
||||
if (isOn) {
|
||||
emit internetOn();
|
||||
} else {
|
||||
emit internetOff();
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] imapPort The IMAP port.
|
||||
/// \param[in] smtpPort The SMTP port.
|
||||
@ -1086,13 +1176,13 @@ void QMLBackend::connectGrpcEvents() {
|
||||
GRPCClient *client = &app().grpc();
|
||||
|
||||
// app events
|
||||
connect(client, &GRPCClient::internetStatus, this, [&](bool isOn) { if (isOn) { emit internetOn(); } else { emit internetOff(); }});
|
||||
connect(client, &GRPCClient::internetStatus, this, &QMLBackend::internetStatusChanged);
|
||||
connect(client, &GRPCClient::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
|
||||
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
|
||||
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
||||
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
||||
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
|
||||
connect(client, &GRPCClient::showMainWindow, this, &QMLBackend::showMainWindow);
|
||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||
|
||||
// cache events
|
||||
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
|
||||
|
||||
@ -45,6 +45,11 @@ public: // member functions.
|
||||
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
|
||||
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
||||
UserList const& users() const; ///< Return the list of users
|
||||
bool isInternetOn() const; ///< Check if bridge considers internet as on.
|
||||
void showMainWindow(QString const &reason); ///< Show the main window.
|
||||
void showHelp(QString const &reason); ///< Show the help page.
|
||||
void showSettings(QString const &reason); ///< Show the settings page.
|
||||
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
|
||||
|
||||
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
|
||||
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
||||
@ -85,7 +90,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
||||
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
|
||||
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
|
||||
|
||||
|
||||
// Qt Property system setters & getters.
|
||||
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
|
||||
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
|
||||
@ -183,6 +187,9 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void onVersionChanged(); ///< Slot for the version change signal.
|
||||
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
||||
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
|
||||
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
|
||||
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
|
||||
void notifyKBArticleClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
||||
|
||||
public slots: // slots for functions that need to be processed locally.
|
||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||
@ -191,6 +198,7 @@ public slots: // slots for functions that need to be processed locally.
|
||||
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
|
||||
|
||||
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
|
||||
void internetStatusChanged(bool isOn); ///< Check if bridge considers internet as on.
|
||||
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
|
||||
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
|
||||
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
||||
@ -273,8 +281,9 @@ private: // data members
|
||||
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
|
||||
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
||||
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
||||
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
std::unique_ptr<TrayIcon> trayIcon_;
|
||||
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
<file>qml/icons/ic-card-identity.svg</file>
|
||||
<file>qml/icons/ic-check.svg</file>
|
||||
<file>qml/icons/ic-chevron-down.svg</file>
|
||||
<file>qml/icons/ic-chevron-right.svg</file>
|
||||
<file>qml/icons/ic-chevron-up.svg</file>
|
||||
<file>qml/icons/ic-cog-wheel.svg</file>
|
||||
<file>qml/icons/ic-connected.svg</file>
|
||||
|
||||
@ -34,7 +34,7 @@ QColor const warnColor(255, 153, 0); ///< The warn state color.
|
||||
QColor const updateColor(35, 158, 206); ///< The warn state color.
|
||||
QColor const greyColor(112, 109, 107); ///< The grey color.
|
||||
qint64 const iconRefreshTimerIntervalMs = 1000; ///< The interval for the refresh timer when switching DPI / screen config, in milliseconds.
|
||||
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during wich we periodically refresh the icon after a DPI change.
|
||||
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during which we periodically refresh the icon after a DPI change.
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
@ -184,10 +184,14 @@ TrayIcon::TrayIcon()
|
||||
this->setContextMenu(menu_.get());
|
||||
|
||||
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
|
||||
connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser);
|
||||
connect(this, &TrayIcon::selectUser, &app().backend(), [](QString const& userID, bool forceShowWindow) {
|
||||
app().backend().selectUser(userID, forceShowWindow);
|
||||
});
|
||||
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
|
||||
// some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually.
|
||||
connect(this, &TrayIcon::messageClicked, &app().backend(), &QMLBackend::showMainWindow);
|
||||
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
|
||||
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
|
||||
}
|
||||
this->show();
|
||||
|
||||
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
||||
@ -226,7 +230,7 @@ void TrayIcon::onUserClicked() {
|
||||
throw Exception("Could not retrieve context menu's selected user.");
|
||||
}
|
||||
|
||||
emit selectUser(userID, true);
|
||||
app().backend().selectUser(userID, true, "tray menu user clicked");
|
||||
} catch (Exception const &e) {
|
||||
app().log().error(e.qwhat());
|
||||
}
|
||||
@ -238,7 +242,7 @@ void TrayIcon::onUserClicked() {
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
|
||||
app().backend().showMainWindow();
|
||||
app().backend().showMainWindow("tray icon activated");
|
||||
}
|
||||
}
|
||||
|
||||
@ -344,8 +348,9 @@ void TrayIcon::refreshContextMenu() {
|
||||
return;
|
||||
}
|
||||
|
||||
bool const internetOn = app().backend().isInternetOn();
|
||||
menu_->clear();
|
||||
menu_->addAction(statusIcon_, stateString_, &app().backend(), &QMLBackend::showMainWindow);
|
||||
menu_->addAction(statusIcon_, stateString_, []() {app().backend().showMainWindow("tray menu status clicked");});
|
||||
menu_->addSeparator();
|
||||
QKeySequence noShortcut;
|
||||
UserList const &users = app().backend().users();
|
||||
@ -355,7 +360,9 @@ void TrayIcon::refreshContextMenu() {
|
||||
User const &user = *users.get(i);
|
||||
UserState const state = user.state();
|
||||
auto action = new QAction(user.primaryEmailOrUsername());
|
||||
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
||||
if (internetOn) {
|
||||
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
||||
}
|
||||
action->setData(user.id());
|
||||
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
|
||||
if ((i < 10) && onMac) {
|
||||
@ -367,11 +374,15 @@ void TrayIcon::refreshContextMenu() {
|
||||
menu_->addSeparator();
|
||||
}
|
||||
|
||||
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, &app().backend(), &QMLBackend::showMainWindow);
|
||||
menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, &app().backend(), &QMLBackend::showHelp);
|
||||
menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, &app().backend(), &QMLBackend::showSettings);
|
||||
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, []() {
|
||||
app().backend().showMainWindow("tray menu 'open bridge' clicked");
|
||||
});
|
||||
menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, []() {
|
||||
app().backend().showHelp("tray menu 'Help' clicked");
|
||||
});
|
||||
menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, []() {
|
||||
app().backend().showSettings("tray menu 'Settings' clicked");
|
||||
});
|
||||
menu_->addSeparator();
|
||||
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -196,7 +196,7 @@ bool isBridgeRunning() {
|
||||
//****************************************************************************************************************************************************
|
||||
void focusOtherInstance() {
|
||||
try {
|
||||
FocusGRPCClient client;
|
||||
FocusGRPCClient client(app().log());
|
||||
GRPCConfig sc;
|
||||
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
|
||||
QFile file(path);
|
||||
@ -209,12 +209,11 @@ void focusOtherInstance() {
|
||||
throw Exception("Server did not provide gRPC Focus service configuration.");
|
||||
}
|
||||
|
||||
|
||||
QString error;
|
||||
if (!client.connectToServer(5000, sc.port, &error)) {
|
||||
throw Exception("Could not connect to bridge focus service for a raise call.", error);
|
||||
}
|
||||
if (!client.raise().ok()) {
|
||||
if (!client.raise("focusOtherInstance").ok()) {
|
||||
throw Exception(QString("The raise call to the bridge focus service failed."));
|
||||
}
|
||||
}
|
||||
@ -286,7 +285,9 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
initQtApplication();
|
||||
|
||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
||||
Log &log = initLog();
|
||||
log.setLevel(cliOptions.logLevel);
|
||||
|
||||
QLockFile lock(bridgepp::userCacheDir() + "/" + bridgeGUILock);
|
||||
if (!checkSingleInstance(lock)) {
|
||||
@ -294,8 +295,6 @@ int main(int argc, char *argv[]) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
registerSecondInstanceHandler();
|
||||
setDockIconVisibleState(!cliOptions.noWindow);
|
||||
@ -304,25 +303,25 @@ int main(int argc, char *argv[]) {
|
||||
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
|
||||
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
|
||||
// these outputs and output them on the command-line.
|
||||
log.setLevel(cliOptions.logLevel);
|
||||
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
|
||||
|
||||
QString bridgeexec;
|
||||
QString const &sessionID = app().sessionID();
|
||||
QString bridgeExe;
|
||||
if (!cliOptions.attach) {
|
||||
if (isBridgeRunning()) {
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||
FocusGRPCClient::removeServiceConfigFile(configDir);
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
bridgeexec = launchBridge(cliOptions.bridgeArgs);
|
||||
bridgeExe = launchBridge(cliOptions.bridgeArgs);
|
||||
}
|
||||
|
||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
|
||||
app().bridgeMonitor()));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(sessionID, configDir,
|
||||
cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||
if (!cliOptions.attach) {
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
}
|
||||
@ -380,9 +379,9 @@ int main(int argc, char *argv[]) {
|
||||
QStringList args = cliOptions.bridgeGuiArgs;
|
||||
args.append(waitFlag);
|
||||
args.append(mainexec);
|
||||
if (!bridgeexec.isEmpty()) {
|
||||
if (!bridgeExe.isEmpty()) {
|
||||
args.append(waitFlag);
|
||||
args.append(bridgeexec);
|
||||
args.append(bridgeExe);
|
||||
}
|
||||
app().setLauncherArgs(cliOptions.launcher, args);
|
||||
result = QGuiApplication::exec();
|
||||
|
||||
@ -90,9 +90,9 @@ SettingsView {
|
||||
|
||||
RowLayout {
|
||||
ColorImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
||||
color: root.colorScheme.interaction_norm
|
||||
height: root.colorScheme.body_font_size
|
||||
sourceSize.height: root.colorScheme.body_font_size
|
||||
|
||||
@ -41,7 +41,9 @@ SettingsView {
|
||||
actionIcon: "/qml/icons/ic-external-link.svg"
|
||||
description: qsTr("Get help setting up your client with our instructions and FAQs.")
|
||||
type: SettingsItem.PrimaryButton
|
||||
onClicked: {Qt.openUrlExternally("https://proton.me/support/bridge")}
|
||||
onClicked: {
|
||||
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
|
||||
Qt.openUrlExternally("https://proton.me/support/bridge")}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@ -87,6 +89,7 @@ SettingsView {
|
||||
type: SettingsItem.Button
|
||||
onClicked: {
|
||||
Backend.updateCurrentMailClient()
|
||||
Backend.notifyReportBugClicked()
|
||||
root.parent.showBugReport()
|
||||
}
|
||||
|
||||
|
||||
@ -891,7 +891,7 @@ QtObject {
|
||||
action: [
|
||||
Action {
|
||||
id: allMail_change
|
||||
text: root.changeAllMailVisibility.isVisibleNow ?
|
||||
text: root.changeAllMailVisibility.isVisibleNow ?
|
||||
qsTr("Hide All Mail folder") :
|
||||
qsTr("Show All Mail folder")
|
||||
onTriggered: {
|
||||
@ -1005,6 +1005,7 @@ QtObject {
|
||||
text: qsTr("Open the support page")
|
||||
|
||||
onTriggered: {
|
||||
Backend.notifyKBArticleClicked(root.rebuildKeychain.supportLink);
|
||||
Qt.openUrlExternally(root.rebuildKeychain.supportLink)
|
||||
Backend.quit()
|
||||
}
|
||||
|
||||
@ -288,7 +288,8 @@ Item {
|
||||
if (user) {
|
||||
switch (clientID) {
|
||||
case 0:
|
||||
root.user.configureAppleMail(root.address)
|
||||
root.user.configureAppleMail(root.address)
|
||||
Backend.notifyAutoconfigClicked("AppleMail");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -298,6 +299,7 @@ Item {
|
||||
var clientObj = clients.get(clientID)
|
||||
if (clientObj != undefined && clientObj.link != "" ) {
|
||||
Qt.openUrlExternally(clientObj.link)
|
||||
Backend.notifyKBArticleClicked(clientObj.link);
|
||||
} else {
|
||||
console.log("unexpected client index", actionID, clientID)
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(6.12323e-17,-1,1,6.12323e-17,-0.800005,16.8)">
|
||||
<g id="ic-chevron-down">
|
||||
<path id="icon" d="M2.3,6.3L8,12L13.7,6.3L13,5.6L8,10.58L3,5.6L2.3,6.3Z" style="fill:rgb(23,24,28);"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 692 B |
@ -32,9 +32,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if (NOT DEFINED BRIDGE_APP_VERSION)
|
||||
message(FATAL_ERROR "BRIDGE_APP_VERSION is not defined.")
|
||||
else()
|
||||
else ()
|
||||
message(STATUS "Bridge version is ${BRIDGE_APP_VERSION}")
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
|
||||
#****************************************************************************************************************************************************
|
||||
@ -148,6 +148,7 @@ add_library(bridgepp
|
||||
bridgepp/Log/Log.h bridgepp/Log/Log.cpp
|
||||
bridgepp/Log/LogUtils.h bridgepp/Log/LogUtils.cpp
|
||||
bridgepp/ProcessMonitor.cpp bridgepp/ProcessMonitor.h
|
||||
bridgepp/SessionID/SessionID.cpp bridgepp/SessionID/SessionID.h
|
||||
bridgepp/User/User.cpp bridgepp/User/User.h
|
||||
bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp)
|
||||
|
||||
@ -167,7 +168,7 @@ target_precompile_headers(bridgepp PRIVATE Pch.h)
|
||||
|
||||
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
|
||||
cmake_policy(SET CMP0135 NEW) # avoid warning DOWNLOAD_EXTRACT_TIMESTAMP
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
@ -188,7 +189,9 @@ enable_testing()
|
||||
add_executable(bridgepp-test EXCLUDE_FROM_ALL
|
||||
Test/TestBridgeUtils.cpp
|
||||
Test/TestException.cpp
|
||||
Test/TestWorker.cpp Test/TestWorker.h)
|
||||
Test/TestSessionID.cpp
|
||||
Test/TestWorker.cpp Test/TestWorker.h
|
||||
)
|
||||
add_dependencies(bridgepp-test bridgepp)
|
||||
target_precompile_headers(bridgepp-test PRIVATE Pch.h)
|
||||
target_link_libraries(bridgepp-test
|
||||
|
||||
36
internal/frontend/bridge-gui/bridgepp/Test/TestSessionID.cpp
Normal file
36
internal/frontend/bridge-gui/bridgepp/Test/TestSessionID.cpp
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "QtCore/qdatetime.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <bridgepp/SessionID/SessionID.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
TEST(SessionID, SessionID) {
|
||||
QString const sessionID = newSessionID();
|
||||
EXPECT_TRUE(sessionID.size() > 0);
|
||||
|
||||
EXPECT_FALSE(sessionIDToDateTime("invalidSessionID").isValid());
|
||||
|
||||
QDateTime const dateTime = sessionIDToDateTime(sessionID);
|
||||
EXPECT_TRUE(dateTime.isValid());
|
||||
EXPECT_TRUE(qAbs(dateTime.secsTo(QDateTime::currentDateTime())) < 5);
|
||||
}
|
||||
@ -46,6 +46,15 @@ QString grpcFocusServerConfigFilename() {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] log The log.
|
||||
//****************************************************************************************************************************************************
|
||||
FocusGRPCClient::FocusGRPCClient(Log& log)
|
||||
:log_(log) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The absolute path of the focus service config path.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -91,6 +100,7 @@ bool FocusGRPCClient::connectToServer(qint64 timeoutMs, quint16 port, QString *o
|
||||
throw Exception("Connexion check with focus service failed.");
|
||||
}
|
||||
|
||||
log_.debug(QString("Successfully connected to focus gRPC service."));
|
||||
return true;
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
@ -103,11 +113,15 @@ bool FocusGRPCClient::connectToServer(qint64 timeoutMs, quint16 port, QString *o
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The reason behind the raise call.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status FocusGRPCClient::raise() {
|
||||
grpc::Status FocusGRPCClient::raise(QString const &reason) {
|
||||
log_.debug("FocusGRPCService::raise()");
|
||||
ClientContext ctx;
|
||||
return stub_->Raise(&ctx, empty, &empty);
|
||||
StringValue s;
|
||||
s.set_value(reason.toStdString());
|
||||
return stub_->Raise(&ctx, s, &empty);
|
||||
}
|
||||
|
||||
|
||||
@ -116,6 +130,7 @@ grpc::Status FocusGRPCClient::raise() {
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status FocusGRPCClient::version(QString &outVersion) {
|
||||
log_.debug("FocusGRPCService::version()");
|
||||
ClientContext ctx;
|
||||
VersionResponse response;
|
||||
Status status = stub_->Version(&ctx, empty, &response);
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
|
||||
#include "grpc++/grpc++.h"
|
||||
#include "focus.grpc.pb.h"
|
||||
#include "../Log/Log.h"
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
@ -36,7 +37,7 @@ public: // static member functions
|
||||
static QString grpcFocusServerConfigPath(QString const &configDir); ///< Return the path of the gRPC Focus server config file.
|
||||
|
||||
public: // member functions.
|
||||
FocusGRPCClient() = default; ///< Default constructor.
|
||||
FocusGRPCClient(Log& log); ///< Default constructor.
|
||||
FocusGRPCClient(FocusGRPCClient const &) = delete; ///< Disabled copy-constructor.
|
||||
FocusGRPCClient(FocusGRPCClient &&) = delete; ///< Disabled assignment copy-constructor.
|
||||
~FocusGRPCClient() = default; ///< Destructor.
|
||||
@ -44,10 +45,11 @@ public: // member functions.
|
||||
FocusGRPCClient &operator=(FocusGRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||
|
||||
bool connectToServer(qint64 timeoutMs, quint16 port, QString *outError = nullptr); ///< Connect to the focus server
|
||||
grpc::Status raise(); ///< Performs the 'raise' call.
|
||||
grpc::Status raise(QString const &reason); ///< Performs the 'raise' call.
|
||||
grpc::Status version(QString &outVersion); ///< Performs the 'version' call.
|
||||
|
||||
private:
|
||||
Log &log_; ///< The log to use for logging calls
|
||||
std::shared_ptr<grpc::Channel> channel_ { nullptr }; ///< The gRPC channel.
|
||||
std::shared_ptr<focus::Focus::Stub> stub_ { nullptr }; ///< The gRPC stub (a.k.a. client).
|
||||
};
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
// Generated by the gRPC C++ plugin.
|
||||
// If you make any local change, they will be lost.
|
||||
// source: focus.proto
|
||||
|
||||
#include "focus.pb.h"
|
||||
#include "focus.grpc.pb.h"
|
||||
|
||||
#include <functional>
|
||||
#include <grpcpp/support/async_stream.h>
|
||||
#include <grpcpp/support/async_unary_call.h>
|
||||
#include <grpcpp/impl/channel_interface.h>
|
||||
#include <grpcpp/impl/client_unary_call.h>
|
||||
#include <grpcpp/support/client_callback.h>
|
||||
#include <grpcpp/support/message_allocator.h>
|
||||
#include <grpcpp/support/method_handler.h>
|
||||
#include <grpcpp/impl/rpc_service_method.h>
|
||||
#include <grpcpp/support/server_callback.h>
|
||||
#include <grpcpp/impl/codegen/server_callback_handlers.h>
|
||||
#include <grpcpp/server_context.h>
|
||||
#include <grpcpp/impl/service_type.h>
|
||||
#include <grpcpp/support/sync_stream.h>
|
||||
namespace focus {
|
||||
|
||||
static const char* Focus_method_names[] = {
|
||||
"/focus.Focus/Raise",
|
||||
"/focus.Focus/Version",
|
||||
};
|
||||
|
||||
std::unique_ptr< Focus::Stub> Focus::NewStub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options) {
|
||||
(void)options;
|
||||
std::unique_ptr< Focus::Stub> stub(new Focus::Stub(channel, options));
|
||||
return stub;
|
||||
}
|
||||
|
||||
Focus::Stub::Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options)
|
||||
: channel_(channel), rpcmethod_Raise_(Focus_method_names[0], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
|
||||
, rpcmethod_Version_(Focus_method_names[1], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
|
||||
{}
|
||||
|
||||
::grpc::Status Focus::Stub::Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::Empty* response) {
|
||||
return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_Raise_, context, request, response);
|
||||
}
|
||||
|
||||
void Focus::Stub::async::Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, std::function<void(::grpc::Status)> f) {
|
||||
::grpc::internal::CallbackUnaryCall< ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Raise_, context, request, response, std::move(f));
|
||||
}
|
||||
|
||||
void Focus::Stub::async::Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, ::grpc::ClientUnaryReactor* reactor) {
|
||||
::grpc::internal::ClientCallbackUnaryFactory::Create< ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Raise_, context, request, response, reactor);
|
||||
}
|
||||
|
||||
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* Focus::Stub::PrepareAsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return ::grpc::internal::ClientAsyncResponseReaderHelper::Create< ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), cq, rpcmethod_Raise_, context, request);
|
||||
}
|
||||
|
||||
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* Focus::Stub::AsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
auto* result =
|
||||
this->PrepareAsyncRaiseRaw(context, request, cq);
|
||||
result->StartCall();
|
||||
return result;
|
||||
}
|
||||
|
||||
::grpc::Status Focus::Stub::Version(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::focus::VersionResponse* response) {
|
||||
return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::Empty, ::focus::VersionResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_Version_, context, request, response);
|
||||
}
|
||||
|
||||
void Focus::Stub::async::Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, std::function<void(::grpc::Status)> f) {
|
||||
::grpc::internal::CallbackUnaryCall< ::google::protobuf::Empty, ::focus::VersionResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Version_, context, request, response, std::move(f));
|
||||
}
|
||||
|
||||
void Focus::Stub::async::Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, ::grpc::ClientUnaryReactor* reactor) {
|
||||
::grpc::internal::ClientCallbackUnaryFactory::Create< ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Version_, context, request, response, reactor);
|
||||
}
|
||||
|
||||
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* Focus::Stub::PrepareAsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return ::grpc::internal::ClientAsyncResponseReaderHelper::Create< ::focus::VersionResponse, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), cq, rpcmethod_Version_, context, request);
|
||||
}
|
||||
|
||||
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* Focus::Stub::AsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
auto* result =
|
||||
this->PrepareAsyncVersionRaw(context, request, cq);
|
||||
result->StartCall();
|
||||
return result;
|
||||
}
|
||||
|
||||
Focus::Service::Service() {
|
||||
AddMethod(new ::grpc::internal::RpcServiceMethod(
|
||||
Focus_method_names[0],
|
||||
::grpc::internal::RpcMethod::NORMAL_RPC,
|
||||
new ::grpc::internal::RpcMethodHandler< Focus::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
|
||||
[](Focus::Service* service,
|
||||
::grpc::ServerContext* ctx,
|
||||
const ::google::protobuf::Empty* req,
|
||||
::google::protobuf::Empty* resp) {
|
||||
return service->Raise(ctx, req, resp);
|
||||
}, this)));
|
||||
AddMethod(new ::grpc::internal::RpcServiceMethod(
|
||||
Focus_method_names[1],
|
||||
::grpc::internal::RpcMethod::NORMAL_RPC,
|
||||
new ::grpc::internal::RpcMethodHandler< Focus::Service, ::google::protobuf::Empty, ::focus::VersionResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
|
||||
[](Focus::Service* service,
|
||||
::grpc::ServerContext* ctx,
|
||||
const ::google::protobuf::Empty* req,
|
||||
::focus::VersionResponse* resp) {
|
||||
return service->Version(ctx, req, resp);
|
||||
}, this)));
|
||||
}
|
||||
|
||||
Focus::Service::~Service() {
|
||||
}
|
||||
|
||||
::grpc::Status Focus::Service::Raise(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response) {
|
||||
(void) context;
|
||||
(void) request;
|
||||
(void) response;
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
|
||||
::grpc::Status Focus::Service::Version(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response) {
|
||||
(void) context;
|
||||
(void) request;
|
||||
(void) response;
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
|
||||
|
||||
} // namespace focus
|
||||
|
||||
@ -1,418 +0,0 @@
|
||||
// Generated by the gRPC C++ plugin.
|
||||
// If you make any local change, they will be lost.
|
||||
// source: focus.proto
|
||||
// Original file comments:
|
||||
// Copyright (c) 2022 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
#ifndef GRPC_focus_2eproto__INCLUDED
|
||||
#define GRPC_focus_2eproto__INCLUDED
|
||||
|
||||
#include "focus.pb.h"
|
||||
|
||||
#include <functional>
|
||||
#include <grpcpp/generic/async_generic_service.h>
|
||||
#include <grpcpp/support/async_stream.h>
|
||||
#include <grpcpp/support/async_unary_call.h>
|
||||
#include <grpcpp/support/client_callback.h>
|
||||
#include <grpcpp/client_context.h>
|
||||
#include <grpcpp/completion_queue.h>
|
||||
#include <grpcpp/support/message_allocator.h>
|
||||
#include <grpcpp/support/method_handler.h>
|
||||
#include <grpcpp/impl/codegen/proto_utils.h>
|
||||
#include <grpcpp/impl/rpc_method.h>
|
||||
#include <grpcpp/support/server_callback.h>
|
||||
#include <grpcpp/impl/codegen/server_callback_handlers.h>
|
||||
#include <grpcpp/server_context.h>
|
||||
#include <grpcpp/impl/service_type.h>
|
||||
#include <grpcpp/impl/codegen/status.h>
|
||||
#include <grpcpp/support/stub_options.h>
|
||||
#include <grpcpp/support/sync_stream.h>
|
||||
|
||||
namespace focus {
|
||||
|
||||
// **********************************************************************************************************************
|
||||
// Service Declaration
|
||||
// **********************************************************************************************************************≠––
|
||||
class Focus final {
|
||||
public:
|
||||
static constexpr char const* service_full_name() {
|
||||
return "focus.Focus";
|
||||
}
|
||||
class StubInterface {
|
||||
public:
|
||||
virtual ~StubInterface() {}
|
||||
virtual ::grpc::Status Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::Empty* response) = 0;
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>> AsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>>(AsyncRaiseRaw(context, request, cq));
|
||||
}
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>> PrepareAsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>>(PrepareAsyncRaiseRaw(context, request, cq));
|
||||
}
|
||||
virtual ::grpc::Status Version(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::focus::VersionResponse* response) = 0;
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>> AsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>>(AsyncVersionRaw(context, request, cq));
|
||||
}
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>> PrepareAsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>>(PrepareAsyncVersionRaw(context, request, cq));
|
||||
}
|
||||
class async_interface {
|
||||
public:
|
||||
virtual ~async_interface() {}
|
||||
virtual void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, std::function<void(::grpc::Status)>) = 0;
|
||||
virtual void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, ::grpc::ClientUnaryReactor* reactor) = 0;
|
||||
virtual void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, std::function<void(::grpc::Status)>) = 0;
|
||||
virtual void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, ::grpc::ClientUnaryReactor* reactor) = 0;
|
||||
};
|
||||
typedef class async_interface experimental_async_interface;
|
||||
virtual class async_interface* async() { return nullptr; }
|
||||
class async_interface* experimental_async() { return async(); }
|
||||
private:
|
||||
virtual ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>* AsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
|
||||
virtual ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>* PrepareAsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
|
||||
virtual ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>* AsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
|
||||
virtual ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>* PrepareAsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
|
||||
};
|
||||
class Stub final : public StubInterface {
|
||||
public:
|
||||
Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options = ::grpc::StubOptions());
|
||||
::grpc::Status Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::Empty* response) override;
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>> AsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>>(AsyncRaiseRaw(context, request, cq));
|
||||
}
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>> PrepareAsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>>(PrepareAsyncRaiseRaw(context, request, cq));
|
||||
}
|
||||
::grpc::Status Version(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::focus::VersionResponse* response) override;
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>> AsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>>(AsyncVersionRaw(context, request, cq));
|
||||
}
|
||||
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>> PrepareAsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
|
||||
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>>(PrepareAsyncVersionRaw(context, request, cq));
|
||||
}
|
||||
class async final :
|
||||
public StubInterface::async_interface {
|
||||
public:
|
||||
void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, std::function<void(::grpc::Status)>) override;
|
||||
void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, ::grpc::ClientUnaryReactor* reactor) override;
|
||||
void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, std::function<void(::grpc::Status)>) override;
|
||||
void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, ::grpc::ClientUnaryReactor* reactor) override;
|
||||
private:
|
||||
friend class Stub;
|
||||
explicit async(Stub* stub): stub_(stub) { }
|
||||
Stub* stub() { return stub_; }
|
||||
Stub* stub_;
|
||||
};
|
||||
class async* async() override { return &async_stub_; }
|
||||
|
||||
private:
|
||||
std::shared_ptr< ::grpc::ChannelInterface> channel_;
|
||||
class async async_stub_{this};
|
||||
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* AsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
|
||||
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* PrepareAsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
|
||||
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* AsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
|
||||
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* PrepareAsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
|
||||
const ::grpc::internal::RpcMethod rpcmethod_Raise_;
|
||||
const ::grpc::internal::RpcMethod rpcmethod_Version_;
|
||||
};
|
||||
static std::unique_ptr<Stub> NewStub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options = ::grpc::StubOptions());
|
||||
|
||||
class Service : public ::grpc::Service {
|
||||
public:
|
||||
Service();
|
||||
virtual ~Service();
|
||||
virtual ::grpc::Status Raise(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response);
|
||||
virtual ::grpc::Status Version(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response);
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithAsyncMethod_Raise : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithAsyncMethod_Raise() {
|
||||
::grpc::Service::MarkMethodAsync(0);
|
||||
}
|
||||
~WithAsyncMethod_Raise() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
void RequestRaise(::grpc::ServerContext* context, ::google::protobuf::Empty* request, ::grpc::ServerAsyncResponseWriter< ::google::protobuf::Empty>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
|
||||
::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag);
|
||||
}
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithAsyncMethod_Version : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithAsyncMethod_Version() {
|
||||
::grpc::Service::MarkMethodAsync(1);
|
||||
}
|
||||
~WithAsyncMethod_Version() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
void RequestVersion(::grpc::ServerContext* context, ::google::protobuf::Empty* request, ::grpc::ServerAsyncResponseWriter< ::focus::VersionResponse>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
|
||||
::grpc::Service::RequestAsyncUnary(1, context, request, response, new_call_cq, notification_cq, tag);
|
||||
}
|
||||
};
|
||||
typedef WithAsyncMethod_Raise<WithAsyncMethod_Version<Service > > AsyncService;
|
||||
template <class BaseClass>
|
||||
class WithCallbackMethod_Raise : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithCallbackMethod_Raise() {
|
||||
::grpc::Service::MarkMethodCallback(0,
|
||||
new ::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::google::protobuf::Empty>(
|
||||
[this](
|
||||
::grpc::CallbackServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response) { return this->Raise(context, request, response); }));}
|
||||
void SetMessageAllocatorFor_Raise(
|
||||
::grpc::MessageAllocator< ::google::protobuf::Empty, ::google::protobuf::Empty>* allocator) {
|
||||
::grpc::internal::MethodHandler* const handler = ::grpc::Service::GetHandler(0);
|
||||
static_cast<::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::google::protobuf::Empty>*>(handler)
|
||||
->SetMessageAllocator(allocator);
|
||||
}
|
||||
~WithCallbackMethod_Raise() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
virtual ::grpc::ServerUnaryReactor* Raise(
|
||||
::grpc::CallbackServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) { return nullptr; }
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithCallbackMethod_Version : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithCallbackMethod_Version() {
|
||||
::grpc::Service::MarkMethodCallback(1,
|
||||
new ::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::focus::VersionResponse>(
|
||||
[this](
|
||||
::grpc::CallbackServerContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response) { return this->Version(context, request, response); }));}
|
||||
void SetMessageAllocatorFor_Version(
|
||||
::grpc::MessageAllocator< ::google::protobuf::Empty, ::focus::VersionResponse>* allocator) {
|
||||
::grpc::internal::MethodHandler* const handler = ::grpc::Service::GetHandler(1);
|
||||
static_cast<::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::focus::VersionResponse>*>(handler)
|
||||
->SetMessageAllocator(allocator);
|
||||
}
|
||||
~WithCallbackMethod_Version() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
virtual ::grpc::ServerUnaryReactor* Version(
|
||||
::grpc::CallbackServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) { return nullptr; }
|
||||
};
|
||||
typedef WithCallbackMethod_Raise<WithCallbackMethod_Version<Service > > CallbackService;
|
||||
typedef CallbackService ExperimentalCallbackService;
|
||||
template <class BaseClass>
|
||||
class WithGenericMethod_Raise : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithGenericMethod_Raise() {
|
||||
::grpc::Service::MarkMethodGeneric(0);
|
||||
}
|
||||
~WithGenericMethod_Raise() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithGenericMethod_Version : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithGenericMethod_Version() {
|
||||
::grpc::Service::MarkMethodGeneric(1);
|
||||
}
|
||||
~WithGenericMethod_Version() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithRawMethod_Raise : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithRawMethod_Raise() {
|
||||
::grpc::Service::MarkMethodRaw(0);
|
||||
}
|
||||
~WithRawMethod_Raise() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
void RequestRaise(::grpc::ServerContext* context, ::grpc::ByteBuffer* request, ::grpc::ServerAsyncResponseWriter< ::grpc::ByteBuffer>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
|
||||
::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag);
|
||||
}
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithRawMethod_Version : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithRawMethod_Version() {
|
||||
::grpc::Service::MarkMethodRaw(1);
|
||||
}
|
||||
~WithRawMethod_Version() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
void RequestVersion(::grpc::ServerContext* context, ::grpc::ByteBuffer* request, ::grpc::ServerAsyncResponseWriter< ::grpc::ByteBuffer>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
|
||||
::grpc::Service::RequestAsyncUnary(1, context, request, response, new_call_cq, notification_cq, tag);
|
||||
}
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithRawCallbackMethod_Raise : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithRawCallbackMethod_Raise() {
|
||||
::grpc::Service::MarkMethodRawCallback(0,
|
||||
new ::grpc::internal::CallbackUnaryHandler< ::grpc::ByteBuffer, ::grpc::ByteBuffer>(
|
||||
[this](
|
||||
::grpc::CallbackServerContext* context, const ::grpc::ByteBuffer* request, ::grpc::ByteBuffer* response) { return this->Raise(context, request, response); }));
|
||||
}
|
||||
~WithRawCallbackMethod_Raise() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
virtual ::grpc::ServerUnaryReactor* Raise(
|
||||
::grpc::CallbackServerContext* /*context*/, const ::grpc::ByteBuffer* /*request*/, ::grpc::ByteBuffer* /*response*/) { return nullptr; }
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithRawCallbackMethod_Version : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithRawCallbackMethod_Version() {
|
||||
::grpc::Service::MarkMethodRawCallback(1,
|
||||
new ::grpc::internal::CallbackUnaryHandler< ::grpc::ByteBuffer, ::grpc::ByteBuffer>(
|
||||
[this](
|
||||
::grpc::CallbackServerContext* context, const ::grpc::ByteBuffer* request, ::grpc::ByteBuffer* response) { return this->Version(context, request, response); }));
|
||||
}
|
||||
~WithRawCallbackMethod_Version() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable synchronous version of this method
|
||||
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
virtual ::grpc::ServerUnaryReactor* Version(
|
||||
::grpc::CallbackServerContext* /*context*/, const ::grpc::ByteBuffer* /*request*/, ::grpc::ByteBuffer* /*response*/) { return nullptr; }
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithStreamedUnaryMethod_Raise : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithStreamedUnaryMethod_Raise() {
|
||||
::grpc::Service::MarkMethodStreamed(0,
|
||||
new ::grpc::internal::StreamedUnaryHandler<
|
||||
::google::protobuf::Empty, ::google::protobuf::Empty>(
|
||||
[this](::grpc::ServerContext* context,
|
||||
::grpc::ServerUnaryStreamer<
|
||||
::google::protobuf::Empty, ::google::protobuf::Empty>* streamer) {
|
||||
return this->StreamedRaise(context,
|
||||
streamer);
|
||||
}));
|
||||
}
|
||||
~WithStreamedUnaryMethod_Raise() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable regular version of this method
|
||||
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
// replace default version of method with streamed unary
|
||||
virtual ::grpc::Status StreamedRaise(::grpc::ServerContext* context, ::grpc::ServerUnaryStreamer< ::google::protobuf::Empty,::google::protobuf::Empty>* server_unary_streamer) = 0;
|
||||
};
|
||||
template <class BaseClass>
|
||||
class WithStreamedUnaryMethod_Version : public BaseClass {
|
||||
private:
|
||||
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
|
||||
public:
|
||||
WithStreamedUnaryMethod_Version() {
|
||||
::grpc::Service::MarkMethodStreamed(1,
|
||||
new ::grpc::internal::StreamedUnaryHandler<
|
||||
::google::protobuf::Empty, ::focus::VersionResponse>(
|
||||
[this](::grpc::ServerContext* context,
|
||||
::grpc::ServerUnaryStreamer<
|
||||
::google::protobuf::Empty, ::focus::VersionResponse>* streamer) {
|
||||
return this->StreamedVersion(context,
|
||||
streamer);
|
||||
}));
|
||||
}
|
||||
~WithStreamedUnaryMethod_Version() override {
|
||||
BaseClassMustBeDerivedFromService(this);
|
||||
}
|
||||
// disable regular version of this method
|
||||
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
|
||||
abort();
|
||||
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
|
||||
}
|
||||
// replace default version of method with streamed unary
|
||||
virtual ::grpc::Status StreamedVersion(::grpc::ServerContext* context, ::grpc::ServerUnaryStreamer< ::google::protobuf::Empty,::focus::VersionResponse>* server_unary_streamer) = 0;
|
||||
};
|
||||
typedef WithStreamedUnaryMethod_Raise<WithStreamedUnaryMethod_Version<Service > > StreamedUnaryService;
|
||||
typedef Service SplitStreamedService;
|
||||
typedef WithStreamedUnaryMethod_Raise<WithStreamedUnaryMethod_Version<Service > > StreamedService;
|
||||
};
|
||||
|
||||
} // namespace focus
|
||||
|
||||
|
||||
#endif // GRPC_focus_2eproto__INCLUDED
|
||||
@ -1,302 +0,0 @@
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: focus.proto
|
||||
|
||||
#include "focus.pb.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <google/protobuf/io/coded_stream.h>
|
||||
#include <google/protobuf/extension_set.h>
|
||||
#include <google/protobuf/wire_format_lite.h>
|
||||
#include <google/protobuf/descriptor.h>
|
||||
#include <google/protobuf/generated_message_reflection.h>
|
||||
#include <google/protobuf/reflection_ops.h>
|
||||
#include <google/protobuf/wire_format.h>
|
||||
// @@protoc_insertion_point(includes)
|
||||
#include <google/protobuf/port_def.inc>
|
||||
|
||||
PROTOBUF_PRAGMA_INIT_SEG
|
||||
|
||||
namespace _pb = ::PROTOBUF_NAMESPACE_ID;
|
||||
namespace _pbi = _pb::internal;
|
||||
|
||||
namespace focus {
|
||||
PROTOBUF_CONSTEXPR VersionResponse::VersionResponse(
|
||||
::_pbi::ConstantInitialized): _impl_{
|
||||
/*decltype(_impl_.version_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}
|
||||
, /*decltype(_impl_._cached_size_)*/{}} {}
|
||||
struct VersionResponseDefaultTypeInternal {
|
||||
PROTOBUF_CONSTEXPR VersionResponseDefaultTypeInternal()
|
||||
: _instance(::_pbi::ConstantInitialized{}) {}
|
||||
~VersionResponseDefaultTypeInternal() {}
|
||||
union {
|
||||
VersionResponse _instance;
|
||||
};
|
||||
};
|
||||
PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 VersionResponseDefaultTypeInternal _VersionResponse_default_instance_;
|
||||
} // namespace focus
|
||||
static ::_pb::Metadata file_level_metadata_focus_2eproto[1];
|
||||
static constexpr ::_pb::EnumDescriptor const** file_level_enum_descriptors_focus_2eproto = nullptr;
|
||||
static constexpr ::_pb::ServiceDescriptor const** file_level_service_descriptors_focus_2eproto = nullptr;
|
||||
|
||||
const uint32_t TableStruct_focus_2eproto::offsets[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = {
|
||||
~0u, // no _has_bits_
|
||||
PROTOBUF_FIELD_OFFSET(::focus::VersionResponse, _internal_metadata_),
|
||||
~0u, // no _extensions_
|
||||
~0u, // no _oneof_case_
|
||||
~0u, // no _weak_field_map_
|
||||
~0u, // no _inlined_string_donated_
|
||||
PROTOBUF_FIELD_OFFSET(::focus::VersionResponse, _impl_.version_),
|
||||
};
|
||||
static const ::_pbi::MigrationSchema schemas[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = {
|
||||
{ 0, -1, -1, sizeof(::focus::VersionResponse)},
|
||||
};
|
||||
|
||||
static const ::_pb::Message* const file_default_instances[] = {
|
||||
&::focus::_VersionResponse_default_instance_._instance,
|
||||
};
|
||||
|
||||
const char descriptor_table_protodef_focus_2eproto[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) =
|
||||
"\n\013focus.proto\022\005focus\032\033google/protobuf/em"
|
||||
"pty.proto\"\"\n\017VersionResponse\022\017\n\007version\030"
|
||||
"\001 \001(\t2{\n\005Focus\0227\n\005Raise\022\026.google.protobu"
|
||||
"f.Empty\032\026.google.protobuf.Empty\0229\n\007Versi"
|
||||
"on\022\026.google.protobuf.Empty\032\026.focus.Versi"
|
||||
"onResponseB=Z;github.com/ProtonMail/prot"
|
||||
"on-bridge/v3/internal/focus/protob\006proto"
|
||||
"3"
|
||||
;
|
||||
static const ::_pbi::DescriptorTable* const descriptor_table_focus_2eproto_deps[1] = {
|
||||
&::descriptor_table_google_2fprotobuf_2fempty_2eproto,
|
||||
};
|
||||
static ::_pbi::once_flag descriptor_table_focus_2eproto_once;
|
||||
const ::_pbi::DescriptorTable descriptor_table_focus_2eproto = {
|
||||
false, false, 281, descriptor_table_protodef_focus_2eproto,
|
||||
"focus.proto",
|
||||
&descriptor_table_focus_2eproto_once, descriptor_table_focus_2eproto_deps, 1, 1,
|
||||
schemas, file_default_instances, TableStruct_focus_2eproto::offsets,
|
||||
file_level_metadata_focus_2eproto, file_level_enum_descriptors_focus_2eproto,
|
||||
file_level_service_descriptors_focus_2eproto,
|
||||
};
|
||||
PROTOBUF_ATTRIBUTE_WEAK const ::_pbi::DescriptorTable* descriptor_table_focus_2eproto_getter() {
|
||||
return &descriptor_table_focus_2eproto;
|
||||
}
|
||||
|
||||
// Force running AddDescriptors() at dynamic initialization time.
|
||||
PROTOBUF_ATTRIBUTE_INIT_PRIORITY2 static ::_pbi::AddDescriptorsRunner dynamic_init_dummy_focus_2eproto(&descriptor_table_focus_2eproto);
|
||||
namespace focus {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class VersionResponse::_Internal {
|
||||
public:
|
||||
};
|
||||
|
||||
VersionResponse::VersionResponse(::PROTOBUF_NAMESPACE_ID::Arena* arena,
|
||||
bool is_message_owned)
|
||||
: ::PROTOBUF_NAMESPACE_ID::Message(arena, is_message_owned) {
|
||||
SharedCtor(arena, is_message_owned);
|
||||
// @@protoc_insertion_point(arena_constructor:focus.VersionResponse)
|
||||
}
|
||||
VersionResponse::VersionResponse(const VersionResponse& from)
|
||||
: ::PROTOBUF_NAMESPACE_ID::Message() {
|
||||
VersionResponse* const _this = this; (void)_this;
|
||||
new (&_impl_) Impl_{
|
||||
decltype(_impl_.version_){}
|
||||
, /*decltype(_impl_._cached_size_)*/{}};
|
||||
|
||||
_internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_);
|
||||
_impl_.version_.InitDefault();
|
||||
#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
|
||||
_impl_.version_.Set("", GetArenaForAllocation());
|
||||
#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
|
||||
if (!from._internal_version().empty()) {
|
||||
_this->_impl_.version_.Set(from._internal_version(),
|
||||
_this->GetArenaForAllocation());
|
||||
}
|
||||
// @@protoc_insertion_point(copy_constructor:focus.VersionResponse)
|
||||
}
|
||||
|
||||
inline void VersionResponse::SharedCtor(
|
||||
::_pb::Arena* arena, bool is_message_owned) {
|
||||
(void)arena;
|
||||
(void)is_message_owned;
|
||||
new (&_impl_) Impl_{
|
||||
decltype(_impl_.version_){}
|
||||
, /*decltype(_impl_._cached_size_)*/{}
|
||||
};
|
||||
_impl_.version_.InitDefault();
|
||||
#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
|
||||
_impl_.version_.Set("", GetArenaForAllocation());
|
||||
#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
|
||||
}
|
||||
|
||||
VersionResponse::~VersionResponse() {
|
||||
// @@protoc_insertion_point(destructor:focus.VersionResponse)
|
||||
if (auto *arena = _internal_metadata_.DeleteReturnArena<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>()) {
|
||||
(void)arena;
|
||||
return;
|
||||
}
|
||||
SharedDtor();
|
||||
}
|
||||
|
||||
inline void VersionResponse::SharedDtor() {
|
||||
GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
|
||||
_impl_.version_.Destroy();
|
||||
}
|
||||
|
||||
void VersionResponse::SetCachedSize(int size) const {
|
||||
_impl_._cached_size_.Set(size);
|
||||
}
|
||||
|
||||
void VersionResponse::Clear() {
|
||||
// @@protoc_insertion_point(message_clear_start:focus.VersionResponse)
|
||||
uint32_t cached_has_bits = 0;
|
||||
// Prevent compiler warnings about cached_has_bits being unused
|
||||
(void) cached_has_bits;
|
||||
|
||||
_impl_.version_.ClearToEmpty();
|
||||
_internal_metadata_.Clear<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>();
|
||||
}
|
||||
|
||||
const char* VersionResponse::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) {
|
||||
#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure
|
||||
while (!ctx->Done(&ptr)) {
|
||||
uint32_t tag;
|
||||
ptr = ::_pbi::ReadTag(ptr, &tag);
|
||||
switch (tag >> 3) {
|
||||
// string version = 1;
|
||||
case 1:
|
||||
if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 10)) {
|
||||
auto str = _internal_mutable_version();
|
||||
ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
|
||||
CHK_(ptr);
|
||||
CHK_(::_pbi::VerifyUTF8(str, "focus.VersionResponse.version"));
|
||||
} else
|
||||
goto handle_unusual;
|
||||
continue;
|
||||
default:
|
||||
goto handle_unusual;
|
||||
} // switch
|
||||
handle_unusual:
|
||||
if ((tag == 0) || ((tag & 7) == 4)) {
|
||||
CHK_(ptr);
|
||||
ctx->SetLastTag(tag);
|
||||
goto message_done;
|
||||
}
|
||||
ptr = UnknownFieldParse(
|
||||
tag,
|
||||
_internal_metadata_.mutable_unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(),
|
||||
ptr, ctx);
|
||||
CHK_(ptr != nullptr);
|
||||
} // while
|
||||
message_done:
|
||||
return ptr;
|
||||
failure:
|
||||
ptr = nullptr;
|
||||
goto message_done;
|
||||
#undef CHK_
|
||||
}
|
||||
|
||||
uint8_t* VersionResponse::_InternalSerialize(
|
||||
uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
|
||||
// @@protoc_insertion_point(serialize_to_array_start:focus.VersionResponse)
|
||||
uint32_t cached_has_bits = 0;
|
||||
(void) cached_has_bits;
|
||||
|
||||
// string version = 1;
|
||||
if (!this->_internal_version().empty()) {
|
||||
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::VerifyUtf8String(
|
||||
this->_internal_version().data(), static_cast<int>(this->_internal_version().length()),
|
||||
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::SERIALIZE,
|
||||
"focus.VersionResponse.version");
|
||||
target = stream->WriteStringMaybeAliased(
|
||||
1, this->_internal_version(), target);
|
||||
}
|
||||
|
||||
if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
|
||||
target = ::_pbi::WireFormat::InternalSerializeUnknownFieldsToArray(
|
||||
_internal_metadata_.unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(::PROTOBUF_NAMESPACE_ID::UnknownFieldSet::default_instance), target, stream);
|
||||
}
|
||||
// @@protoc_insertion_point(serialize_to_array_end:focus.VersionResponse)
|
||||
return target;
|
||||
}
|
||||
|
||||
size_t VersionResponse::ByteSizeLong() const {
|
||||
// @@protoc_insertion_point(message_byte_size_start:focus.VersionResponse)
|
||||
size_t total_size = 0;
|
||||
|
||||
uint32_t cached_has_bits = 0;
|
||||
// Prevent compiler warnings about cached_has_bits being unused
|
||||
(void) cached_has_bits;
|
||||
|
||||
// string version = 1;
|
||||
if (!this->_internal_version().empty()) {
|
||||
total_size += 1 +
|
||||
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize(
|
||||
this->_internal_version());
|
||||
}
|
||||
|
||||
return MaybeComputeUnknownFieldsSize(total_size, &_impl_._cached_size_);
|
||||
}
|
||||
|
||||
const ::PROTOBUF_NAMESPACE_ID::Message::ClassData VersionResponse::_class_data_ = {
|
||||
::PROTOBUF_NAMESPACE_ID::Message::CopyWithSourceCheck,
|
||||
VersionResponse::MergeImpl
|
||||
};
|
||||
const ::PROTOBUF_NAMESPACE_ID::Message::ClassData*VersionResponse::GetClassData() const { return &_class_data_; }
|
||||
|
||||
|
||||
void VersionResponse::MergeImpl(::PROTOBUF_NAMESPACE_ID::Message& to_msg, const ::PROTOBUF_NAMESPACE_ID::Message& from_msg) {
|
||||
auto* const _this = static_cast<VersionResponse*>(&to_msg);
|
||||
auto& from = static_cast<const VersionResponse&>(from_msg);
|
||||
// @@protoc_insertion_point(class_specific_merge_from_start:focus.VersionResponse)
|
||||
GOOGLE_DCHECK_NE(&from, _this);
|
||||
uint32_t cached_has_bits = 0;
|
||||
(void) cached_has_bits;
|
||||
|
||||
if (!from._internal_version().empty()) {
|
||||
_this->_internal_set_version(from._internal_version());
|
||||
}
|
||||
_this->_internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_);
|
||||
}
|
||||
|
||||
void VersionResponse::CopyFrom(const VersionResponse& from) {
|
||||
// @@protoc_insertion_point(class_specific_copy_from_start:focus.VersionResponse)
|
||||
if (&from == this) return;
|
||||
Clear();
|
||||
MergeFrom(from);
|
||||
}
|
||||
|
||||
bool VersionResponse::IsInitialized() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
void VersionResponse::InternalSwap(VersionResponse* other) {
|
||||
using std::swap;
|
||||
auto* lhs_arena = GetArenaForAllocation();
|
||||
auto* rhs_arena = other->GetArenaForAllocation();
|
||||
_internal_metadata_.InternalSwap(&other->_internal_metadata_);
|
||||
::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap(
|
||||
&_impl_.version_, lhs_arena,
|
||||
&other->_impl_.version_, rhs_arena
|
||||
);
|
||||
}
|
||||
|
||||
::PROTOBUF_NAMESPACE_ID::Metadata VersionResponse::GetMetadata() const {
|
||||
return ::_pbi::AssignDescriptors(
|
||||
&descriptor_table_focus_2eproto_getter, &descriptor_table_focus_2eproto_once,
|
||||
file_level_metadata_focus_2eproto[0]);
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(namespace_scope)
|
||||
} // namespace focus
|
||||
PROTOBUF_NAMESPACE_OPEN
|
||||
template<> PROTOBUF_NOINLINE ::focus::VersionResponse*
|
||||
Arena::CreateMaybeMessage< ::focus::VersionResponse >(Arena* arena) {
|
||||
return Arena::CreateMessageInternal< ::focus::VersionResponse >(arena);
|
||||
}
|
||||
PROTOBUF_NAMESPACE_CLOSE
|
||||
|
||||
// @@protoc_insertion_point(global_scope)
|
||||
#include <google/protobuf/port_undef.inc>
|
||||
@ -1,283 +0,0 @@
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: focus.proto
|
||||
|
||||
#ifndef GOOGLE_PROTOBUF_INCLUDED_focus_2eproto
|
||||
#define GOOGLE_PROTOBUF_INCLUDED_focus_2eproto
|
||||
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
#include <google/protobuf/port_def.inc>
|
||||
#if PROTOBUF_VERSION < 3021000
|
||||
#error This file was generated by a newer version of protoc which is
|
||||
#error incompatible with your Protocol Buffer headers. Please update
|
||||
#error your headers.
|
||||
#endif
|
||||
#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION
|
||||
#error This file was generated by an older version of protoc which is
|
||||
#error incompatible with your Protocol Buffer headers. Please
|
||||
#error regenerate this file with a newer version of protoc.
|
||||
#endif
|
||||
|
||||
#include <google/protobuf/port_undef.inc>
|
||||
#include <google/protobuf/io/coded_stream.h>
|
||||
#include <google/protobuf/arena.h>
|
||||
#include <google/protobuf/arenastring.h>
|
||||
#include <google/protobuf/generated_message_util.h>
|
||||
#include <google/protobuf/metadata_lite.h>
|
||||
#include <google/protobuf/generated_message_reflection.h>
|
||||
#include <google/protobuf/message.h>
|
||||
#include <google/protobuf/repeated_field.h> // IWYU pragma: export
|
||||
#include <google/protobuf/extension_set.h> // IWYU pragma: export
|
||||
#include <google/protobuf/unknown_field_set.h>
|
||||
#include <google/protobuf/empty.pb.h>
|
||||
// @@protoc_insertion_point(includes)
|
||||
#include <google/protobuf/port_def.inc>
|
||||
#define PROTOBUF_INTERNAL_EXPORT_focus_2eproto
|
||||
PROTOBUF_NAMESPACE_OPEN
|
||||
namespace internal {
|
||||
class AnyMetadata;
|
||||
} // namespace internal
|
||||
PROTOBUF_NAMESPACE_CLOSE
|
||||
|
||||
// Internal implementation detail -- do not use these members.
|
||||
struct TableStruct_focus_2eproto {
|
||||
static const uint32_t offsets[];
|
||||
};
|
||||
extern const ::PROTOBUF_NAMESPACE_ID::internal::DescriptorTable descriptor_table_focus_2eproto;
|
||||
namespace focus {
|
||||
class VersionResponse;
|
||||
struct VersionResponseDefaultTypeInternal;
|
||||
extern VersionResponseDefaultTypeInternal _VersionResponse_default_instance_;
|
||||
} // namespace focus
|
||||
PROTOBUF_NAMESPACE_OPEN
|
||||
template<> ::focus::VersionResponse* Arena::CreateMaybeMessage<::focus::VersionResponse>(Arena*);
|
||||
PROTOBUF_NAMESPACE_CLOSE
|
||||
namespace focus {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class VersionResponse final :
|
||||
public ::PROTOBUF_NAMESPACE_ID::Message /* @@protoc_insertion_point(class_definition:focus.VersionResponse) */ {
|
||||
public:
|
||||
inline VersionResponse() : VersionResponse(nullptr) {}
|
||||
~VersionResponse() override;
|
||||
explicit PROTOBUF_CONSTEXPR VersionResponse(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized);
|
||||
|
||||
VersionResponse(const VersionResponse& from);
|
||||
VersionResponse(VersionResponse&& from) noexcept
|
||||
: VersionResponse() {
|
||||
*this = ::std::move(from);
|
||||
}
|
||||
|
||||
inline VersionResponse& operator=(const VersionResponse& from) {
|
||||
CopyFrom(from);
|
||||
return *this;
|
||||
}
|
||||
inline VersionResponse& operator=(VersionResponse&& from) noexcept {
|
||||
if (this == &from) return *this;
|
||||
if (GetOwningArena() == from.GetOwningArena()
|
||||
#ifdef PROTOBUF_FORCE_COPY_IN_MOVE
|
||||
&& GetOwningArena() != nullptr
|
||||
#endif // !PROTOBUF_FORCE_COPY_IN_MOVE
|
||||
) {
|
||||
InternalSwap(&from);
|
||||
} else {
|
||||
CopyFrom(from);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
static const ::PROTOBUF_NAMESPACE_ID::Descriptor* descriptor() {
|
||||
return GetDescriptor();
|
||||
}
|
||||
static const ::PROTOBUF_NAMESPACE_ID::Descriptor* GetDescriptor() {
|
||||
return default_instance().GetMetadata().descriptor;
|
||||
}
|
||||
static const ::PROTOBUF_NAMESPACE_ID::Reflection* GetReflection() {
|
||||
return default_instance().GetMetadata().reflection;
|
||||
}
|
||||
static const VersionResponse& default_instance() {
|
||||
return *internal_default_instance();
|
||||
}
|
||||
static inline const VersionResponse* internal_default_instance() {
|
||||
return reinterpret_cast<const VersionResponse*>(
|
||||
&_VersionResponse_default_instance_);
|
||||
}
|
||||
static constexpr int kIndexInFileMessages =
|
||||
0;
|
||||
|
||||
friend void swap(VersionResponse& a, VersionResponse& b) {
|
||||
a.Swap(&b);
|
||||
}
|
||||
inline void Swap(VersionResponse* other) {
|
||||
if (other == this) return;
|
||||
#ifdef PROTOBUF_FORCE_COPY_IN_SWAP
|
||||
if (GetOwningArena() != nullptr &&
|
||||
GetOwningArena() == other->GetOwningArena()) {
|
||||
#else // PROTOBUF_FORCE_COPY_IN_SWAP
|
||||
if (GetOwningArena() == other->GetOwningArena()) {
|
||||
#endif // !PROTOBUF_FORCE_COPY_IN_SWAP
|
||||
InternalSwap(other);
|
||||
} else {
|
||||
::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other);
|
||||
}
|
||||
}
|
||||
void UnsafeArenaSwap(VersionResponse* other) {
|
||||
if (other == this) return;
|
||||
GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena());
|
||||
InternalSwap(other);
|
||||
}
|
||||
|
||||
// implements Message ----------------------------------------------
|
||||
|
||||
VersionResponse* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final {
|
||||
return CreateMaybeMessage<VersionResponse>(arena);
|
||||
}
|
||||
using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
|
||||
void CopyFrom(const VersionResponse& from);
|
||||
using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
|
||||
void MergeFrom( const VersionResponse& from) {
|
||||
VersionResponse::MergeImpl(*this, from);
|
||||
}
|
||||
private:
|
||||
static void MergeImpl(::PROTOBUF_NAMESPACE_ID::Message& to_msg, const ::PROTOBUF_NAMESPACE_ID::Message& from_msg);
|
||||
public:
|
||||
PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final;
|
||||
bool IsInitialized() const final;
|
||||
|
||||
size_t ByteSizeLong() const final;
|
||||
const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final;
|
||||
uint8_t* _InternalSerialize(
|
||||
uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final;
|
||||
int GetCachedSize() const final { return _impl_._cached_size_.Get(); }
|
||||
|
||||
private:
|
||||
void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned);
|
||||
void SharedDtor();
|
||||
void SetCachedSize(int size) const final;
|
||||
void InternalSwap(VersionResponse* other);
|
||||
|
||||
private:
|
||||
friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata;
|
||||
static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
|
||||
return "focus.VersionResponse";
|
||||
}
|
||||
protected:
|
||||
explicit VersionResponse(::PROTOBUF_NAMESPACE_ID::Arena* arena,
|
||||
bool is_message_owned = false);
|
||||
public:
|
||||
|
||||
static const ClassData _class_data_;
|
||||
const ::PROTOBUF_NAMESPACE_ID::Message::ClassData*GetClassData() const final;
|
||||
|
||||
::PROTOBUF_NAMESPACE_ID::Metadata GetMetadata() const final;
|
||||
|
||||
// nested types ----------------------------------------------------
|
||||
|
||||
// accessors -------------------------------------------------------
|
||||
|
||||
enum : int {
|
||||
kVersionFieldNumber = 1,
|
||||
};
|
||||
// string version = 1;
|
||||
void clear_version();
|
||||
const std::string& version() const;
|
||||
template <typename ArgT0 = const std::string&, typename... ArgT>
|
||||
void set_version(ArgT0&& arg0, ArgT... args);
|
||||
std::string* mutable_version();
|
||||
PROTOBUF_NODISCARD std::string* release_version();
|
||||
void set_allocated_version(std::string* version);
|
||||
private:
|
||||
const std::string& _internal_version() const;
|
||||
inline PROTOBUF_ALWAYS_INLINE void _internal_set_version(const std::string& value);
|
||||
std::string* _internal_mutable_version();
|
||||
public:
|
||||
|
||||
// @@protoc_insertion_point(class_scope:focus.VersionResponse)
|
||||
private:
|
||||
class _Internal;
|
||||
|
||||
template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper;
|
||||
typedef void InternalArenaConstructable_;
|
||||
typedef void DestructorSkippable_;
|
||||
struct Impl_ {
|
||||
::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr version_;
|
||||
mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
|
||||
};
|
||||
union { Impl_ _impl_; };
|
||||
friend struct ::TableStruct_focus_2eproto;
|
||||
};
|
||||
// ===================================================================
|
||||
|
||||
|
||||
// ===================================================================
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
|
||||
#endif // __GNUC__
|
||||
// VersionResponse
|
||||
|
||||
// string version = 1;
|
||||
inline void VersionResponse::clear_version() {
|
||||
_impl_.version_.ClearToEmpty();
|
||||
}
|
||||
inline const std::string& VersionResponse::version() const {
|
||||
// @@protoc_insertion_point(field_get:focus.VersionResponse.version)
|
||||
return _internal_version();
|
||||
}
|
||||
template <typename ArgT0, typename... ArgT>
|
||||
inline PROTOBUF_ALWAYS_INLINE
|
||||
void VersionResponse::set_version(ArgT0&& arg0, ArgT... args) {
|
||||
|
||||
_impl_.version_.Set(static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
|
||||
// @@protoc_insertion_point(field_set:focus.VersionResponse.version)
|
||||
}
|
||||
inline std::string* VersionResponse::mutable_version() {
|
||||
std::string* _s = _internal_mutable_version();
|
||||
// @@protoc_insertion_point(field_mutable:focus.VersionResponse.version)
|
||||
return _s;
|
||||
}
|
||||
inline const std::string& VersionResponse::_internal_version() const {
|
||||
return _impl_.version_.Get();
|
||||
}
|
||||
inline void VersionResponse::_internal_set_version(const std::string& value) {
|
||||
|
||||
_impl_.version_.Set(value, GetArenaForAllocation());
|
||||
}
|
||||
inline std::string* VersionResponse::_internal_mutable_version() {
|
||||
|
||||
return _impl_.version_.Mutable(GetArenaForAllocation());
|
||||
}
|
||||
inline std::string* VersionResponse::release_version() {
|
||||
// @@protoc_insertion_point(field_release:focus.VersionResponse.version)
|
||||
return _impl_.version_.Release();
|
||||
}
|
||||
inline void VersionResponse::set_allocated_version(std::string* version) {
|
||||
if (version != nullptr) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
_impl_.version_.SetAllocated(version, GetArenaForAllocation());
|
||||
#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
|
||||
if (_impl_.version_.IsDefault()) {
|
||||
_impl_.version_.Set("", GetArenaForAllocation());
|
||||
}
|
||||
#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
|
||||
// @@protoc_insertion_point(field_set_allocated:focus.VersionResponse.version)
|
||||
}
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif // __GNUC__
|
||||
|
||||
// @@protoc_insertion_point(namespace_scope)
|
||||
|
||||
} // namespace focus
|
||||
|
||||
// @@protoc_insertion_point(global_scope)
|
||||
|
||||
#include <google/protobuf/port_undef.inc>
|
||||
#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_focus_2eproto
|
||||
@ -57,11 +57,13 @@ void GRPCClient::removeServiceConfigFile(QString const &configDir) {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] sessionID The sessionID.
|
||||
/// \param[in] timeoutMs The timeout in milliseconds
|
||||
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
|
||||
/// \return The service config.
|
||||
//****************************************************************************************************************************************************
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, ProcessMonitor *serverProcess) {
|
||||
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const & sessionID, QString const &configDir, qint64 timeoutMs,
|
||||
ProcessMonitor *serverProcess) {
|
||||
QString const path = grpcServerConfigPath(configDir);
|
||||
QFile file(path);
|
||||
|
||||
@ -71,7 +73,7 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qi
|
||||
while (true) {
|
||||
if (serverProcess && serverProcess->getStatus().ended) {
|
||||
throw Exception("Bridge application exited before providing a gRPC service configuration file.", QString(), __FUNCTION__,
|
||||
tailOfLatestBridgeLog());
|
||||
tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
if (file.exists()) {
|
||||
@ -85,7 +87,7 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qi
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
throw Exception("Server did not provide gRPC service configuration in time.", QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||
throw Exception("Server did not provide gRPC service configuration in time.", QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
GRPCConfig sc;
|
||||
@ -114,10 +116,12 @@ void GRPCClient::setLog(Log *log) {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] sessionID The sessionID.
|
||||
/// \param[in] configDir The configuration directory
|
||||
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
|
||||
/// \return true iff the connection was successful.
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &config, ProcessMonitor *serverProcess) {
|
||||
void GRPCClient::connectToServer(QString const &sessionID, QString const &configDir, GRPCConfig const &config, ProcessMonitor *serverProcess) {
|
||||
try {
|
||||
serverToken_ = config.token.toStdString();
|
||||
QString address;
|
||||
@ -147,7 +151,7 @@ void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &con
|
||||
while (true) {
|
||||
if (serverProcess && serverProcess->getStatus().ended) {
|
||||
throw Exception("Bridge application ended before gRPC connexion could be established.", QString(), __FUNCTION__,
|
||||
tailOfLatestBridgeLog());
|
||||
tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
||||
@ -158,7 +162,7 @@ void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &con
|
||||
|
||||
if (QDateTime::currentDateTime() > giveUpTime) {
|
||||
throw Exception("Connection to the gRPC server failed because of a timeout.", QString(), __FUNCTION__,
|
||||
tailOfLatestBridgeLog());
|
||||
tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1486,5 +1490,25 @@ UPClientContext GRPCClient::clientContext() const {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The user ID.
|
||||
/// \param[in] address The email address.
|
||||
/// \return the status for the gRPC call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::reportBugClicked() {
|
||||
return this->logGRPCCallStatus(stub_->ReportBugClicked(this->clientContext().get(), empty, &empty), __FUNCTION__);
|
||||
}
|
||||
|
||||
grpc::Status GRPCClient::autoconfigClicked(QString const &client) {
|
||||
StringValue s;
|
||||
s.set_value(client.toStdString());
|
||||
return this->logGRPCCallStatus(stub_->AutoconfigClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
|
||||
}
|
||||
|
||||
grpc::Status GRPCClient::KBArticleClicked(QString const &article) {
|
||||
StringValue s;
|
||||
s.set_value(article.toStdString());
|
||||
return this->logGRPCCallStatus(stub_->KBArticleClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
|
||||
}
|
||||
|
||||
} // namespace bridgepp
|
||||
|
||||
@ -49,7 +49,8 @@ class GRPCClient : public QObject {
|
||||
Q_OBJECT
|
||||
public: // static member functions
|
||||
static void removeServiceConfigFile(QString const &configDir); ///< Delete the service config file.
|
||||
static GRPCConfig waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
|
||||
static GRPCConfig waitAndRetrieveServiceConfig(QString const &sessionID, QString const &configDir, qint64 timeoutMs,
|
||||
class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
|
||||
|
||||
public: // member functions.
|
||||
GRPCClient() = default; ///< Default constructor.
|
||||
@ -59,7 +60,7 @@ public: // member functions.
|
||||
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||
void setLog(Log *log); ///< Set the log for the client.
|
||||
void connectToServer(QString const &configDir, GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
|
||||
void connectToServer(QString const &sessionID, QString const &configDir, GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
|
||||
bool isConnected() const; ///< Check whether the gRPC client is connected to the server.
|
||||
|
||||
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
|
||||
@ -189,6 +190,11 @@ signals:
|
||||
void syncFinished(QString const &userID);
|
||||
void syncProgress(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs);
|
||||
|
||||
public: // telemetry related calls
|
||||
grpc::Status reportBugClicked(); ///< Performs the 'reportBugClicked' call.
|
||||
grpc::Status autoconfigClicked(QString const &userID); ///< Performs the 'AutoconfigClicked' call.
|
||||
grpc::Status KBArticleClicked(QString const &userID); ///< Performs the 'KBArticleClicked' call.
|
||||
|
||||
public: // keychain related calls
|
||||
grpc::Status availableKeychains(QStringList &outKeychains);
|
||||
grpc::Status currentKeychain(QString &outKeychain);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -46,7 +46,7 @@ public: // static member functions.
|
||||
static bool stringToLevel(QString const &str, Log::Level &outLevel); ///< parse a level from a string.
|
||||
|
||||
public: // static data member.
|
||||
static const Level defaultLevel { Level::Info }; ///< The default log level (the same as logrus).
|
||||
static const Level defaultLevel { Level::Debug }; ///< The default log level in Bridge.
|
||||
|
||||
public: // member functions.
|
||||
Log(); ///< Default constructor.
|
||||
|
||||
@ -35,34 +35,29 @@ QString userLogsDir() {
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Return the path of the latest bridge log.
|
||||
///
|
||||
/// \param[in] sessionID The sessionID.
|
||||
/// \return The path of the latest bridge log file.
|
||||
/// \return An empty string if no bridge log file was found.
|
||||
//****************************************************************************************************************************************************
|
||||
QString latestBridgeLogPath() {
|
||||
QString latestBridgeLogPath(QString const &sessionID) {
|
||||
QDir const logsDir(userLogsDir());
|
||||
if (logsDir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
|
||||
if (files.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
|
||||
return lhs.birthTime() < rhs.birthTime();
|
||||
});
|
||||
return files.back().absoluteFilePath();
|
||||
QFileInfoList const files = logsDir.entryInfoList({ sessionID + "_bri_*.log" }, QDir::Files, QDir::Name);
|
||||
return files.isEmpty() ? QString() : files.back().absoluteFilePath();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// Return the maxSize last bytes of the latest bridge log.
|
||||
//****************************************************************************************************************************************************
|
||||
QByteArray tailOfLatestBridgeLog() {
|
||||
QString path = latestBridgeLogPath();
|
||||
QByteArray tailOfLatestBridgeLog(QString const &sessionID) {
|
||||
QString path = latestBridgeLogPath(sessionID);
|
||||
if (path.isEmpty()) {
|
||||
return QByteArray();
|
||||
return QString("We could not find a bridge log file for the current session.").toLocal8Bit();
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
|
||||
@ -24,7 +24,7 @@ namespace bridgepp {
|
||||
|
||||
|
||||
QString userLogsDir(); ///< Return the path of the user logs dir.
|
||||
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
|
||||
QByteArray tailOfLatestBridgeLog(QString const &sessionID); ///< Return the last bytes of the last bridge log.
|
||||
|
||||
|
||||
} // namespace bridgepp
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "SessionID.h"
|
||||
#include "QtCore/qdatetime.h"
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
QString const dateTimeFormat = "yyyyMMdd_hhmmsszzz"; ///< The format string for date/time used by the sessionID.
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return a new session ID based on the current local date/time
|
||||
//****************************************************************************************************************************************************
|
||||
QString newSessionID() {
|
||||
return QDateTime::currentDateTime().toString(dateTimeFormat);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] sessionID The sessionID.
|
||||
/// \return The date/time corresponding to the sessionID.
|
||||
/// \return An invalid date/time if an error occurs.
|
||||
//****************************************************************************************************************************************************
|
||||
QDateTime sessionIDToDateTime(QString const &sessionID) {
|
||||
return QDateTime::fromString(sessionID, dateTimeFormat);
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_PP_SESSION_ID_H
|
||||
#define BRIDGE_PP_SESSION_ID_H
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
|
||||
|
||||
QString newSessionID(); ///< Create a new sessions
|
||||
QDateTime sessionIDToDateTime(QString const &sessionID); ///< Parse the date/time from a sessionID.
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif //BRIDGE_PP_SESSION_ID_H
|
||||
74
internal/frontend/cli/debug.go
Normal file
74
internal/frontend/cli/debug.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
func (f *frontendCLI) debugMailboxState(c *ishell.Context) {
|
||||
f.ShowPrompt(false)
|
||||
defer f.ShowPrompt(true)
|
||||
|
||||
checkFlags := f.yesNoQuestion("Also check message flags")
|
||||
|
||||
c.Println("Starting state check. Note that depending on your message count this may take a while.")
|
||||
|
||||
result, err := f.bridge.CheckClientState(context.Background(), checkFlags, func(s string) {
|
||||
c.Println(s)
|
||||
})
|
||||
if err != nil {
|
||||
c.Printf("State check failed : %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Println("State check finished, see log for more details.")
|
||||
|
||||
if len(result.MissingMessages) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
f.Println("\n\nSome missing messages were detected. Bridge can download these messages for you")
|
||||
f.Println("in a directory which you can later send to the developers for analysis.\n")
|
||||
f.Println(bold("Note that the Messages will be stored unencrypted on disk.") + " If you do not wish")
|
||||
f.Println("to continue, input no in the prompt below.\n")
|
||||
|
||||
if !f.yesNoQuestion("Would you like to proceed") {
|
||||
return
|
||||
}
|
||||
|
||||
location, err := os.MkdirTemp("", "debug-state-check-*")
|
||||
if err != nil {
|
||||
f.Printf("Failed to create temporary directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Printf("Messages will be downloaded to: %v\n\n", bold(location))
|
||||
|
||||
if err := f.bridge.DebugDownloadFailedMessages(context.Background(), result, location, func(s string, i int, i2 int) {
|
||||
f.Printf("[%v] Retrieving message %v of %v\n", s, i, i2)
|
||||
}); err != nil {
|
||||
f.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Printf("\nMessage download finished. Data is available at %v\n", bold(location))
|
||||
}
|
||||
@ -312,6 +312,19 @@ func New(
|
||||
})
|
||||
fe.AddCmd(telemetryCmd)
|
||||
|
||||
dbgCmd := &ishell.Cmd{
|
||||
Name: "debug",
|
||||
Help: "Debug diagnostics ",
|
||||
}
|
||||
|
||||
dbgCmd.AddCmd(&ishell.Cmd{
|
||||
Name: "mailbox-state",
|
||||
Help: "Verify local mailbox state against proton server state",
|
||||
Func: fe.debugMailboxState,
|
||||
})
|
||||
|
||||
fe.AddCmd(dbgCmd)
|
||||
|
||||
go fe.watchEvents(eventCh)
|
||||
|
||||
go func() {
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.21.3
|
||||
// protoc v3.21.12
|
||||
// source: bridge.proto
|
||||
|
||||
package grpc
|
||||
@ -4803,8 +4803,8 @@ var file_bridge_proto_rawDesc = []byte{
|
||||
0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x54, 0x4c, 0x53,
|
||||
0x5f, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52,
|
||||
0x4f, 0x52, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x4c, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f,
|
||||
0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0x89,
|
||||
0x1f, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65,
|
||||
0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0xe2,
|
||||
0x20, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65,
|
||||
0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
|
||||
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
@ -5044,19 +5044,33 @@ var file_bridge_proto_rawDesc = []byte{
|
||||
0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70,
|
||||
0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
|
||||
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65,
|
||||
0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e,
|
||||
0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
|
||||
0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74,
|
||||
0x42, 0x75, 0x67, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
|
||||
0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d,
|
||||
0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67,
|
||||
0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72,
|
||||
0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x11, 0x41, 0x75,
|
||||
0x74, 0x6f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12,
|
||||
0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x48, 0x0a, 0x10, 0x4b, 0x42, 0x41, 0x72, 0x74, 0x69, 0x63,
|
||||
0x6c, 0x65, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
|
||||
0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
|
||||
0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72,
|
||||
0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01,
|
||||
0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72,
|
||||
0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
|
||||
0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e,
|
||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@ -5266,67 +5280,73 @@ var file_bridge_proto_depIdxs = []int32{
|
||||
71, // 112: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue
|
||||
71, // 113: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue
|
||||
18, // 114: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest
|
||||
19, // 115: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
|
||||
72, // 116: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
|
||||
71, // 117: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
|
||||
72, // 118: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
|
||||
8, // 119: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
|
||||
72, // 120: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
|
||||
72, // 121: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
|
||||
73, // 122: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
|
||||
72, // 123: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
|
||||
73, // 124: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
|
||||
72, // 125: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
|
||||
73, // 126: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
|
||||
72, // 127: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
|
||||
73, // 128: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
|
||||
72, // 129: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty
|
||||
73, // 130: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue
|
||||
71, // 131: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
|
||||
72, // 132: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
|
||||
71, // 133: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
|
||||
71, // 134: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
|
||||
71, // 135: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
|
||||
71, // 136: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
|
||||
71, // 137: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
|
||||
71, // 138: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
|
||||
72, // 139: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
|
||||
71, // 140: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
|
||||
71, // 141: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
|
||||
72, // 142: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
|
||||
72, // 143: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
|
||||
72, // 144: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
|
||||
72, // 145: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
|
||||
72, // 146: grpc.Bridge.Login:output_type -> google.protobuf.Empty
|
||||
72, // 147: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
|
||||
72, // 148: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
|
||||
72, // 149: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
|
||||
72, // 150: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
|
||||
72, // 151: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
|
||||
72, // 152: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
|
||||
73, // 153: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
|
||||
71, // 154: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
|
||||
72, // 155: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
|
||||
72, // 156: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
|
||||
73, // 157: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
|
||||
12, // 158: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
|
||||
72, // 159: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
|
||||
71, // 160: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
|
||||
73, // 161: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
|
||||
13, // 162: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
|
||||
72, // 163: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
|
||||
71, // 164: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
|
||||
17, // 165: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
|
||||
14, // 166: grpc.Bridge.GetUser:output_type -> grpc.User
|
||||
72, // 167: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
|
||||
72, // 168: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
|
||||
72, // 169: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
|
||||
72, // 170: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
|
||||
72, // 171: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty
|
||||
20, // 172: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent
|
||||
72, // 173: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty
|
||||
117, // [117:174] is the sub-list for method output_type
|
||||
60, // [60:117] is the sub-list for method input_type
|
||||
72, // 115: grpc.Bridge.ReportBugClicked:input_type -> google.protobuf.Empty
|
||||
71, // 116: grpc.Bridge.AutoconfigClicked:input_type -> google.protobuf.StringValue
|
||||
71, // 117: grpc.Bridge.KBArticleClicked:input_type -> google.protobuf.StringValue
|
||||
19, // 118: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
|
||||
72, // 119: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
|
||||
71, // 120: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
|
||||
72, // 121: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
|
||||
8, // 122: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
|
||||
72, // 123: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
|
||||
72, // 124: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
|
||||
73, // 125: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
|
||||
72, // 126: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
|
||||
73, // 127: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
|
||||
72, // 128: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
|
||||
73, // 129: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
|
||||
72, // 130: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
|
||||
73, // 131: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
|
||||
72, // 132: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty
|
||||
73, // 133: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue
|
||||
71, // 134: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
|
||||
72, // 135: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
|
||||
71, // 136: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
|
||||
71, // 137: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
|
||||
71, // 138: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
|
||||
71, // 139: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
|
||||
71, // 140: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
|
||||
71, // 141: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
|
||||
72, // 142: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
|
||||
71, // 143: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
|
||||
71, // 144: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
|
||||
72, // 145: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
|
||||
72, // 146: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
|
||||
72, // 147: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
|
||||
72, // 148: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
|
||||
72, // 149: grpc.Bridge.Login:output_type -> google.protobuf.Empty
|
||||
72, // 150: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
|
||||
72, // 151: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
|
||||
72, // 152: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
|
||||
72, // 153: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
|
||||
72, // 154: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
|
||||
72, // 155: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
|
||||
73, // 156: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
|
||||
71, // 157: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
|
||||
72, // 158: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
|
||||
72, // 159: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
|
||||
73, // 160: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
|
||||
12, // 161: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
|
||||
72, // 162: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
|
||||
71, // 163: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
|
||||
73, // 164: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
|
||||
13, // 165: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
|
||||
72, // 166: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
|
||||
71, // 167: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
|
||||
17, // 168: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
|
||||
14, // 169: grpc.Bridge.GetUser:output_type -> grpc.User
|
||||
72, // 170: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
|
||||
72, // 171: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
|
||||
72, // 172: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
|
||||
72, // 173: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
|
||||
72, // 174: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty
|
||||
72, // 175: grpc.Bridge.ReportBugClicked:output_type -> google.protobuf.Empty
|
||||
72, // 176: grpc.Bridge.AutoconfigClicked:output_type -> google.protobuf.Empty
|
||||
72, // 177: grpc.Bridge.KBArticleClicked:output_type -> google.protobuf.Empty
|
||||
20, // 178: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent
|
||||
72, // 179: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty
|
||||
120, // [120:180] is the sub-list for method output_type
|
||||
60, // [60:120] is the sub-list for method input_type
|
||||
60, // [60:60] is the sub-list for extension type_name
|
||||
60, // [60:60] is the sub-list for extension extendee
|
||||
0, // [0:60] is the sub-list for field type_name
|
||||
|
||||
@ -98,6 +98,11 @@ service Bridge {
|
||||
rpc RemoveUser(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
rpc ConfigureUserAppleMail(ConfigureAppleMailRequest) returns (google.protobuf.Empty);
|
||||
|
||||
// Telemetry
|
||||
rpc ReportBugClicked(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
rpc AutoconfigClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
rpc KBArticleClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
|
||||
// Server -> Client event stream
|
||||
rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
|
||||
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.3
|
||||
// - protoc v3.21.12
|
||||
// source: bridge.proto
|
||||
|
||||
package grpc
|
||||
@ -86,6 +86,10 @@ type BridgeClient interface {
|
||||
LogoutUser(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
RemoveUser(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
ConfigureUserAppleMail(ctx context.Context, in *ConfigureAppleMailRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// Telemetry
|
||||
ReportBugClicked(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
AutoconfigClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
KBArticleClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// Server -> Client event stream
|
||||
RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error)
|
||||
StopEventStream(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
@ -594,6 +598,33 @@ func (c *bridgeClient) ConfigureUserAppleMail(ctx context.Context, in *Configure
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *bridgeClient) ReportBugClicked(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/grpc.Bridge/ReportBugClicked", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *bridgeClient) AutoconfigClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/grpc.Bridge/AutoconfigClicked", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *bridgeClient) KBArticleClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/grpc.Bridge/KBArticleClicked", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *bridgeClient) RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &Bridge_ServiceDesc.Streams[0], "/grpc.Bridge/RunEventStream", opts...)
|
||||
if err != nil {
|
||||
@ -701,6 +732,10 @@ type BridgeServer interface {
|
||||
LogoutUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
|
||||
RemoveUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
|
||||
ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*emptypb.Empty, error)
|
||||
// Telemetry
|
||||
ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
AutoconfigClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
|
||||
KBArticleClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
|
||||
// Server -> Client event stream
|
||||
RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error
|
||||
StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
@ -876,6 +911,15 @@ func (UnimplementedBridgeServer) RemoveUser(context.Context, *wrapperspb.StringV
|
||||
func (UnimplementedBridgeServer) ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ConfigureUserAppleMail not implemented")
|
||||
}
|
||||
func (UnimplementedBridgeServer) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReportBugClicked not implemented")
|
||||
}
|
||||
func (UnimplementedBridgeServer) AutoconfigClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AutoconfigClicked not implemented")
|
||||
}
|
||||
func (UnimplementedBridgeServer) KBArticleClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method KBArticleClicked not implemented")
|
||||
}
|
||||
func (UnimplementedBridgeServer) RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method RunEventStream not implemented")
|
||||
}
|
||||
@ -1885,6 +1929,60 @@ func _Bridge_ConfigureUserAppleMail_Handler(srv interface{}, ctx context.Context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Bridge_ReportBugClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(BridgeServer).ReportBugClicked(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/grpc.Bridge/ReportBugClicked",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(BridgeServer).ReportBugClicked(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Bridge_AutoconfigClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(wrapperspb.StringValue)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(BridgeServer).AutoconfigClicked(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/grpc.Bridge/AutoconfigClicked",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(BridgeServer).AutoconfigClicked(ctx, req.(*wrapperspb.StringValue))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Bridge_KBArticleClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(wrapperspb.StringValue)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(BridgeServer).KBArticleClicked(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/grpc.Bridge/KBArticleClicked",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(BridgeServer).KBArticleClicked(ctx, req.(*wrapperspb.StringValue))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Bridge_RunEventStream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(EventStreamRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
@ -2151,6 +2249,18 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ConfigureUserAppleMail",
|
||||
Handler: _Bridge_ConfigureUserAppleMail_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReportBugClicked",
|
||||
Handler: _Bridge_ReportBugClicked_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AutoconfigClicked",
|
||||
Handler: _Bridge_AutoconfigClicked_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "KBArticleClicked",
|
||||
Handler: _Bridge_KBArticleClicked_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "StopEventStream",
|
||||
Handler: _Bridge_StopEventStream_Handler,
|
||||
|
||||
40
internal/frontend/grpc/service_telemetry.go
Normal file
40
internal/frontend/grpc/service_telemetry.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
func (s *Service) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
s.bridge.ReportBugClicked()
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
s.bridge.AutoconfigUsed(client.Value)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *Service) KBArticleClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
s.bridge.KBArticleOpened(article.Value)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
@ -188,6 +188,16 @@ func (l *Locations) ProvideUpdatesPath() (string, error) {
|
||||
return l.getUpdatesPath(), nil
|
||||
}
|
||||
|
||||
// ProvideStatsPath returns a location for statistics files (e.g. ~/.local/share/<company>/<app>/stats).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideStatsPath() (string, error) {
|
||||
if err := os.MkdirAll(l.getStatsPath(), 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return l.getStatsPath(), nil
|
||||
}
|
||||
|
||||
func (l *Locations) getGluonCachePath() string {
|
||||
return filepath.Join(l.userData, "gluon")
|
||||
}
|
||||
@ -216,6 +226,10 @@ func (l *Locations) getUpdatesPath() string {
|
||||
return filepath.Join(l.userData, "updates")
|
||||
}
|
||||
|
||||
func (l *Locations) getStatsPath() string {
|
||||
return filepath.Join(l.userData, "stats")
|
||||
}
|
||||
|
||||
// Clear removes everything except the lock and update files.
|
||||
func (l *Locations) Clear(except ...string) error {
|
||||
return files.Remove(
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func clearLogs(logDir string, maxLogs int, maxCrashes int) error {
|
||||
files, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read log directory: %w", err)
|
||||
}
|
||||
|
||||
names := xslices.Map(files, func(file fs.DirEntry) string {
|
||||
return file.Name()
|
||||
})
|
||||
|
||||
// Remove old logs.
|
||||
removeOldLogs(logDir, xslices.Filter(names, func(name string) bool {
|
||||
return MatchLogName(name) && !MatchStackTraceName(name)
|
||||
}), maxLogs)
|
||||
|
||||
// Remove old stack traces.
|
||||
removeOldLogs(logDir, xslices.Filter(names, func(name string) bool {
|
||||
return MatchLogName(name) && MatchStackTraceName(name)
|
||||
}), maxCrashes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeOldLogs(dir string, names []string, max int) {
|
||||
if count := len(names); count <= max {
|
||||
return
|
||||
}
|
||||
|
||||
// Sort by timestamp, oldest first.
|
||||
slices.SortFunc(names, func(a, b string) bool {
|
||||
return getLogTime(a) < getLogTime(b)
|
||||
})
|
||||
|
||||
for _, path := range xslices.Map(names[:len(names)-max], func(name string) string { return filepath.Join(dir, name) }) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
logrus.WithError(err).WithField("path", path).Warn("Failed to remove old log file")
|
||||
}
|
||||
}
|
||||
}
|
||||
170
internal/logging/compression.go
Normal file
170
internal/logging/compression.go
Normal file
@ -0,0 +1,170 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoInputFile = errors.New("no file was provided to put in the archive")
|
||||
errCannotFitAnyFile = errors.New("no file can fit in the archive")
|
||||
)
|
||||
|
||||
// zipFilesWithMaxSize compress the maximum number of files from the given list that can fit a ZIP archive file whose size does not exceed
|
||||
// maxSize. Input files are taken in order and the function returns as soon as the next file cannot fit, even if another file further in the list
|
||||
// may fit. The function return the number of files that were included in the archive. The files included are filePath[:fileCount].
|
||||
func zipFilesWithMaxSize(filePaths []string, maxSize int64) (buffer *bytes.Buffer, fileCount int, err error) {
|
||||
if len(filePaths) == 0 {
|
||||
return nil, 0, errNoInputFile
|
||||
}
|
||||
buffer, err = createZipFromFile(filePaths[0])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if int64(buffer.Len()) > maxSize {
|
||||
return nil, 0, errCannotFitAnyFile
|
||||
}
|
||||
|
||||
fileCount = 1
|
||||
var previousBuffer *bytes.Buffer
|
||||
|
||||
for _, filePath := range filePaths[1:] {
|
||||
previousBuffer = cloneBuffer(buffer)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(buffer.Bytes()), int64(len(buffer.Bytes())))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
buffer, err = addFileToArchive(zipReader, filePath)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if int64(buffer.Len()) > maxSize {
|
||||
return previousBuffer, fileCount, nil
|
||||
}
|
||||
|
||||
fileCount++
|
||||
}
|
||||
|
||||
return buffer, fileCount, nil
|
||||
}
|
||||
|
||||
// cloneBuffer clones a buffer.
|
||||
func cloneBuffer(buffer *bytes.Buffer) *bytes.Buffer {
|
||||
return bytes.NewBuffer(bytes.Clone(buffer.Bytes()))
|
||||
}
|
||||
|
||||
// createZip creates a zip archive containing a single file.
|
||||
func createZipFromFile(filePath string) (*bytes.Buffer, error) {
|
||||
file, err := os.Open(filePath) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
return createZip(file, filepath.Base(filePath))
|
||||
}
|
||||
|
||||
// createZip creates a zip file containing a file names filename with content read from reader.
|
||||
func createZip(reader io.Reader, filename string) (*bytes.Buffer, error) {
|
||||
b := bytes.NewBuffer(make([]byte, 0))
|
||||
zipWriter := zip.NewWriter(b)
|
||||
|
||||
f, err := zipWriter.Create(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(f, reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = zipWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// addToArchive adds a file to an archive. Because go zip package does not support adding a file to existing (closed) archive file, the way to do it
|
||||
// is to create a new archive copy the raw content of the archive to the new one and add the new file before closing the archive.
|
||||
func addFileToArchive(zipReader *zip.Reader, filePath string) (*bytes.Buffer, error) {
|
||||
file, err := os.Open(filePath) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
return addToArchive(zipReader, file, filepath.Base(filePath))
|
||||
}
|
||||
|
||||
// addToArchive adds data from a reader to a file in an archive.
|
||||
func addToArchive(zipReader *zip.Reader, reader io.Reader, filename string) (*bytes.Buffer, error) {
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
zipWriter := zip.NewWriter(buffer)
|
||||
|
||||
if err := copyZipContent(zipReader, zipWriter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := zipWriter.Create(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := zipWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
// copyZipContent copies the content of a zip to another without recompression.
|
||||
func copyZipContent(zipReader *zip.Reader, zipWriter *zip.Writer) error {
|
||||
for _, zipItem := range zipReader.File {
|
||||
itemReader, err := zipItem.OpenRaw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header := zipItem.FileHeader
|
||||
targetItem, err := zipWriter.CreateRaw(&header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(targetItem, itemReader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
134
internal/logging/compression_test.go
Normal file
134
internal/logging/compression_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogging_LogCompression(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
files := []fileInfo{
|
||||
{filepath.Join(dir, "1.log"), 100000},
|
||||
{filepath.Join(dir, "2.log"), 200000},
|
||||
{filepath.Join(dir, "3.log"), 300000},
|
||||
}
|
||||
|
||||
// Files will have a content and size (relative to the zip format overhead) that ensure a compression ratio of roughly 2:1.
|
||||
createRandomFiles(t, files)
|
||||
paths := xslices.Map(files, func(fileInfo fileInfo) string { return fileInfo.filename })
|
||||
|
||||
// Case 1: no input file.
|
||||
_, _, err := zipFilesWithMaxSize([]string{}, 10)
|
||||
require.ErrorIs(t, err, errNoInputFile)
|
||||
|
||||
// Case 2: limit to low, no file can be included.
|
||||
_, _, err = zipFilesWithMaxSize(paths, 100)
|
||||
require.ErrorIs(t, err, errCannotFitAnyFile)
|
||||
|
||||
// case 3: 1 file fits.
|
||||
buffer, fileCount, err := zipFilesWithMaxSize(paths, 100000)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, fileCount)
|
||||
checkZipFileContent(t, buffer, paths[0:1])
|
||||
|
||||
// case 4: 2 files fit.
|
||||
buffer, fileCount, err = zipFilesWithMaxSize(paths, 200000)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, fileCount)
|
||||
checkZipFileContent(t, buffer, paths[0:2])
|
||||
|
||||
// case 5: 3 files fit.
|
||||
buffer, fileCount, err = zipFilesWithMaxSize(paths, 500000)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, fileCount)
|
||||
checkZipFileContent(t, buffer, paths)
|
||||
}
|
||||
|
||||
func createRandomFiles(t *testing.T, files []fileInfo) {
|
||||
// The file is crafted to have a compression ratio of roughly 2:1 by filling the first half with random data, and the second with zeroes.
|
||||
for _, file := range files {
|
||||
randomData := make([]byte, file.size)
|
||||
_, err := rand.Read(randomData[:file.size/2])
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(file.filename, randomData, 0660))
|
||||
}
|
||||
}
|
||||
|
||||
func checkZipFileContent(t *testing.T, buffer *bytes.Buffer, expectedFilePaths []string) {
|
||||
dir := t.TempDir()
|
||||
count := unzipFile(t, buffer, dir)
|
||||
require.Equal(t, len(expectedFilePaths), count)
|
||||
for _, file := range expectedFilePaths {
|
||||
checkFilesAreIdentical(t, file, filepath.Join(dir, filepath.Base(file)))
|
||||
}
|
||||
}
|
||||
|
||||
func unzipFile(t *testing.T, buffer *bytes.Buffer, dir string) int {
|
||||
reader, err := zip.NewReader(bytes.NewReader(buffer.Bytes()), int64(len(buffer.Bytes())))
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range reader.File {
|
||||
info := f.FileInfo()
|
||||
require.False(t, info.IsDir())
|
||||
require.Equal(t, filepath.Base(info.Name()), info.Name()) // no sub-folder
|
||||
extractFileFromZip(t, f, filepath.Join(dir, f.Name))
|
||||
}
|
||||
|
||||
return len(reader.File)
|
||||
}
|
||||
|
||||
func extractFileFromZip(t *testing.T, zip *zip.File, path string) {
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zip.Mode())
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
reader, err := zip.Open()
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
_, err = io.Copy(file, reader)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func checkFilesAreIdentical(t *testing.T, path1, path2 string) {
|
||||
require.EqualValues(t, sha256Sum(t, path1), sha256Sum(t, path2))
|
||||
}
|
||||
|
||||
func sha256Sum(t *testing.T, path string) []byte {
|
||||
f, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
hash := sha256.New()
|
||||
_, err = io.Copy(hash, f)
|
||||
require.NoError(t, err)
|
||||
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
@ -23,16 +23,15 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func DumpStackTrace(logsPath string) crash.RecoveryAction {
|
||||
func DumpStackTrace(logsPath string, sessionID SessionID, appName AppName) crash.RecoveryAction {
|
||||
return func(r interface{}) error {
|
||||
file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision))
|
||||
file := filepath.Join(logsPath, getStackTraceName(sessionID, appName, constants.Version, constants.Tag))
|
||||
|
||||
f, err := os.OpenFile(filepath.Clean(file), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600)
|
||||
if err != nil {
|
||||
@ -53,10 +52,10 @@ func DumpStackTrace(logsPath string) crash.RecoveryAction {
|
||||
}
|
||||
}
|
||||
|
||||
func getStackTraceName(version, revision string) string {
|
||||
return fmt.Sprintf("v%v_%v_crash_%v.log", version, revision, time.Now().Unix())
|
||||
func getStackTraceName(sessionID SessionID, appName AppName, version, tag string) string {
|
||||
return fmt.Sprintf("%v_%v_000_v%v_%v_crash.log", sessionID, appName, version, tag)
|
||||
}
|
||||
|
||||
func MatchStackTraceName(name string) bool {
|
||||
return regexp.MustCompile(`^v.*_crash_.*\.log$`).MatchString(name)
|
||||
return regexp.MustCompile(`^\d{8}_\d{9}_.*_000_.*_crash\.log$`).MatchString(name)
|
||||
}
|
||||
|
||||
32
internal/logging/crash_test.go
Normal file
32
internal/logging/crash_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogging_MatchStackTraceName(t *testing.T) {
|
||||
filename := getStackTraceName(NewSessionID(), constants.AppName, constants.Version, constants.Tag)
|
||||
require.True(t, len(filename) > 0)
|
||||
require.True(t, MatchStackTraceName(filename))
|
||||
require.False(t, MatchStackTraceName("Invalid.log"))
|
||||
}
|
||||
@ -18,32 +18,37 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLogSize defines the maximum log size we should permit: 5 MB
|
||||
// DefaultMaxLogFileSize defines the maximum log size we should permit: 5 MB
|
||||
//
|
||||
// The Zendesk limit for an attachement is 50MB and this is what will
|
||||
// The Zendesk limit for an attachment is 50MB and this is what will
|
||||
// be allowed via the API. However, if that fails for some reason, the
|
||||
// fallback is sending the report via email, which has a limit of 10mb
|
||||
// total or 7MB per file. Since we can produce up to 6 logs, and we
|
||||
// compress all the files (avarage compression - 80%), we need to have
|
||||
// a limit of 30MB total before compression, hence 5MB per log file.
|
||||
MaxLogSize = 5 * 1024 * 1024
|
||||
// total or 7MB per file.
|
||||
DefaultMaxLogFileSize = 5 * 1024 * 1024
|
||||
)
|
||||
|
||||
// MaxLogs defines how many log files should be kept.
|
||||
MaxLogs = 10
|
||||
type AppName string
|
||||
|
||||
const (
|
||||
BridgeShortAppName AppName = "bri"
|
||||
LauncherShortAppName AppName = "lau"
|
||||
GUIShortAppName AppName = "gui"
|
||||
)
|
||||
|
||||
type coloredStdOutHook struct {
|
||||
@ -82,7 +87,9 @@ func (cs *coloredStdOutHook) Fire(entry *logrus.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Init(logsPath, level string) error {
|
||||
// Init Initialize logging. Log files are rotated when their size exceeds rotationSize. if pruningSize >= 0, pruning occurs using
|
||||
// the default pruning algorithm.
|
||||
func Init(logsPath string, sessionID SessionID, appName AppName, rotationSize, pruningSize int64, level string) (io.Closer, error) {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableColors: true,
|
||||
FullTimestamp: true,
|
||||
@ -91,20 +98,80 @@ func Init(logsPath, level string) error {
|
||||
|
||||
logrus.AddHook(newColoredStdOutHook())
|
||||
|
||||
rotator, err := NewRotator(MaxLogSize, func() (io.WriteCloser, error) {
|
||||
if err := clearLogs(logsPath, MaxLogs, MaxLogs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.Create(filepath.Join(logsPath, getLogName(constants.Version, constants.Revision))) //nolint:gosec // G304
|
||||
})
|
||||
rotator, err := NewDefaultRotator(logsPath, sessionID, appName, rotationSize, pruningSize)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.SetOutput(rotator)
|
||||
|
||||
return setLevel(level)
|
||||
return rotator, setLevel(level)
|
||||
}
|
||||
|
||||
// Close closes the log file. if closer is nil, no error is reported.
|
||||
func Close(closer io.Closer) error {
|
||||
if closer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.SetOutput(os.Stdout)
|
||||
return closer.Close()
|
||||
}
|
||||
|
||||
// ZipLogsForBugReport returns an archive containing the logs for bug report.
|
||||
func ZipLogsForBugReport(logsPath string, maxSessionCount int, maxZipSize int64) (*bytes.Buffer, error) {
|
||||
paths, err := getOrderedLogFileListForBugReport(logsPath, maxSessionCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer, _, err := zipFilesWithMaxSize(paths, maxZipSize)
|
||||
return buffer, err
|
||||
}
|
||||
|
||||
// getOrderedLogFileListForBugReport returns the ordered list of log file paths to include in the user triggered bug reports. Only the last
|
||||
// maxSessionCount sessions are included. Priorities:
|
||||
// - session in chronologically descending order.
|
||||
// - for each session: last 2 bridge logs, first bridge log, gui logs, launcher logs, all other bridge logs.
|
||||
func getOrderedLogFileListForBugReport(logsPath string, maxSessionCount int) ([]string, error) {
|
||||
sessionInfoList, err := buildSessionInfoList(logsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sortedSessions := maps.Values(sessionInfoList)
|
||||
slices.SortFunc(sortedSessions, func(lhs, rhs *sessionInfo) bool { return lhs.sessionID > rhs.sessionID })
|
||||
count := len(sortedSessions)
|
||||
if count > maxSessionCount {
|
||||
sortedSessions = sortedSessions[:maxSessionCount]
|
||||
}
|
||||
|
||||
filePathFunc := func(logFileInfo logFileInfo) string { return filepath.Join(logsPath, logFileInfo.filename) }
|
||||
|
||||
var result []string
|
||||
for _, session := range sortedSessions {
|
||||
bridgeLogCount := len(session.bridgeLogs)
|
||||
if bridgeLogCount > 0 {
|
||||
result = append(result, filepath.Join(logsPath, session.bridgeLogs[bridgeLogCount-1].filename))
|
||||
}
|
||||
if bridgeLogCount > 1 {
|
||||
result = append(result, filepath.Join(logsPath, session.bridgeLogs[bridgeLogCount-2].filename))
|
||||
}
|
||||
if bridgeLogCount > 2 {
|
||||
result = append(result, filepath.Join(logsPath, session.bridgeLogs[0].filename))
|
||||
}
|
||||
if len(session.guiLogs) > 0 {
|
||||
result = append(result, xslices.Map(session.guiLogs, filePathFunc)...)
|
||||
}
|
||||
if len(session.launcherLogs) > 0 {
|
||||
result = append(result, xslices.Map(session.launcherLogs, filePathFunc)...)
|
||||
}
|
||||
if bridgeLogCount > 3 {
|
||||
result = append(result, xslices.Map(session.bridgeLogs[1:bridgeLogCount-2], filePathFunc)...)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// setLevel will change the level of logging and in case of Debug or Trace
|
||||
@ -137,34 +204,51 @@ func setLevel(level string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLogName(version, revision string) string {
|
||||
return fmt.Sprintf("v%v_%v_%v.log", version, revision, time.Now().Unix())
|
||||
}
|
||||
func getLogSessionID(filename string) (SessionID, error) {
|
||||
re := regexp.MustCompile(`^(?P<sessionID>\d{8}_\d{9})_.*\.log$`)
|
||||
|
||||
func getLogTime(name string) int {
|
||||
re := regexp.MustCompile(`^v.*_.*_(?P<timestamp>\d+).log$`)
|
||||
|
||||
match := re.FindStringSubmatch(name)
|
||||
match := re.FindStringSubmatch(filename)
|
||||
|
||||
errInvalidFileName := errors.New("log file name is invalid")
|
||||
if len(match) == 0 {
|
||||
logrus.Warn("Could not parse log name: ", name)
|
||||
return 0
|
||||
logrus.WithField("filename", filename).Warn("Could not parse log filename")
|
||||
return "", errInvalidFileName
|
||||
}
|
||||
|
||||
timestamp, err := strconv.Atoi(match[re.SubexpIndex("timestamp")])
|
||||
index := re.SubexpIndex("sessionID")
|
||||
if index < 0 {
|
||||
logrus.WithField("filename", filename).Warn("Could not parse log filename")
|
||||
return "", errInvalidFileName
|
||||
}
|
||||
|
||||
return SessionID(match[index]), nil
|
||||
}
|
||||
|
||||
func getLogTime(filename string) time.Time {
|
||||
sessionID, err := getLogSessionID(filename)
|
||||
if err != nil {
|
||||
return 0
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
return timestamp
|
||||
return sessionID.toTime()
|
||||
}
|
||||
|
||||
func MatchLogName(name string) bool {
|
||||
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
|
||||
// MatchBridgeLogName return true iff filename is a bridge log filename.
|
||||
func MatchBridgeLogName(filename string) bool {
|
||||
return matchLogName(filename, BridgeShortAppName)
|
||||
}
|
||||
|
||||
func MatchGUILogName(name string) bool {
|
||||
return regexp.MustCompile(`^gui_v.*\.log$`).MatchString(name)
|
||||
// MatchGUILogName return true iff filename is a bridge-gui log filename.
|
||||
func MatchGUILogName(filename string) bool {
|
||||
return matchLogName(filename, GUIShortAppName)
|
||||
}
|
||||
|
||||
// MatchLauncherLogName return true iff filename is a launcher log filename.
|
||||
func MatchLauncherLogName(filename string) bool {
|
||||
return matchLogName(filename, LauncherShortAppName)
|
||||
}
|
||||
|
||||
func matchLogName(logName string, appName AppName) bool {
|
||||
return regexp.MustCompile(`^\d{8}_\d{9}_\Q` + string(appName) + `\E_\d{3}_.*\.log$`).MatchString(logName)
|
||||
}
|
||||
|
||||
type logKey string
|
||||
@ -180,12 +264,3 @@ func WithLogrusField(ctx context.Context, key string, value interface{}) context
|
||||
fields[key] = value
|
||||
return context.WithValue(ctx, logrusFields, fields)
|
||||
}
|
||||
|
||||
func LogFromContext(ctx context.Context) *logrus.Entry {
|
||||
fields, ok := ctx.Value(logrusFields).(logrus.Fields)
|
||||
if !ok || fields == nil {
|
||||
return logrus.WithField("ctx", "empty")
|
||||
}
|
||||
|
||||
return logrus.WithFields(fields)
|
||||
}
|
||||
|
||||
@ -22,62 +22,84 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestClearLogs tests that cearLogs removes only bridge old log files keeping last three of them.
|
||||
func TestClearLogs(t *testing.T) {
|
||||
func TestLogging_GetLogTime(t *testing.T) {
|
||||
sessionID := NewSessionID()
|
||||
fp := defaultFileProvider(os.TempDir(), sessionID, "bridge-test")
|
||||
wc, err := fp(0)
|
||||
require.NoError(t, err)
|
||||
file, ok := wc.(*os.File)
|
||||
require.True(t, ok)
|
||||
|
||||
sessionIDTime := sessionID.toTime()
|
||||
require.False(t, sessionIDTime.IsZero())
|
||||
logTime := getLogTime(filepath.Base(file.Name()))
|
||||
require.False(t, logTime.IsZero())
|
||||
require.Equal(t, sessionIDTime, logTime)
|
||||
}
|
||||
|
||||
func TestLogging_MatchLogName(t *testing.T) {
|
||||
bridgeLog := "20230602_094633102_bri_000_v3.0.99+git_5b650b1be3.log"
|
||||
crashLog := "20230602_094633102_bri_000_v3.0.99+git_5b650b1be3_crash.log"
|
||||
guiLog := "20230602_094633102_gui_000_v3.0.99+git_5b650b1be3.log"
|
||||
launcherLog := "20230602_094633102_lau_000_v3.0.99+git_5b650b1be3.log"
|
||||
require.True(t, MatchBridgeLogName(bridgeLog))
|
||||
require.False(t, MatchGUILogName(bridgeLog))
|
||||
require.False(t, MatchLauncherLogName(bridgeLog))
|
||||
require.True(t, MatchBridgeLogName(crashLog))
|
||||
require.False(t, MatchGUILogName(crashLog))
|
||||
require.False(t, MatchLauncherLogName(crashLog))
|
||||
require.False(t, MatchBridgeLogName(guiLog))
|
||||
require.True(t, MatchGUILogName(guiLog))
|
||||
require.False(t, MatchLauncherLogName(guiLog))
|
||||
require.False(t, MatchBridgeLogName(launcherLog))
|
||||
require.False(t, MatchGUILogName(launcherLog))
|
||||
require.True(t, MatchLauncherLogName(launcherLog))
|
||||
}
|
||||
|
||||
func TestLogging_GetOrderedLogFileListForBugReport(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create some old log files.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "other.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.7_debe87f2f5_0000000001.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.8_debe87f2f5_0000000002.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.9_debe87f2f5_0000000003.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.0_debe87f2f5_0000000004.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.1_debe87f2f5_0000000005.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.2_debe87f2f5_0000000006.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.3_debe87f2f5_0000000007.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.4_debe87f2f5_0000000008.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.5_debe87f2f5_0000000009.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.6_debe87f2f5_0000000010.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.7_debe87f2f5_0000000011.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.8_debe87f2f5_0000000012.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.12_debe87f2f5_0000000013.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.9_debe87f2f5_0000000014.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.10_debe87f2f5_0000000015.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.11_debe87f2f5_0000000016.log"), []byte("Hello"), 0o755))
|
||||
|
||||
// Clear the logs.
|
||||
require.NoError(t, clearLogs(dir, 3, 0))
|
||||
|
||||
// We should only clear matching files, and keep the 3 most recent ones.
|
||||
checkFileNames(t, dir, []string{
|
||||
"other.log",
|
||||
"v2.5.9_debe87f2f5_0000000014.log",
|
||||
"v2.5.10_debe87f2f5_0000000015.log",
|
||||
"v2.5.11_debe87f2f5_0000000016.log",
|
||||
})
|
||||
}
|
||||
|
||||
func checkFileNames(t *testing.T, dir string, expectedFileNames []string) {
|
||||
require.ElementsMatch(t, expectedFileNames, getFileNames(t, dir))
|
||||
}
|
||||
|
||||
func getFileNames(t *testing.T, dir string) []string {
|
||||
files, err := os.ReadDir(dir)
|
||||
filePaths, err := getOrderedLogFileListForBugReport(dir, 3)
|
||||
require.NoError(t, err)
|
||||
require.True(t, len(filePaths) == 0)
|
||||
|
||||
fileNames := []string{}
|
||||
for _, file := range files {
|
||||
fileNames = append(fileNames, file.Name())
|
||||
if file.IsDir() {
|
||||
subDir := filepath.Join(dir, file.Name())
|
||||
subFileNames := getFileNames(t, subDir)
|
||||
for _, subFileName := range subFileNames {
|
||||
fileNames = append(fileNames, file.Name()+"/"+subFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileNames
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "invalid.log"), []byte("proton"), 0660))
|
||||
|
||||
_ = createDummySession(t, dir, 1000, 250, 500, 3000)
|
||||
sessionID1 := createDummySession(t, dir, 1000, 250, 500, 500)
|
||||
sessionID2 := createDummySession(t, dir, 1000, 250, 500, 500)
|
||||
sessionID3 := createDummySession(t, dir, 1000, 250, 500, 4500)
|
||||
|
||||
filePaths, err = getOrderedLogFileListForBugReport(dir, 3)
|
||||
fileSuffix := "_v" + constants.Version + "_" + constants.Tag + ".log"
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, []string{
|
||||
filepath.Join(dir, string(sessionID3)+"_bri_004"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID3)+"_bri_003"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID3)+"_bri_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID3)+"_gui_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID3)+"_lau_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID3)+"_bri_001"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID3)+"_bri_002"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID2)+"_bri_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID2)+"_gui_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID2)+"_lau_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID1)+"_bri_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID1)+"_gui_000"+fileSuffix),
|
||||
filepath.Join(dir, string(sessionID1)+"_lau_000"+fileSuffix),
|
||||
}, filePaths)
|
||||
}
|
||||
|
||||
func TestLogging_Close(t *testing.T) {
|
||||
d := t.TempDir()
|
||||
closer, err := Init(d, NewSessionID(), constants.AppName, 1, DefaultPruningSize, "debug")
|
||||
require.NoError(t, err)
|
||||
logrus.Debug("Test") // because we set max log file size to 1, this will force a rotation of the log file.
|
||||
require.NotNil(t, closer)
|
||||
require.NoError(t, closer.Close())
|
||||
}
|
||||
|
||||
230
internal/logging/pruning.go
Normal file
230
internal/logging/pruning.go
Normal file
@ -0,0 +1,230 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultPruningSize = 1024 * 1024 * 200
|
||||
NoPruning = -1
|
||||
)
|
||||
|
||||
type Pruner func() (failureCount int, err error)
|
||||
|
||||
type logFileInfo struct {
|
||||
filename string
|
||||
size int64
|
||||
}
|
||||
|
||||
type sessionInfo struct {
|
||||
dir string
|
||||
sessionID SessionID
|
||||
launcherLogs []logFileInfo
|
||||
guiLogs []logFileInfo
|
||||
bridgeLogs []logFileInfo
|
||||
}
|
||||
|
||||
func defaultPruner(logsDir string, currentSessionID SessionID, pruningSize int64) func() (failureCount int, err error) {
|
||||
return func() (int, error) {
|
||||
return pruneLogs(logsDir, currentSessionID, pruningSize)
|
||||
}
|
||||
}
|
||||
|
||||
func nullPruner() (failureCount int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// DefaultPruner gets rid of the older log files according to the following policy:
|
||||
// - We will limit the total size of the log files to roughly pruningSize.
|
||||
// The current session is included in this quota, in order not to grow indefinitely on setups where bridge can run uninterrupted for months.
|
||||
// - If the current session's log files total size is above the pruning size, we delete all other sessions log. For the current we keep
|
||||
// launcher and gui log (they're limited to a few kb at most by nature), and we have n bridge log files,
|
||||
// We keep the first and list log file (startup information contains relevant information, notably the SentryID), and start deleting the other
|
||||
// starting with the oldest until the total size drops below the pruning size.
|
||||
// - Otherwise: If the total size of log files for all sessions exceeds pruningSize, sessions gets deleted starting with the oldest, until the size
|
||||
// drops below the pruning size. Sessions are treated atomically. Current session is left untouched in that case.
|
||||
func pruneLogs(logDir string, currentSessionID SessionID, pruningSize int64) (failureCount int, err error) {
|
||||
sessionInfoList, err := buildSessionInfoList(logDir)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// we want total size to include the current session.
|
||||
totalSize := xslices.Reduce(maps.Values(sessionInfoList), int64(0), func(sum int64, info *sessionInfo) int64 { return sum + info.size() })
|
||||
if totalSize <= pruningSize {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
currentSessionInfo, ok := sessionInfoList[currentSessionID]
|
||||
if ok {
|
||||
delete(sessionInfoList, currentSessionID)
|
||||
|
||||
if currentSessionInfo.size() > pruningSize {
|
||||
// current session is already too big. We delete all other sessions and prune the current session.
|
||||
for _, session := range sessionInfoList {
|
||||
failureCount += session.deleteFiles()
|
||||
}
|
||||
|
||||
failureCount += currentSessionInfo.pruneAsCurrentSession(pruningSize)
|
||||
return failureCount, nil
|
||||
}
|
||||
}
|
||||
|
||||
// current session size if below max size, so we erase older session starting with the eldest until we go below maxFileSize
|
||||
sortedSessions := maps.Values(sessionInfoList)
|
||||
slices.SortFunc(sortedSessions, func(lhs, rhs *sessionInfo) bool { return lhs.sessionID < rhs.sessionID })
|
||||
for _, sessionInfo := range sortedSessions {
|
||||
totalSize -= sessionInfo.size()
|
||||
failureCount += sessionInfo.deleteFiles()
|
||||
if totalSize <= pruningSize {
|
||||
return failureCount, nil
|
||||
}
|
||||
}
|
||||
|
||||
return failureCount, nil
|
||||
}
|
||||
|
||||
func newSessionInfo(dir string, sessionID SessionID) (*sessionInfo, error) {
|
||||
paths, err := filepath.Glob(filepath.Join(dir, string(sessionID)+"_*.log"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rx := regexp.MustCompile(`^\Q` + string(sessionID) + `\E_([^_]*)_\d+_.*\.log$`)
|
||||
|
||||
result := sessionInfo{sessionID: sessionID, dir: dir}
|
||||
for _, path := range paths {
|
||||
filename := filepath.Base(path)
|
||||
match := rx.FindStringSubmatch(filename)
|
||||
if len(match) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
stats, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fileInfo := logFileInfo{
|
||||
filename: filename,
|
||||
size: stats.Size(),
|
||||
}
|
||||
|
||||
switch AppName(match[1]) {
|
||||
case LauncherShortAppName:
|
||||
result.launcherLogs = append(result.launcherLogs, fileInfo)
|
||||
case GUIShortAppName:
|
||||
result.guiLogs = append(result.guiLogs, fileInfo)
|
||||
case BridgeShortAppName:
|
||||
result.bridgeLogs = append(result.bridgeLogs, fileInfo)
|
||||
}
|
||||
}
|
||||
|
||||
lessFunc := func(lhs, rhs logFileInfo) bool { return strings.Compare(lhs.filename, rhs.filename) < 0 }
|
||||
slices.SortFunc(result.launcherLogs, lessFunc)
|
||||
slices.SortFunc(result.guiLogs, lessFunc)
|
||||
slices.SortFunc(result.bridgeLogs, lessFunc)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *sessionInfo) size() int64 {
|
||||
summer := func(accum int64, logInfo logFileInfo) int64 { return accum + logInfo.size }
|
||||
size := xslices.Reduce(s.launcherLogs, 0, summer)
|
||||
size = xslices.Reduce(s.guiLogs, size, summer)
|
||||
size = xslices.Reduce(s.bridgeLogs, size, summer)
|
||||
return size
|
||||
}
|
||||
|
||||
func (s *sessionInfo) deleteFiles() (failureCount int) {
|
||||
var allLogs []logFileInfo
|
||||
allLogs = append(allLogs, s.launcherLogs...)
|
||||
allLogs = append(allLogs, s.guiLogs...)
|
||||
allLogs = append(allLogs, s.bridgeLogs...)
|
||||
|
||||
for _, log := range allLogs {
|
||||
if err := os.Remove(filepath.Join(s.dir, log.filename)); err != nil {
|
||||
failureCount++
|
||||
}
|
||||
}
|
||||
|
||||
return failureCount
|
||||
}
|
||||
|
||||
func (s *sessionInfo) pruneAsCurrentSession(pruningSize int64) (failureCount int) {
|
||||
// when pruning the current session, we keep the launcher and GUI logs, the first and last bridge log file
|
||||
// and we delete intermediate bridge logs until the size constraint is satisfied (or there nothing left to delete).
|
||||
if len(s.bridgeLogs) < 3 {
|
||||
return 0
|
||||
}
|
||||
|
||||
size := s.size()
|
||||
if size <= pruningSize {
|
||||
return 0
|
||||
}
|
||||
|
||||
for _, fileInfo := range s.bridgeLogs[1 : len(s.bridgeLogs)-1] {
|
||||
if err := os.Remove(filepath.Join(s.dir, fileInfo.filename)); err != nil {
|
||||
failureCount++
|
||||
}
|
||||
size -= fileInfo.size
|
||||
if size <= pruningSize {
|
||||
return failureCount
|
||||
}
|
||||
}
|
||||
|
||||
return failureCount
|
||||
}
|
||||
|
||||
func buildSessionInfoList(dir string) (map[SessionID]*sessionInfo, error) {
|
||||
result := make(map[SessionID]*sessionInfo)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
rx := regexp.MustCompile(`^(\d{8}_\d{9})_.*\.log$`)
|
||||
match := rx.FindStringSubmatch(entry.Name())
|
||||
if match == nil || len(match) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
sessionID := SessionID(match[1])
|
||||
if _, ok := result[sessionID]; !ok {
|
||||
sessionInfo, err := newSessionInfo(dir, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[sessionID] = sessionInfo
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
236
internal/logging/pruning_test.go
Normal file
236
internal/logging/pruning_test.go
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
filename string
|
||||
size int64
|
||||
}
|
||||
|
||||
var logFileSuffix = "_v" + constants.Version + "_" + constants.Tag + ".log"
|
||||
|
||||
func TestLogging_Pruning(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
const maxLogSize = 100
|
||||
sessionID1 := createDummySession(t, dir, maxLogSize, 50, 50, 250)
|
||||
sessionID2 := createDummySession(t, dir, maxLogSize, 100, 100, 350)
|
||||
sessionID3 := createDummySession(t, dir, maxLogSize, 150, 100, 350)
|
||||
|
||||
// Expected files per session
|
||||
session1Files := []fileInfo{
|
||||
{filename: string(sessionID1) + "_lau_000" + logFileSuffix, size: 50},
|
||||
{filename: string(sessionID1) + "_gui_000" + logFileSuffix, size: 50},
|
||||
{filename: string(sessionID1) + "_bri_000" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID1) + "_bri_001" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID1) + "_bri_002" + logFileSuffix, size: 50},
|
||||
}
|
||||
|
||||
session2Files := []fileInfo{
|
||||
{filename: string(sessionID2) + "_lau_000" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID2) + "_gui_000" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID2) + "_bri_000" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID2) + "_bri_001" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID2) + "_bri_002" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID2) + "_bri_003" + logFileSuffix, size: 50},
|
||||
}
|
||||
|
||||
session3Files := []fileInfo{
|
||||
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID3) + "_lau_001" + logFileSuffix, size: 50},
|
||||
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID3) + "_bri_001" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID3) + "_bri_002" + logFileSuffix, size: 100},
|
||||
{filename: string(sessionID3) + "_bri_003" + logFileSuffix, size: 50},
|
||||
}
|
||||
|
||||
allSessions := session1Files
|
||||
allSessions = append(allSessions, append(session2Files, session3Files...)...)
|
||||
checkFolderContent(t, dir, allSessions...)
|
||||
|
||||
failureCount, err := pruneLogs(dir, sessionID3, 2000) // nothing to prune
|
||||
require.Equal(t, failureCount, 0)
|
||||
require.NoError(t, err)
|
||||
checkFolderContent(t, dir, allSessions...)
|
||||
|
||||
failureCount, err = pruneLogs(dir, sessionID3, 1200) // session 1 is pruned
|
||||
require.Equal(t, failureCount, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkFolderContent(t, dir, append(session2Files, session3Files...)...)
|
||||
failureCount, err = pruneLogs(dir, sessionID3, 1000) // session 2 is pruned
|
||||
require.Equal(t, failureCount, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkFolderContent(t, dir, session3Files...)
|
||||
}
|
||||
|
||||
func TestLogging_PruningBigCurrentSession(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
const maxLogFileSize = 1000
|
||||
sessionID1 := createDummySession(t, dir, maxLogFileSize, 500, 500, 2500)
|
||||
sessionID2 := createDummySession(t, dir, maxLogFileSize, 1000, 1000, 3500)
|
||||
sessionID3 := createDummySession(t, dir, maxLogFileSize, 500, 500, 10500)
|
||||
|
||||
// Expected files per session
|
||||
session1Files := []fileInfo{
|
||||
{filename: string(sessionID1) + "_lau_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID1) + "_gui_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID1) + "_bri_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID1) + "_bri_001" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID1) + "_bri_002" + logFileSuffix, size: 500},
|
||||
}
|
||||
|
||||
session2Files := []fileInfo{
|
||||
{filename: string(sessionID2) + "_lau_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID2) + "_gui_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID2) + "_bri_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID2) + "_bri_001" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID2) + "_bri_002" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID2) + "_bri_003" + logFileSuffix, size: 500},
|
||||
}
|
||||
|
||||
session3Files := []fileInfo{
|
||||
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_001" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_002" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_003" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_004" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_005" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_006" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_007" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_008" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_009" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
|
||||
}
|
||||
|
||||
allSessions := session1Files
|
||||
allSessions = append(allSessions, append(session2Files, session3Files...)...)
|
||||
checkFolderContent(t, dir, allSessions...)
|
||||
|
||||
// current session is bigger than maxFileSize. We keep launcher and gui logs, the first and last bridge log
|
||||
// and only the last bridge log that keep the total file size under the limit.
|
||||
failureCount, err := pruneLogs(dir, sessionID3, 8000)
|
||||
require.Equal(t, failureCount, 0)
|
||||
require.NoError(t, err)
|
||||
checkFolderContent(t, dir, []fileInfo{
|
||||
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_005" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_006" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_007" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_008" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_009" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
|
||||
}...)
|
||||
|
||||
failureCount, err = pruneLogs(dir, sessionID3, 5000)
|
||||
require.Equal(t, failureCount, 0)
|
||||
require.NoError(t, err)
|
||||
checkFolderContent(t, dir, []fileInfo{
|
||||
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_008" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_009" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
|
||||
}...)
|
||||
|
||||
// whatever maxFileSize is, we will always keep the following files
|
||||
minimalFiles := []fileInfo{
|
||||
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
|
||||
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
|
||||
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
|
||||
}
|
||||
failureCount, err = pruneLogs(dir, sessionID3, 2000)
|
||||
require.Equal(t, failureCount, 0)
|
||||
require.NoError(t, err)
|
||||
checkFolderContent(t, dir, minimalFiles...)
|
||||
|
||||
failureCount, err = pruneLogs(dir, sessionID3, 0)
|
||||
require.Equal(t, failureCount, 0)
|
||||
require.NoError(t, err)
|
||||
checkFolderContent(t, dir, minimalFiles...)
|
||||
}
|
||||
|
||||
func createDummySession(t *testing.T, dir string, maxLogFileSize int64, launcherLogSize, guiLogSize, bridgeLogSize int64) SessionID {
|
||||
time.Sleep(2 * time.Millisecond) // ensure our sessionID is unused.
|
||||
sessionID := NewSessionID()
|
||||
if launcherLogSize > 0 {
|
||||
createDummyRotatedLogFile(t, dir, sessionID, LauncherShortAppName, launcherLogSize, maxLogFileSize)
|
||||
}
|
||||
|
||||
if guiLogSize > 0 {
|
||||
createDummyRotatedLogFile(t, dir, sessionID, GUIShortAppName, guiLogSize, maxLogFileSize)
|
||||
}
|
||||
|
||||
if bridgeLogSize > 0 {
|
||||
createDummyRotatedLogFile(t, dir, sessionID, BridgeShortAppName, bridgeLogSize, maxLogFileSize)
|
||||
}
|
||||
|
||||
return sessionID
|
||||
}
|
||||
|
||||
func createDummyRotatedLogFile(t *testing.T, dir string, sessionID SessionID, appName AppName, totalSize, maxLogFileSize int64) {
|
||||
rotator, err := NewDefaultRotator(dir, sessionID, appName, maxLogFileSize, NoPruning)
|
||||
require.NoError(t, err)
|
||||
for i := int64(0); i < totalSize/maxLogFileSize; i++ {
|
||||
count, err := rotator.Write(make([]byte, maxLogFileSize))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(count), maxLogFileSize)
|
||||
}
|
||||
|
||||
remainder := totalSize % maxLogFileSize
|
||||
if remainder > 0 {
|
||||
count, err := rotator.Write(make([]byte, remainder))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(count), remainder)
|
||||
}
|
||||
|
||||
require.NoError(t, rotator.wc.Close())
|
||||
}
|
||||
|
||||
func checkFolderContent(t *testing.T, dir string, fileInfos ...fileInfo) {
|
||||
for _, fi := range fileInfos {
|
||||
checkFileExistsWithSize(t, dir, fi)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(fileInfos), len(entries))
|
||||
}
|
||||
|
||||
func checkFileExistsWithSize(t *testing.T, dir string, info fileInfo) {
|
||||
stat, err := os.Stat(filepath.Join(dir, info.filename))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, stat.Size(), info.size)
|
||||
}
|
||||
@ -17,21 +17,39 @@
|
||||
|
||||
package logging
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
)
|
||||
|
||||
type Rotator struct {
|
||||
getFile FileProvider
|
||||
wc io.WriteCloser
|
||||
size int
|
||||
maxSize int
|
||||
getFile FileProvider
|
||||
prune Pruner
|
||||
wc io.WriteCloser
|
||||
size int64
|
||||
maxFileSize int64
|
||||
nextIndex int
|
||||
}
|
||||
|
||||
type FileProvider func() (io.WriteCloser, error)
|
||||
type FileProvider func(index int) (io.WriteCloser, error)
|
||||
|
||||
func NewRotator(maxSize int, getFile FileProvider) (*Rotator, error) {
|
||||
func defaultFileProvider(logsPath string, sessionID SessionID, appName AppName) FileProvider {
|
||||
return func(index int) (io.WriteCloser, error) {
|
||||
return os.Create(filepath.Join(logsPath, //nolint:gosec // G304
|
||||
fmt.Sprintf("%v_%v_%03d_v%v_%v.log", sessionID, appName, index, constants.Version, constants.Tag),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func NewRotator(maxFileSize int64, getFile FileProvider, prune Pruner) (*Rotator, error) {
|
||||
r := &Rotator{
|
||||
getFile: getFile,
|
||||
maxSize: maxSize,
|
||||
getFile: getFile,
|
||||
prune: prune,
|
||||
maxFileSize: maxFileSize,
|
||||
}
|
||||
|
||||
if err := r.rotate(); err != nil {
|
||||
@ -41,8 +59,19 @@ func NewRotator(maxSize int, getFile FileProvider) (*Rotator, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func NewDefaultRotator(logsPath string, sessionID SessionID, appName AppName, maxLogFileSize, pruningSize int64) (*Rotator, error) {
|
||||
var pruner Pruner
|
||||
if pruningSize < 0 {
|
||||
pruner = nullPruner
|
||||
} else {
|
||||
pruner = defaultPruner(logsPath, sessionID, pruningSize)
|
||||
}
|
||||
|
||||
return NewRotator(maxLogFileSize, defaultFileProvider(logsPath, sessionID, appName), pruner)
|
||||
}
|
||||
|
||||
func (r *Rotator) Write(p []byte) (int, error) {
|
||||
if r.size+len(p) > r.maxSize {
|
||||
if r.size+int64(len(p)) > r.maxFileSize {
|
||||
if err := r.rotate(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -53,21 +82,33 @@ func (r *Rotator) Write(p []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
r.size += n
|
||||
|
||||
r.size += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *Rotator) Close() error {
|
||||
if r.wc != nil {
|
||||
return r.wc.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Rotator) rotate() error {
|
||||
if r.wc != nil {
|
||||
_ = r.wc.Close()
|
||||
}
|
||||
|
||||
wc, err := r.getFile()
|
||||
if _, err := r.prune(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wc, err := r.getFile(r.nextIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.nextIndex++
|
||||
r.wc = wc
|
||||
r.size = 0
|
||||
|
||||
|
||||
@ -19,8 +19,10 @@ package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -35,15 +37,15 @@ func (c *WriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRotator(t *testing.T) {
|
||||
func TestLogging_Rotator(t *testing.T) {
|
||||
n := 0
|
||||
|
||||
getFile := func() (io.WriteCloser, error) {
|
||||
getFile := func(_ int) (io.WriteCloser, error) {
|
||||
n++
|
||||
return &WriteCloser{}, nil
|
||||
}
|
||||
|
||||
r, err := NewRotator(10, getFile)
|
||||
r, err := NewRotator(10, getFile, nullPruner)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = r.Write([]byte("12345"))
|
||||
@ -75,12 +77,89 @@ func TestRotator(t *testing.T) {
|
||||
assert.Equal(t, 4, n)
|
||||
}
|
||||
|
||||
func BenchmarkRotate(b *testing.B) {
|
||||
benchRotate(b, MaxLogSize, getTestFile(b, b.TempDir(), MaxLogSize-1))
|
||||
func TestLogging_DefaultRotator(t *testing.T) {
|
||||
fiveBytes := []byte("00000")
|
||||
tmpDir := os.TempDir()
|
||||
|
||||
sessionID := NewSessionID()
|
||||
basePath := filepath.Join(tmpDir, string(sessionID))
|
||||
|
||||
r, err := NewDefaultRotator(tmpDir, sessionID, "bri", 10, NoPruning)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"_bri_000_*.log"))
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
|
||||
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
|
||||
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
|
||||
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, countFilesMatching(basePath+"*.log"))
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"_bri_001_*.log"))
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.NoError(t, r.wc.Close())
|
||||
|
||||
// total written: 35 bytes, i.e. 4 log files
|
||||
logFileCount := countFilesMatching(basePath + "*.log")
|
||||
require.Equal(t, 4, logFileCount)
|
||||
for i := 0; i < logFileCount; i++ {
|
||||
require.Equal(t, 1, countFilesMatching(basePath+fmt.Sprintf("_bri_%03d_*.log", i)))
|
||||
}
|
||||
|
||||
cleanupLogs(t, sessionID)
|
||||
}
|
||||
|
||||
func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, error)) {
|
||||
r, err := NewRotator(logSize, getFile)
|
||||
func TestLogging_DefaultRotatorWithPruning(t *testing.T) {
|
||||
tenBytes := []byte("0000000000")
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sessionID := NewSessionID()
|
||||
basePath := filepath.Join(tmpDir, string(sessionID))
|
||||
|
||||
// fill the log dir while below the pruning quota
|
||||
r, err := NewDefaultRotator(tmpDir, sessionID, "bri", 10, 40)
|
||||
require.NoError(t, err)
|
||||
for i := 0; i < 4; i++ {
|
||||
_, err = r.Write(tenBytes)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// from now on at every rotation, (i.e. every write in this case), we will prune, then create a new file.
|
||||
// we should always have 4 files, remaining after prune, plus the newly rotated file with the last written bytes.
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := r.Write(tenBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 5, countFilesMatching(basePath+"_bri_*.log"))
|
||||
}
|
||||
|
||||
require.NoError(t, r.wc.Close())
|
||||
|
||||
// Final check. 000, 010, 011, 012 are what's left after the last pruning, 013 never got to pass through pruning.
|
||||
checkFolderContent(t, tmpDir, []fileInfo{
|
||||
{filename: string(sessionID) + "_bri_000" + logFileSuffix, size: 10},
|
||||
{filename: string(sessionID) + "_bri_010" + logFileSuffix, size: 10},
|
||||
{filename: string(sessionID) + "_bri_011" + logFileSuffix, size: 10},
|
||||
{filename: string(sessionID) + "_bri_012" + logFileSuffix, size: 10},
|
||||
{filename: string(sessionID) + "_bri_013" + logFileSuffix, size: 10},
|
||||
}...)
|
||||
}
|
||||
|
||||
func BenchmarkRotate(b *testing.B) {
|
||||
benchRotate(b, DefaultMaxLogFileSize, getTestFile(b, b.TempDir(), DefaultMaxLogFileSize-1))
|
||||
}
|
||||
|
||||
func benchRotate(b *testing.B, logSize int64, getFile func(index int) (io.WriteCloser, error)) {
|
||||
r, err := NewRotator(logSize, getFile, nullPruner)
|
||||
require.NoError(b, err)
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
@ -92,8 +171,8 @@ func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, erro
|
||||
}
|
||||
}
|
||||
|
||||
func getTestFile(b *testing.B, dir string, length int) func() (io.WriteCloser, error) {
|
||||
return func() (io.WriteCloser, error) {
|
||||
func getTestFile(b *testing.B, dir string, length int) func(int) (io.WriteCloser, error) {
|
||||
return func(index int) (io.WriteCloser, error) {
|
||||
b.StopTimer()
|
||||
defer b.StartTimer()
|
||||
|
||||
@ -113,3 +192,20 @@ func getTestFile(b *testing.B, dir string, length int) func() (io.WriteCloser, e
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
func countFilesMatching(pattern string) int {
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return len(files)
|
||||
}
|
||||
|
||||
func cleanupLogs(t *testing.T, sessionID SessionID) {
|
||||
paths, err := filepath.Glob(filepath.Join(os.TempDir(), string(sessionID)+"*.log"))
|
||||
require.NoError(t, err)
|
||||
for _, path := range paths {
|
||||
require.NoError(t, os.Remove(path))
|
||||
}
|
||||
}
|
||||
|
||||
64
internal/logging/session_id.go
Normal file
64
internal/logging/session_id.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionID string
|
||||
|
||||
const (
|
||||
timeFormat = "20060102_150405" // time format in Go does not support milliseconds without dot, so we'll process it manually.
|
||||
)
|
||||
|
||||
// NewSessionID creates a sessionID based on the current time.
|
||||
func NewSessionID() SessionID {
|
||||
now := time.Now()
|
||||
return SessionID(now.Format(timeFormat) + fmt.Sprintf("%03d", now.Nanosecond()/1000000))
|
||||
}
|
||||
|
||||
// NewSessionIDFromString Return a new sessionID from string. If the str is empty a new time based sessionID is returned, otherwise the string
|
||||
// is used as the sessionID.
|
||||
func NewSessionIDFromString(str string) SessionID {
|
||||
if (len(str)) > 0 {
|
||||
return SessionID(str)
|
||||
}
|
||||
|
||||
return NewSessionID()
|
||||
}
|
||||
|
||||
// toTime converts a sessionID to a date/Time, considering the time zone is local.
|
||||
func (s SessionID) toTime() time.Time {
|
||||
if len(s) < 3 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
t, err := time.ParseInLocation(timeFormat, string(s)[:len(s)-3], time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
var ms int
|
||||
if ms, err = strconv.Atoi(string(s)[len(s)-3:]); err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
return t.Add(time.Duration(ms) * time.Millisecond)
|
||||
}
|
||||
38
internal/logging/session_id_test.go
Normal file
38
internal/logging/session_id_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogging_SessionID(t *testing.T) {
|
||||
now := time.Now()
|
||||
sessionID := NewSessionID()
|
||||
sessionTime := sessionID.toTime()
|
||||
require.False(t, sessionTime.IsZero())
|
||||
require.WithinRange(t, sessionTime, now.Add(-1*time.Millisecond), now.Add(1*time.Millisecond))
|
||||
|
||||
fromString := NewSessionIDFromString("")
|
||||
require.True(t, len(fromString) > 0)
|
||||
fromString = NewSessionIDFromString(string(sessionID))
|
||||
require.True(t, len(fromString) > 0)
|
||||
require.Equal(t, sessionID, fromString)
|
||||
}
|
||||
@ -18,6 +18,7 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@ -149,12 +150,12 @@ func (heartbeat *Heartbeat) SetPrevVersion(val string) {
|
||||
heartbeat.metrics.Dimensions.PrevVersion = val
|
||||
}
|
||||
|
||||
func (heartbeat *Heartbeat) TrySending() {
|
||||
if heartbeat.manager.IsTelemetryAvailable() {
|
||||
func (heartbeat *Heartbeat) TrySending(ctx context.Context) {
|
||||
if heartbeat.manager.IsTelemetryAvailable(ctx) {
|
||||
lastSent := heartbeat.manager.GetLastHeartbeatSent()
|
||||
now := time.Now()
|
||||
if now.Year() > lastSent.Year() || (now.Year() == lastSent.Year() && now.YearDay() > lastSent.YearDay()) {
|
||||
if !heartbeat.manager.SendHeartbeat(&heartbeat.metrics) {
|
||||
if !heartbeat.manager.SendHeartbeat(ctx, &heartbeat.metrics) {
|
||||
heartbeat.log.WithFields(logrus.Fields{
|
||||
"metrics": heartbeat.metrics,
|
||||
}).Error("Failed to send heartbeat")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user