Compare commits
6 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
511504c2eb | |
|
|
721b111c0c | |
|
|
d4ebcbde41 | |
|
|
df114a194a | |
|
|
4cb69eb583 | |
|
|
87b0e2d9c8 |
91
.cirrus.yml
91
.cirrus.yml
|
|
@ -1,91 +0,0 @@
|
|||
task:
|
||||
name: Lint
|
||||
container:
|
||||
image: golangci/golangci-lint:latest
|
||||
cpu: 2
|
||||
memory: 4096
|
||||
prepare_script:
|
||||
- apt-get update
|
||||
- apt-get install -y libx11-dev
|
||||
lint_script:
|
||||
- golangci-lint run -v --output.json.path golangci.json
|
||||
always:
|
||||
report_artifacts:
|
||||
path: golangci.json
|
||||
type: text/json
|
||||
format: golangci
|
||||
|
||||
task:
|
||||
name: Test (Linux)
|
||||
alias: Tests
|
||||
container:
|
||||
image: golang:latest
|
||||
|
||||
prepare_script:
|
||||
- apt-get update
|
||||
- apt-get install -y libx11-dev
|
||||
test_script: go test -v ./...
|
||||
|
||||
task:
|
||||
name: Test (macOS)
|
||||
alias: Tests
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
|
||||
prepare_script: brew install go
|
||||
test_script: go test -v ./...
|
||||
|
||||
task:
|
||||
name: Check for lacking "buf generate" invocation
|
||||
|
||||
container:
|
||||
image: golang:latest
|
||||
|
||||
install_buf_script: go install github.com/bufbuild/buf/cmd/buf@v1.50.0
|
||||
generate_script: buf generate
|
||||
check_script: git diff --exit-code
|
||||
|
||||
task:
|
||||
only_if: $CIRRUS_BRANCH != '' && $CIRRUS_PR == '' && $CIRRUS_REPO_OWNER == 'cirruslabs'
|
||||
name: buf push
|
||||
|
||||
container:
|
||||
image: bufbuild/buf
|
||||
|
||||
login_script: echo "$BUF_TOKEN" | buf registry login --token-stdin
|
||||
push_script: buf push --git-metadata
|
||||
|
||||
env:
|
||||
BUF_TOKEN: ENCRYPTED[!8ee7eb2504cc84b08d4a7c0dacbe103640b1feaa26d06f0df010784e872d39e65a0cdea3fc7c09b065a917a77113b96b!]
|
||||
|
||||
task:
|
||||
name: Release (Dry Run)
|
||||
only_if: $CIRRUS_TAG == ''
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
depends_on:
|
||||
- Lint
|
||||
- Tests
|
||||
install_script:
|
||||
- brew install go
|
||||
- brew install --cask goreleaser/tap/goreleaser-pro
|
||||
release_script: goreleaser release --clean --snapshot
|
||||
goreleaser_artifacts:
|
||||
path: "dist/**"
|
||||
|
||||
task:
|
||||
name: Release
|
||||
only_if: $CIRRUS_TAG != ''
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
depends_on:
|
||||
- Lint
|
||||
- Tests
|
||||
env:
|
||||
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
||||
FURY_TOKEN: ENCRYPTED[!97fe4497d9aca60a3d64904883b81e21f19706c6aedda625c97f62f67ec46b8efa74c55699956158bbf0a23726e7d9f6!]
|
||||
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
||||
install_script:
|
||||
- brew install go
|
||||
- brew install --cask goreleaser/tap/goreleaser-pro
|
||||
release_script: goreleaser
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install --yes libx11-dev
|
||||
- uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.12.0
|
||||
only-new-issues: true
|
||||
|
||||
test-linux:
|
||||
name: Test (Linux)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install --yes libx11-dev
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
||||
test-macos:
|
||||
name: Test (macOS)
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Run tests
|
||||
run: go test -v ./...
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
if: github.ref_type == 'tag'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
environment: publish
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Create release app token for this repository
|
||||
id: release-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
- name: Create release app token for homebrew-tools
|
||||
id: tap-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
owner: openai
|
||||
repositories: homebrew-tools
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
- name: Release
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.release-token.outputs.token }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.tap-token.outputs.token }}
|
||||
|
||||
dry-run:
|
||||
name: Release (Dry Run)
|
||||
if: github.ref_type != 'tag'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Release dry run
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: "~> v2"
|
||||
args: release --skip=publish --snapshot --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
- name: Upload dry-run artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: tart-guest-agent-snapshot
|
||||
path: dist/**
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
version: 2
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters-settings:
|
||||
# Even in Rust you can get away with partial matching,
|
||||
# so make sure that the linter respects the programmer's
|
||||
# intent expressed in the form of "default" case.
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
default: all
|
||||
|
||||
settings:
|
||||
# Even in Rust you can get away with partial matching,
|
||||
# so make sure that the linter respects the programmer's
|
||||
# intent expressed in the form of "default" case.
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
disable:
|
||||
# We don't have high-performance requirements at this moment, so sacrificing
|
||||
|
|
@ -32,6 +32,7 @@ linters:
|
|||
|
||||
# Style linters that are total nuts.
|
||||
- wsl
|
||||
- wsl_v5
|
||||
- funlen
|
||||
|
||||
# Enough parallelism for now.
|
||||
|
|
@ -61,6 +62,15 @@ linters:
|
|||
# Not all errors need to be checked
|
||||
- errcheck
|
||||
|
||||
# It's OK to not initialize some struct fields
|
||||
- exhaustruct
|
||||
|
||||
# This is not a library, so it's OK to use dynamic errors
|
||||
- err113
|
||||
|
||||
# Inline error handling keeps assignment and checking together
|
||||
- noinlineerr
|
||||
|
||||
issues:
|
||||
# Don't hide multiple issues that belong to one class since GitHub annotations can handle them all nicely.
|
||||
max-issues-per-linter: 0
|
||||
|
|
|
|||
|
|
@ -50,10 +50,15 @@ release:
|
|||
|
||||
brews:
|
||||
- name: "{{ .ProjectName }}"
|
||||
directory: Formula
|
||||
repository:
|
||||
owner: cirruslabs
|
||||
name: homebrew-cli
|
||||
homepage: https://github.com/cirruslabs/tart-guest-agent
|
||||
owner: openai
|
||||
name: homebrew-tools
|
||||
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
|
||||
branch: "{{ .ProjectName }}-{{ .Version }}"
|
||||
pull_request:
|
||||
enabled: true
|
||||
homepage: https://github.com/openai/tart-guest-agent
|
||||
license: FSL-1.1-Apache-2.0
|
||||
description: Guest agent for Tart VMs
|
||||
skip_upload: auto
|
||||
|
|
@ -79,6 +84,3 @@ nfpms:
|
|||
postinstall: packaging/postinstall.sh
|
||||
preremove: packaging/preremove.sh
|
||||
postremove: packaging/postremove.sh
|
||||
|
||||
furies:
|
||||
- account: cirruslabs
|
||||
|
|
|
|||
6
LICENSE
6
LICENSE
|
|
@ -1,12 +1,12 @@
|
|||
# Functional Source License, Version 1.1, Apache 2.0 Future License
|
||||
# Functional Source License, Version 1.1, ALv2 Future License
|
||||
|
||||
## Abbreviation
|
||||
|
||||
FSL-1.1-Apache-2.0
|
||||
FSL-1.1-ALv2
|
||||
|
||||
## Notice
|
||||
|
||||
Copyright 2025 Cirrus Labs, Inc.
|
||||
Copyright 2025-2026 OpenAI
|
||||
|
||||
## Terms and Conditions
|
||||
|
||||
|
|
|
|||
24
go.mod
24
go.mod
|
|
@ -3,33 +3,37 @@ module github.com/cirruslabs/tart-guest-agent
|
|||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/Masterminds/semver/v3 v3.5.0
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
github.com/puzpuzpuz/xsync/v4 v4.5.0
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.design/x/clipboard v0.7.1
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.42.0
|
||||
google.golang.org/grpc v1.79.3
|
||||
go.uber.org/zap v1.28.0
|
||||
golang.design/x/clipboard v0.8.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
google.golang.org/grpc v1.83.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.design/x/x11 v0.2.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
||||
golang.org/x/image v0.28.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
71
go.sum
71
go.sum
|
|
@ -1,6 +1,6 @@
|
|||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
|
|
@ -10,6 +10,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
|
||||
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
|
|
@ -20,13 +22,15 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
|
|
@ -38,25 +42,28 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
|
||||
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
|
||||
golang.design/x/clipboard v0.8.0 h1:6VEcH28wwcSgKc+vnxHHDWiRjrakTQIAJnPUrt3aOgg=
|
||||
golang.design/x/clipboard v0.8.0/go.mod h1:s0pwrtA3Q9fgnVtGDmP5ZK/pp55cQKB23esKsjwWhWM=
|
||||
golang.design/x/x11 v0.2.0 h1:Uiwu2guGihsJX/ZCzpoDPFz5gR/Qntm08mvoBCmRydo=
|
||||
golang.design/x/x11 v0.2.0/go.mod h1:/5q1mFkdc1rL8mvB7DsQFi6as4tIkBv4FXjcP07mrkE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||
|
|
@ -70,24 +77,24 @@ golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mT
|
|||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
|
||||
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc (unknown)
|
||||
// protoc v7.35.1
|
||||
// source: rpc/agent.proto
|
||||
|
||||
package rpc
|
||||
|
|
@ -9,7 +9,7 @@ package rpc
|
|||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
_ "google.golang.org/protobuf/types/known/emptypb"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
|
@ -22,6 +22,55 @@ const (
|
|||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type SignalRequest_Signal int32
|
||||
|
||||
const (
|
||||
SignalRequest_SIGNAL_UNSPECIFIED SignalRequest_Signal = 0
|
||||
SignalRequest_SIGNAL_SIGTERM SignalRequest_Signal = 1
|
||||
SignalRequest_SIGNAL_SIGKILL SignalRequest_Signal = 2
|
||||
)
|
||||
|
||||
// Enum value maps for SignalRequest_Signal.
|
||||
var (
|
||||
SignalRequest_Signal_name = map[int32]string{
|
||||
0: "SIGNAL_UNSPECIFIED",
|
||||
1: "SIGNAL_SIGTERM",
|
||||
2: "SIGNAL_SIGKILL",
|
||||
}
|
||||
SignalRequest_Signal_value = map[string]int32{
|
||||
"SIGNAL_UNSPECIFIED": 0,
|
||||
"SIGNAL_SIGTERM": 1,
|
||||
"SIGNAL_SIGKILL": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x SignalRequest_Signal) Enum() *SignalRequest_Signal {
|
||||
p := new(SignalRequest_Signal)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x SignalRequest_Signal) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (SignalRequest_Signal) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_rpc_agent_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (SignalRequest_Signal) Type() protoreflect.EnumType {
|
||||
return &file_rpc_agent_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x SignalRequest_Signal) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SignalRequest_Signal.Descriptor instead.
|
||||
func (SignalRequest_Signal) EnumDescriptor() ([]byte, []int) {
|
||||
return file_rpc_agent_proto_rawDescGZIP(), []int{6, 0}
|
||||
}
|
||||
|
||||
type ExecRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Types that are valid to be assigned to Type:
|
||||
|
|
@ -127,6 +176,7 @@ type ExecResponse struct {
|
|||
// *ExecResponse_Exit_
|
||||
// *ExecResponse_StandardOutput
|
||||
// *ExecResponse_StandardError
|
||||
// *ExecResponse_Started_
|
||||
Type isExecResponse_Type `protobuf_oneof:"type"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
@ -196,6 +246,15 @@ func (x *ExecResponse) GetStandardError() *IOChunk {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecResponse) GetStarted() *ExecResponse_Started {
|
||||
if x != nil {
|
||||
if x, ok := x.Type.(*ExecResponse_Started_); ok {
|
||||
return x.Started
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isExecResponse_Type interface {
|
||||
isExecResponse_Type()
|
||||
}
|
||||
|
|
@ -212,12 +271,18 @@ type ExecResponse_StandardError struct {
|
|||
StandardError *IOChunk `protobuf:"bytes,3,opt,name=standard_error,json=standardError,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ExecResponse_Started_ struct {
|
||||
Started *ExecResponse_Started `protobuf:"bytes,4,opt,name=started,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ExecResponse_Exit_) isExecResponse_Type() {}
|
||||
|
||||
func (*ExecResponse_StandardOutput) isExecResponse_Type() {}
|
||||
|
||||
func (*ExecResponse_StandardError) isExecResponse_Type() {}
|
||||
|
||||
func (*ExecResponse_Started_) isExecResponse_Type() {}
|
||||
|
||||
type TerminalSize struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Rows uint32 `protobuf:"varint,1,opt,name=rows,proto3" json:"rows,omitempty"`
|
||||
|
|
@ -394,6 +459,58 @@ func (x *ResolveIPResponse) GetIp() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
type SignalRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ExecId string `protobuf:"bytes,1,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"`
|
||||
Signal SignalRequest_Signal `protobuf:"varint,2,opt,name=signal,proto3,enum=SignalRequest_Signal" json:"signal,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SignalRequest) Reset() {
|
||||
*x = SignalRequest{}
|
||||
mi := &file_rpc_agent_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SignalRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SignalRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SignalRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_agent_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SignalRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SignalRequest) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_agent_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *SignalRequest) GetExecId() string {
|
||||
if x != nil {
|
||||
return x.ExecId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SignalRequest) GetSignal() SignalRequest_Signal {
|
||||
if x != nil {
|
||||
return x.Signal
|
||||
}
|
||||
return SignalRequest_SIGNAL_UNSPECIFIED
|
||||
}
|
||||
|
||||
type ExecRequest_Command struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
|
|
@ -404,13 +521,14 @@ type ExecRequest_Command struct {
|
|||
Detach bool `protobuf:"varint,6,opt,name=detach,proto3" json:"detach,omitempty"`
|
||||
Env map[string]string `protobuf:"bytes,7,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
Workdir string `protobuf:"bytes,8,opt,name=workdir,proto3" json:"workdir,omitempty"`
|
||||
User string `protobuf:"bytes,9,opt,name=user,proto3" json:"user,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExecRequest_Command) Reset() {
|
||||
*x = ExecRequest_Command{}
|
||||
mi := &file_rpc_agent_proto_msgTypes[6]
|
||||
mi := &file_rpc_agent_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -422,7 +540,7 @@ func (x *ExecRequest_Command) String() string {
|
|||
func (*ExecRequest_Command) ProtoMessage() {}
|
||||
|
||||
func (x *ExecRequest_Command) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_agent_proto_msgTypes[6]
|
||||
mi := &file_rpc_agent_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -494,6 +612,13 @@ func (x *ExecRequest_Command) GetWorkdir() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (x *ExecRequest_Command) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ExecResponse_Exit struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
|
|
@ -503,7 +628,7 @@ type ExecResponse_Exit struct {
|
|||
|
||||
func (x *ExecResponse_Exit) Reset() {
|
||||
*x = ExecResponse_Exit{}
|
||||
mi := &file_rpc_agent_proto_msgTypes[8]
|
||||
mi := &file_rpc_agent_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -515,7 +640,7 @@ func (x *ExecResponse_Exit) String() string {
|
|||
func (*ExecResponse_Exit) ProtoMessage() {}
|
||||
|
||||
func (x *ExecResponse_Exit) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_agent_proto_msgTypes[8]
|
||||
mi := &file_rpc_agent_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -538,15 +663,59 @@ func (x *ExecResponse_Exit) GetCode() int32 {
|
|||
return 0
|
||||
}
|
||||
|
||||
type ExecResponse_Started struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ExecId string `protobuf:"bytes,1,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExecResponse_Started) Reset() {
|
||||
*x = ExecResponse_Started{}
|
||||
mi := &file_rpc_agent_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExecResponse_Started) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExecResponse_Started) ProtoMessage() {}
|
||||
|
||||
func (x *ExecResponse_Started) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_rpc_agent_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExecResponse_Started.ProtoReflect.Descriptor instead.
|
||||
func (*ExecResponse_Started) Descriptor() ([]byte, []int) {
|
||||
return file_rpc_agent_proto_rawDescGZIP(), []int{1, 1}
|
||||
}
|
||||
|
||||
func (x *ExecResponse_Started) GetExecId() string {
|
||||
if x != nil {
|
||||
return x.ExecId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_rpc_agent_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_rpc_agent_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0frpc/agent.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xeb\x03\n" +
|
||||
"\x0frpc/agent.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xff\x03\n" +
|
||||
"\vExecRequest\x120\n" +
|
||||
"\acommand\x18\x01 \x01(\v2\x14.ExecRequest.CommandH\x00R\acommand\x121\n" +
|
||||
"\x0estandard_input\x18\x02 \x01(\v2\b.IOChunkH\x00R\rstandardInput\x128\n" +
|
||||
"\x0fterminal_resize\x18\x03 \x01(\v2\r.TerminalSizeH\x00R\x0eterminalResize\x1a\xb4\x02\n" +
|
||||
"\x0fterminal_resize\x18\x03 \x01(\v2\r.TerminalSizeH\x00R\x0eterminalResize\x1a\xc8\x02\n" +
|
||||
"\aCommand\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04args\x18\x02 \x03(\tR\x04args\x12 \n" +
|
||||
|
|
@ -555,17 +724,21 @@ const file_rpc_agent_proto_rawDesc = "" +
|
|||
"\rterminal_size\x18\x05 \x01(\v2\r.TerminalSizeR\fterminalSize\x12\x16\n" +
|
||||
"\x06detach\x18\x06 \x01(\bR\x06detach\x12/\n" +
|
||||
"\x03env\x18\a \x03(\v2\x1d.ExecRequest.Command.EnvEntryR\x03env\x12\x18\n" +
|
||||
"\aworkdir\x18\b \x01(\tR\aworkdir\x1a6\n" +
|
||||
"\aworkdir\x18\b \x01(\tR\aworkdir\x12\x12\n" +
|
||||
"\x04user\x18\t \x01(\tR\x04user\x1a6\n" +
|
||||
"\bEnvEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" +
|
||||
"\x04type\"\xc4\x01\n" +
|
||||
"\x04type\"\x9b\x02\n" +
|
||||
"\fExecResponse\x12(\n" +
|
||||
"\x04exit\x18\x01 \x01(\v2\x12.ExecResponse.ExitH\x00R\x04exit\x123\n" +
|
||||
"\x0fstandard_output\x18\x02 \x01(\v2\b.IOChunkH\x00R\x0estandardOutput\x121\n" +
|
||||
"\x0estandard_error\x18\x03 \x01(\v2\b.IOChunkH\x00R\rstandardError\x1a\x1a\n" +
|
||||
"\x0estandard_error\x18\x03 \x01(\v2\b.IOChunkH\x00R\rstandardError\x121\n" +
|
||||
"\astarted\x18\x04 \x01(\v2\x15.ExecResponse.StartedH\x00R\astarted\x1a\x1a\n" +
|
||||
"\x04Exit\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04codeB\x06\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x1a\"\n" +
|
||||
"\aStarted\x12\x17\n" +
|
||||
"\aexec_id\x18\x01 \x01(\tR\x06execIdB\x06\n" +
|
||||
"\x04type\"6\n" +
|
||||
"\fTerminalSize\x12\x12\n" +
|
||||
"\x04rows\x18\x01 \x01(\rR\x04rows\x12\x12\n" +
|
||||
|
|
@ -574,9 +747,17 @@ const file_rpc_agent_proto_rawDesc = "" +
|
|||
"\x04data\x18\x01 \x01(\fR\x04data\"\x12\n" +
|
||||
"\x10ResolveIPRequest\"#\n" +
|
||||
"\x11ResolveIPResponse\x12\x0e\n" +
|
||||
"\x02ip\x18\x01 \x01(\tR\x02ip2d\n" +
|
||||
"\x02ip\x18\x01 \x01(\tR\x02ip\"\xa1\x01\n" +
|
||||
"\rSignalRequest\x12\x17\n" +
|
||||
"\aexec_id\x18\x01 \x01(\tR\x06execId\x12-\n" +
|
||||
"\x06signal\x18\x02 \x01(\x0e2\x15.SignalRequest.SignalR\x06signal\"H\n" +
|
||||
"\x06Signal\x12\x16\n" +
|
||||
"\x12SIGNAL_UNSPECIFIED\x10\x00\x12\x12\n" +
|
||||
"\x0eSIGNAL_SIGTERM\x10\x01\x12\x12\n" +
|
||||
"\x0eSIGNAL_SIGKILL\x10\x022\x96\x01\n" +
|
||||
"\x05Agent\x12'\n" +
|
||||
"\x04Exec\x12\f.ExecRequest\x1a\r.ExecResponse(\x010\x01\x122\n" +
|
||||
"\x04Exec\x12\f.ExecRequest\x1a\r.ExecResponse(\x010\x01\x120\n" +
|
||||
"\x06Signal\x12\x0e.SignalRequest\x1a\x16.google.protobuf.Empty\x122\n" +
|
||||
"\tResolveIP\x12\x11.ResolveIPRequest\x1a\x12.ResolveIPResponseB5Z3github.com/cirruslabs/tart-guest-agent/internal/rpcb\x06proto3"
|
||||
|
||||
var (
|
||||
|
|
@ -591,36 +772,45 @@ func file_rpc_agent_proto_rawDescGZIP() []byte {
|
|||
return file_rpc_agent_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_rpc_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_rpc_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_rpc_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_rpc_agent_proto_goTypes = []any{
|
||||
(*ExecRequest)(nil), // 0: ExecRequest
|
||||
(*ExecResponse)(nil), // 1: ExecResponse
|
||||
(*TerminalSize)(nil), // 2: TerminalSize
|
||||
(*IOChunk)(nil), // 3: IOChunk
|
||||
(*ResolveIPRequest)(nil), // 4: ResolveIPRequest
|
||||
(*ResolveIPResponse)(nil), // 5: ResolveIPResponse
|
||||
(*ExecRequest_Command)(nil), // 6: ExecRequest.Command
|
||||
nil, // 7: ExecRequest.Command.EnvEntry
|
||||
(*ExecResponse_Exit)(nil), // 8: ExecResponse.Exit
|
||||
(SignalRequest_Signal)(0), // 0: SignalRequest.Signal
|
||||
(*ExecRequest)(nil), // 1: ExecRequest
|
||||
(*ExecResponse)(nil), // 2: ExecResponse
|
||||
(*TerminalSize)(nil), // 3: TerminalSize
|
||||
(*IOChunk)(nil), // 4: IOChunk
|
||||
(*ResolveIPRequest)(nil), // 5: ResolveIPRequest
|
||||
(*ResolveIPResponse)(nil), // 6: ResolveIPResponse
|
||||
(*SignalRequest)(nil), // 7: SignalRequest
|
||||
(*ExecRequest_Command)(nil), // 8: ExecRequest.Command
|
||||
nil, // 9: ExecRequest.Command.EnvEntry
|
||||
(*ExecResponse_Exit)(nil), // 10: ExecResponse.Exit
|
||||
(*ExecResponse_Started)(nil), // 11: ExecResponse.Started
|
||||
(*emptypb.Empty)(nil), // 12: google.protobuf.Empty
|
||||
}
|
||||
var file_rpc_agent_proto_depIdxs = []int32{
|
||||
6, // 0: ExecRequest.command:type_name -> ExecRequest.Command
|
||||
3, // 1: ExecRequest.standard_input:type_name -> IOChunk
|
||||
2, // 2: ExecRequest.terminal_resize:type_name -> TerminalSize
|
||||
8, // 3: ExecResponse.exit:type_name -> ExecResponse.Exit
|
||||
3, // 4: ExecResponse.standard_output:type_name -> IOChunk
|
||||
3, // 5: ExecResponse.standard_error:type_name -> IOChunk
|
||||
2, // 6: ExecRequest.Command.terminal_size:type_name -> TerminalSize
|
||||
7, // 7: ExecRequest.Command.env:type_name -> ExecRequest.Command.EnvEntry
|
||||
0, // 8: Agent.Exec:input_type -> ExecRequest
|
||||
4, // 9: Agent.ResolveIP:input_type -> ResolveIPRequest
|
||||
1, // 10: Agent.Exec:output_type -> ExecResponse
|
||||
5, // 11: Agent.ResolveIP:output_type -> ResolveIPResponse
|
||||
10, // [10:12] is the sub-list for method output_type
|
||||
8, // [8:10] is the sub-list for method input_type
|
||||
8, // [8:8] is the sub-list for extension type_name
|
||||
8, // [8:8] is the sub-list for extension extendee
|
||||
0, // [0:8] is the sub-list for field type_name
|
||||
8, // 0: ExecRequest.command:type_name -> ExecRequest.Command
|
||||
4, // 1: ExecRequest.standard_input:type_name -> IOChunk
|
||||
3, // 2: ExecRequest.terminal_resize:type_name -> TerminalSize
|
||||
10, // 3: ExecResponse.exit:type_name -> ExecResponse.Exit
|
||||
4, // 4: ExecResponse.standard_output:type_name -> IOChunk
|
||||
4, // 5: ExecResponse.standard_error:type_name -> IOChunk
|
||||
11, // 6: ExecResponse.started:type_name -> ExecResponse.Started
|
||||
0, // 7: SignalRequest.signal:type_name -> SignalRequest.Signal
|
||||
3, // 8: ExecRequest.Command.terminal_size:type_name -> TerminalSize
|
||||
9, // 9: ExecRequest.Command.env:type_name -> ExecRequest.Command.EnvEntry
|
||||
1, // 10: Agent.Exec:input_type -> ExecRequest
|
||||
7, // 11: Agent.Signal:input_type -> SignalRequest
|
||||
5, // 12: Agent.ResolveIP:input_type -> ResolveIPRequest
|
||||
2, // 13: Agent.Exec:output_type -> ExecResponse
|
||||
12, // 14: Agent.Signal:output_type -> google.protobuf.Empty
|
||||
6, // 15: Agent.ResolveIP:output_type -> ResolveIPResponse
|
||||
13, // [13:16] is the sub-list for method output_type
|
||||
10, // [10:13] is the sub-list for method input_type
|
||||
10, // [10:10] is the sub-list for extension type_name
|
||||
10, // [10:10] is the sub-list for extension extendee
|
||||
0, // [0:10] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_rpc_agent_proto_init() }
|
||||
|
|
@ -637,19 +827,21 @@ func file_rpc_agent_proto_init() {
|
|||
(*ExecResponse_Exit_)(nil),
|
||||
(*ExecResponse_StandardOutput)(nil),
|
||||
(*ExecResponse_StandardError)(nil),
|
||||
(*ExecResponse_Started_)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_agent_proto_rawDesc), len(file_rpc_agent_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumEnums: 1,
|
||||
NumMessages: 11,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_rpc_agent_proto_goTypes,
|
||||
DependencyIndexes: file_rpc_agent_proto_depIdxs,
|
||||
EnumInfos: file_rpc_agent_proto_enumTypes,
|
||||
MessageInfos: file_rpc_agent_proto_msgTypes,
|
||||
}.Build()
|
||||
File_rpc_agent_proto = out.File
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc (unknown)
|
||||
// - protoc v7.35.1
|
||||
// source: rpc/agent.proto
|
||||
|
||||
package rpc
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
|
|
@ -20,6 +21,7 @@ const _ = grpc.SupportPackageIsVersion9
|
|||
|
||||
const (
|
||||
Agent_Exec_FullMethodName = "/Agent/Exec"
|
||||
Agent_Signal_FullMethodName = "/Agent/Signal"
|
||||
Agent_ResolveIP_FullMethodName = "/Agent/ResolveIP"
|
||||
)
|
||||
|
||||
|
|
@ -28,6 +30,7 @@ const (
|
|||
// 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 AgentClient interface {
|
||||
Exec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecRequest, ExecResponse], error)
|
||||
Signal(ctx context.Context, in *SignalRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
ResolveIP(ctx context.Context, in *ResolveIPRequest, opts ...grpc.CallOption) (*ResolveIPResponse, error)
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +55,16 @@ func (c *agentClient) Exec(ctx context.Context, opts ...grpc.CallOption) (grpc.B
|
|||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Agent_ExecClient = grpc.BidiStreamingClient[ExecRequest, ExecResponse]
|
||||
|
||||
func (c *agentClient) Signal(ctx context.Context, in *SignalRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, Agent_Signal_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *agentClient) ResolveIP(ctx context.Context, in *ResolveIPRequest, opts ...grpc.CallOption) (*ResolveIPResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ResolveIPResponse)
|
||||
|
|
@ -67,6 +80,7 @@ func (c *agentClient) ResolveIP(ctx context.Context, in *ResolveIPRequest, opts
|
|||
// for forward compatibility.
|
||||
type AgentServer interface {
|
||||
Exec(grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error
|
||||
Signal(context.Context, *SignalRequest) (*emptypb.Empty, error)
|
||||
ResolveIP(context.Context, *ResolveIPRequest) (*ResolveIPResponse, error)
|
||||
mustEmbedUnimplementedAgentServer()
|
||||
}
|
||||
|
|
@ -81,6 +95,9 @@ type UnimplementedAgentServer struct{}
|
|||
func (UnimplementedAgentServer) Exec(grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method Exec not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServer) Signal(context.Context, *SignalRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Signal not implemented")
|
||||
}
|
||||
func (UnimplementedAgentServer) ResolveIP(context.Context, *ResolveIPRequest) (*ResolveIPResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResolveIP not implemented")
|
||||
}
|
||||
|
|
@ -112,6 +129,24 @@ func _Agent_Exec_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type Agent_ExecServer = grpc.BidiStreamingServer[ExecRequest, ExecResponse]
|
||||
|
||||
func _Agent_Signal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SignalRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AgentServer).Signal(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Agent_Signal_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AgentServer).Signal(ctx, req.(*SignalRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Agent_ResolveIP_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ResolveIPRequest)
|
||||
if err := dec(in); err != nil {
|
||||
|
|
@ -137,6 +172,10 @@ var Agent_ServiceDesc = grpc.ServiceDesc{
|
|||
ServiceName: "Agent",
|
||||
HandlerType: (*AgentServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Signal",
|
||||
Handler: _Agent_Signal_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ResolveIP",
|
||||
Handler: _Agent_ResolveIP_Handler,
|
||||
|
|
|
|||
|
|
@ -4,23 +4,34 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
userpkg "os/user"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/google/uuid"
|
||||
"github.com/samber/lo"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const (
|
||||
standardStreamsBufferSize = 4096
|
||||
|
||||
eofChar = 0x04
|
||||
|
||||
// execRuntimeFailureExitCode matches Docker's exit code for runtime failures before a process starts.
|
||||
execRuntimeFailureExitCode = 125
|
||||
// signalExitCodeOffset is the base for shell-style exit codes of processes terminated by signals.
|
||||
signalExitCodeOffset = 128
|
||||
)
|
||||
|
||||
func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error {
|
||||
|
|
@ -50,20 +61,38 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
|
||||
cmd := exec.CommandContext(execCtx, firstExecRequestCommand.Command.Name,
|
||||
firstExecRequestCommand.Command.Args...)
|
||||
applyExecOverrides(cmd, firstExecRequestCommand.Command)
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
|
||||
if err := applyExecOverrides(cmd, firstExecRequestCommand.Command); err != nil {
|
||||
zap.S().Warnf("failed to configure %s: %v", formatCommandAndArgs(firstExecRequestCommand.Command.GetName(),
|
||||
firstExecRequestCommand.Command.GetArgs()), err)
|
||||
|
||||
return sendStartFailure(stream)
|
||||
}
|
||||
|
||||
if firstExecRequestCommand.Command.Detach {
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
cmd.SysProcAttr.Setsid = true
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
zap.S().Warnf("failed to start %s: %v", formatCommandAndArgs(firstExecRequestCommand.Command.GetName(),
|
||||
firstExecRequestCommand.Command.GetArgs()), err)
|
||||
|
||||
return sendStartFailure(stream)
|
||||
}
|
||||
|
||||
// Release ownership before sending responses so failures do not leak the process handle
|
||||
if err := cmd.Process.Release(); err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.Process != nil {
|
||||
if err := cmd.Process.Release(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Explicitly notify the client that the process was started,
|
||||
// but don't provide an exec ID since it's a detached process
|
||||
err = sendStartSuccess(stream, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := stream.Send(&ExecResponse{
|
||||
|
|
@ -79,6 +108,11 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
return nil
|
||||
}
|
||||
|
||||
// Kill the whole process group when the exec stream is canceled
|
||||
cmd.Cancel = func() error {
|
||||
return signalProcessGroup(cmd.Process, syscall.SIGKILL)
|
||||
}
|
||||
|
||||
var stdin io.WriteCloser
|
||||
var stdout, stderr io.ReadCloser
|
||||
var ptmx *os.File
|
||||
|
|
@ -95,6 +129,9 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
stdout = ptmx
|
||||
stderr = ptmx
|
||||
} else {
|
||||
// Start the command in its own process group so signals reach all descendants
|
||||
cmd.SysProcAttr.Setpgid = true
|
||||
|
||||
if firstExecRequestCommand.Command.Interactive {
|
||||
stdin, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
|
|
@ -114,22 +151,57 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
|
||||
err = cmd.Start()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
zap.S().Warnf("failed to start %s: %v", formatCommandAndArgs(firstExecRequestCommand.Command.GetName(),
|
||||
firstExecRequestCommand.Command.GetArgs()), err)
|
||||
|
||||
return sendStartFailure(stream)
|
||||
}
|
||||
|
||||
// Ensure the PTY is closed if sending the Started response fails
|
||||
if ptmx != nil {
|
||||
defer ptmx.Close()
|
||||
}
|
||||
|
||||
execID := uuid.NewString()
|
||||
rpc.execs.Store(execID, cmd.Process)
|
||||
defer rpc.execs.Delete(execID)
|
||||
|
||||
// Explicitly notify the client that the process was started
|
||||
err = sendStartSuccess(stream, execID)
|
||||
if err != nil {
|
||||
// Output readers have not started yet, so cancel and reap directly
|
||||
_ = cmd.Cancel()
|
||||
_ = cmd.Wait()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle standard input and terminal resize events from the client
|
||||
fromClientErrCh := make(chan error, 1)
|
||||
reportClientError := func(err error) {
|
||||
fromClientErrCh <- err
|
||||
_ = cmd.Cancel()
|
||||
}
|
||||
|
||||
go func() {
|
||||
var stdinClosed bool
|
||||
|
||||
for {
|
||||
request, err := stream.Recv()
|
||||
if err != nil {
|
||||
// Allow the client to close its sending side while continuing to receive responses
|
||||
if errors.Is(err, io.EOF) {
|
||||
if err := closeStdin(stdin, firstExecRequestCommand.Command.GetTty(), &stdinClosed); err != nil {
|
||||
reportClientError(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
fromClientErrCh <- err
|
||||
reportClientError(err)
|
||||
}
|
||||
|
||||
return
|
||||
|
|
@ -143,30 +215,19 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
continue
|
||||
}
|
||||
|
||||
dataToWrite := typedAction.StandardInput.Data
|
||||
|
||||
// Check if the remote client has received EOF on their standard input
|
||||
if len(typedAction.StandardInput.Data) == 0 {
|
||||
if firstExecRequestCommand.Command.Tty {
|
||||
// When using pseudo-terminal, we can't simply close the
|
||||
// standard input, as the file descriptor is shared for
|
||||
// standard output and standard error too, so we send
|
||||
// an EOF character instead
|
||||
dataToWrite = []byte{eofChar}
|
||||
} else {
|
||||
// Close the standard input
|
||||
if err := stdin.Close(); err != nil {
|
||||
fromClientErrCh <- err
|
||||
if err := closeStdin(stdin, firstExecRequestCommand.Command.GetTty(), &stdinClosed); err != nil {
|
||||
reportClientError(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := stdin.Write(dataToWrite); err != nil {
|
||||
fromClientErrCh <- err
|
||||
if _, err := stdin.Write(typedAction.StandardInput.GetData()); err != nil {
|
||||
reportClientError(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -181,7 +242,7 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
Rows: uint16(typedAction.TerminalResize.GetRows()),
|
||||
Cols: uint16(typedAction.TerminalResize.GetCols()),
|
||||
}); err != nil {
|
||||
fromClientErrCh <- err
|
||||
reportClientError(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -189,6 +250,16 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
}
|
||||
}()
|
||||
|
||||
// Serialize responses from the stdout and stderr goroutines
|
||||
var sendMutex sync.Mutex
|
||||
|
||||
sendResponse := func(response *ExecResponse) error {
|
||||
sendMutex.Lock()
|
||||
defer sendMutex.Unlock()
|
||||
|
||||
return stream.Send(response)
|
||||
}
|
||||
|
||||
group, _ := errgroup.WithContext(stream.Context())
|
||||
|
||||
// Handle standard output from the command
|
||||
|
|
@ -210,7 +281,7 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
return err
|
||||
}
|
||||
|
||||
if err := stream.Send(&ExecResponse{
|
||||
if err := sendResponse(&ExecResponse{
|
||||
Type: &ExecResponse_StandardOutput{
|
||||
StandardOutput: &IOChunk{
|
||||
Data: slices.Clone(buf[:n]),
|
||||
|
|
@ -240,7 +311,7 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
return err
|
||||
}
|
||||
|
||||
if err := stream.Send(&ExecResponse{
|
||||
if err := sendResponse(&ExecResponse{
|
||||
Type: &ExecResponse_StandardError{
|
||||
StandardError: &IOChunk{
|
||||
Data: slices.Clone(buf[:n]),
|
||||
|
|
@ -258,15 +329,32 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
}
|
||||
|
||||
// Wait for the command to finish
|
||||
err = cmd.Wait()
|
||||
|
||||
// Minimize the window in which a finished exec can still be signaled
|
||||
rpc.execs.Delete(execID)
|
||||
|
||||
// Prefer a client error over the command exit result
|
||||
select {
|
||||
case err := <-fromClientErrCh:
|
||||
return err
|
||||
default:
|
||||
}
|
||||
|
||||
exitCode := 0
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
exitCode = exitError.ExitCode()
|
||||
} else {
|
||||
if !errors.As(err, &exitError) {
|
||||
return err
|
||||
}
|
||||
|
||||
// ExitCode returns -1 for signals; report the containerd-compatible 128 + signal instead
|
||||
exitCode = exitError.ExitCode()
|
||||
|
||||
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok && waitStatus.Signaled() {
|
||||
exitCode = signalExitCodeOffset + int(waitStatus.Signal())
|
||||
}
|
||||
}
|
||||
|
||||
return stream.Send(&ExecResponse{
|
||||
|
|
@ -278,7 +366,91 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
|||
})
|
||||
}
|
||||
|
||||
func applyExecOverrides(cmd *exec.Cmd, command *ExecRequest_Command) {
|
||||
func signalProcessGroup(process *os.Process, signal syscall.Signal) error {
|
||||
if err := syscall.Kill(-process.Pid, signal); err != nil {
|
||||
// Translate a missing process group into the process-finished error expected by os/exec
|
||||
if errors.Is(err, syscall.ESRCH) {
|
||||
return os.ErrProcessDone
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeStdin(stdin io.WriteCloser, tty bool, closed *bool) error {
|
||||
if stdin == nil || *closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tty {
|
||||
// When using pseudo-terminal, we can't simply close the
|
||||
// standard input, as the file descriptor is shared for
|
||||
// standard output and standard error too, so we send
|
||||
// an EOF character instead
|
||||
if _, err := stdin.Write([]byte{eofChar}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := stdin.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*closed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rpc *RPC) Signal(_ context.Context, request *SignalRequest) (*emptypb.Empty, error) {
|
||||
process, ok := rpc.execs.Load(request.GetExecId())
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("exec %q is not running", request.GetExecId())
|
||||
}
|
||||
|
||||
var signal syscall.Signal
|
||||
|
||||
switch request.GetSignal() {
|
||||
case SignalRequest_SIGNAL_SIGTERM:
|
||||
signal = syscall.SIGTERM
|
||||
case SignalRequest_SIGNAL_SIGKILL:
|
||||
signal = syscall.SIGKILL
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported exec signal %q", request.GetSignal().String())
|
||||
}
|
||||
|
||||
if err := signalProcessGroup(process, signal); err != nil {
|
||||
// The process may exit after lookup, so treat the missing process as a no-op
|
||||
if errors.Is(err, os.ErrProcessDone) {
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func sendStartSuccess(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse], execID string) error {
|
||||
return stream.Send(&ExecResponse{
|
||||
Type: &ExecResponse_Started_{
|
||||
Started: &ExecResponse_Started{
|
||||
ExecId: execID,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func sendStartFailure(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error {
|
||||
return stream.Send(&ExecResponse{
|
||||
Type: &ExecResponse_Exit_{
|
||||
Exit: &ExecResponse_Exit{
|
||||
Code: execRuntimeFailureExitCode,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func applyExecOverrides(cmd *exec.Cmd, command *ExecRequest_Command) error {
|
||||
if command.Workdir != "" {
|
||||
cmd.Dir = command.Workdir
|
||||
}
|
||||
|
|
@ -286,6 +458,37 @@ func applyExecOverrides(cmd *exec.Cmd, command *ExecRequest_Command) {
|
|||
if len(command.Env) > 0 {
|
||||
cmd.Env = mergeEnv(command.Env)
|
||||
}
|
||||
|
||||
if user := command.GetUser(); user != "" {
|
||||
selectedUser, err := userpkg.Lookup(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve user %q: %w", user, err)
|
||||
}
|
||||
|
||||
uid, err := strconv.ParseUint(selectedUser.Uid, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse UID %q for user %q: %w",
|
||||
selectedUser.Uid, user, err)
|
||||
}
|
||||
|
||||
gid, err := strconv.ParseUint(selectedUser.Gid, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse GID %q for user %q: %w",
|
||||
selectedUser.Gid, user, err)
|
||||
}
|
||||
|
||||
if uint32(uid) == uint32(os.Geteuid()) && uint32(gid) == uint32(os.Getegid()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Avoid changing credentials when the requested user is the same as guest agen't user
|
||||
cmd.SysProcAttr.Credential = &syscall.Credential{
|
||||
Uid: uint32(uid),
|
||||
Gid: uint32(gid),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeEnv(overrides map[string]string) []string {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
// In-process stream scaffolding intentionally favors direct test construction.
|
||||
//
|
||||
//nolint:containedctx,testpackage,wsl_v5
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
execTestShell = "/bin/sh"
|
||||
execTestTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type execTestStream struct {
|
||||
grpc.ServerStream
|
||||
|
||||
ctx context.Context
|
||||
requests chan *ExecRequest
|
||||
responses chan *ExecResponse
|
||||
sendHook func(*ExecResponse) error
|
||||
}
|
||||
|
||||
var _ grpc.BidiStreamingServer[ExecRequest, ExecResponse] = (*execTestStream)(nil)
|
||||
|
||||
func newExecTestStream(ctx context.Context) *execTestStream {
|
||||
return &execTestStream{
|
||||
ctx: ctx,
|
||||
requests: make(chan *ExecRequest, 8),
|
||||
responses: make(chan *ExecResponse, 8),
|
||||
}
|
||||
}
|
||||
|
||||
func (stream *execTestStream) Send(response *ExecResponse) error {
|
||||
if stream.sendHook != nil {
|
||||
if err := stream.sendHook(response); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case stream.responses <- response:
|
||||
return nil
|
||||
case <-stream.ctx.Done():
|
||||
return stream.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (stream *execTestStream) Recv() (*ExecRequest, error) {
|
||||
select {
|
||||
case request, ok := <-stream.requests:
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return request, nil
|
||||
case <-stream.ctx.Done():
|
||||
return nil, stream.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (stream *execTestStream) Context() context.Context { return stream.ctx }
|
||||
|
||||
func TestExecSendsStartedBeforeOutputAndExit(t *testing.T) {
|
||||
_, stream, result := startExecTest(t, &ExecRequest_Command{
|
||||
Name: execTestShell,
|
||||
Args: []string{"-c", "printf hello"},
|
||||
})
|
||||
|
||||
first := receiveExecResponse(t, stream)
|
||||
require.NotNil(t, first.GetStarted())
|
||||
|
||||
var output []byte
|
||||
for {
|
||||
response := receiveExecResponse(t, stream)
|
||||
switch response := response.GetType().(type) {
|
||||
case *ExecResponse_StandardOutput:
|
||||
output = append(output, response.StandardOutput.GetData()...)
|
||||
case *ExecResponse_Exit_:
|
||||
require.EqualValues(t, 0, response.Exit.GetCode())
|
||||
require.Equal(t, []byte("hello"), output)
|
||||
require.NoError(t, receiveExecResult(t, result))
|
||||
return
|
||||
default:
|
||||
t.Fatalf("unexpected exec response %T", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecClosesStandardInputOnRequestStreamEOF(t *testing.T) {
|
||||
_, stream, result := startExecTest(t, &ExecRequest_Command{
|
||||
Name: "/bin/cat",
|
||||
Interactive: true,
|
||||
})
|
||||
require.NotNil(t, receiveExecResponse(t, stream).GetStarted())
|
||||
|
||||
stream.requests <- &ExecRequest{
|
||||
Type: &ExecRequest_StandardInput{
|
||||
StandardInput: &IOChunk{Data: []byte("hello")},
|
||||
},
|
||||
}
|
||||
close(stream.requests)
|
||||
|
||||
response := receiveExecResponse(t, stream)
|
||||
require.Equal(t, []byte("hello"), response.GetStandardOutput().GetData())
|
||||
response = receiveExecResponse(t, stream)
|
||||
require.EqualValues(t, 0, response.GetExit().GetCode())
|
||||
require.NoError(t, receiveExecResult(t, result))
|
||||
}
|
||||
|
||||
func TestExecClosesStandardInputOnEmptyChunk(t *testing.T) {
|
||||
_, stream, result := startExecTest(t, &ExecRequest_Command{
|
||||
Name: "/bin/cat",
|
||||
Interactive: true,
|
||||
})
|
||||
require.NotNil(t, receiveExecResponse(t, stream).GetStarted())
|
||||
|
||||
stream.requests <- &ExecRequest{
|
||||
Type: &ExecRequest_StandardInput{
|
||||
StandardInput: &IOChunk{Data: []byte("hello")},
|
||||
},
|
||||
}
|
||||
stream.requests <- &ExecRequest{
|
||||
Type: &ExecRequest_StandardInput{
|
||||
StandardInput: &IOChunk{},
|
||||
},
|
||||
}
|
||||
|
||||
response := receiveExecResponse(t, stream)
|
||||
require.Equal(t, []byte("hello"), response.GetStandardOutput().GetData())
|
||||
response = receiveExecResponse(t, stream)
|
||||
require.EqualValues(t, 0, response.GetExit().GetCode())
|
||||
require.NoError(t, receiveExecResult(t, result))
|
||||
}
|
||||
|
||||
func TestExecReportsStartFailureBeforeStarted(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
command *ExecRequest_Command
|
||||
}{
|
||||
{
|
||||
name: "missing executable",
|
||||
command: &ExecRequest_Command{
|
||||
Name: "/definitely/missing/tart-guest-agent-test-command",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing workdir",
|
||||
command: &ExecRequest_Command{
|
||||
Name: execTestShell,
|
||||
Workdir: "/definitely/missing/tart-guest-agent-test-workdir",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, stream, result := startExecTest(t, test.command)
|
||||
response := receiveExecResponse(t, stream)
|
||||
require.Nil(t, response.GetStarted())
|
||||
require.EqualValues(t, execRuntimeFailureExitCode, response.GetExit().GetCode())
|
||||
require.NoError(t, receiveExecResult(t, result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecSignalsProcess(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
signal SignalRequest_Signal
|
||||
code int32
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "SIGTERM",
|
||||
signal: SignalRequest_SIGNAL_SIGTERM,
|
||||
code: int32(signalExitCodeOffset + syscall.SIGTERM),
|
||||
},
|
||||
{
|
||||
name: "SIGKILL",
|
||||
signal: SignalRequest_SIGNAL_SIGKILL,
|
||||
code: int32(signalExitCodeOffset + syscall.SIGKILL),
|
||||
},
|
||||
{
|
||||
name: "unsupported",
|
||||
signal: SignalRequest_SIGNAL_UNSPECIFIED,
|
||||
err: `unsupported exec signal "SIGNAL_UNSPECIFIED"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rpc, stream, result := startExecTest(t, &ExecRequest_Command{
|
||||
Name: "/bin/sleep",
|
||||
Args: []string{"30"},
|
||||
})
|
||||
started := receiveExecResponse(t, stream).GetStarted()
|
||||
require.NotNil(t, started)
|
||||
|
||||
_, err := rpc.Signal(context.Background(), &SignalRequest{
|
||||
ExecId: started.GetExecId(),
|
||||
Signal: test.signal,
|
||||
})
|
||||
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
_, err = rpc.Signal(context.Background(), &SignalRequest{
|
||||
ExecId: started.GetExecId(),
|
||||
Signal: SignalRequest_SIGNAL_SIGKILL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
receiveExecResponse(t, stream)
|
||||
require.NoError(t, receiveExecResult(t, result))
|
||||
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
response := receiveExecResponse(t, stream)
|
||||
require.NotNil(t, response.GetExit())
|
||||
require.Equal(t, test.code, response.GetExit().GetCode())
|
||||
require.NoError(t, receiveExecResult(t, result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecSignalsProcessGroup(t *testing.T) {
|
||||
rpc, stream, result := startExecTest(t, &ExecRequest_Command{
|
||||
Name: execTestShell,
|
||||
Args: []string{"-c", "sleep 30 & printf ready; wait"},
|
||||
})
|
||||
started := receiveExecResponse(t, stream).GetStarted()
|
||||
require.NotNil(t, started)
|
||||
require.Equal(t, []byte("ready"), receiveExecResponse(t, stream).GetStandardOutput().GetData())
|
||||
|
||||
_, err := rpc.Signal(context.Background(), &SignalRequest{
|
||||
ExecId: started.GetExecId(),
|
||||
Signal: SignalRequest_SIGNAL_SIGTERM,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
response := receiveExecResponse(t, stream)
|
||||
require.EqualValues(t, signalExitCodeOffset+syscall.SIGTERM, response.GetExit().GetCode())
|
||||
require.NoError(t, receiveExecResult(t, result))
|
||||
}
|
||||
|
||||
func TestExecReapsProcessWhenStartedCannotBeSent(t *testing.T) {
|
||||
pidPath := filepath.Join(t.TempDir(), "pid")
|
||||
sendErr := errors.New("failed to send Started")
|
||||
var processPID int
|
||||
|
||||
_, _, result := startExecTest(t, &ExecRequest_Command{
|
||||
Name: execTestShell,
|
||||
Args: []string{"-c", `printf %d "$$" > "$PID_FILE"; exec sleep 30`},
|
||||
Env: map[string]string{"PID_FILE": pidPath},
|
||||
}, func(stream *execTestStream) {
|
||||
stream.sendHook = func(response *ExecResponse) error {
|
||||
if response.GetStarted() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
processPID, err = waitForExecTestPID(pidPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sendErr
|
||||
}
|
||||
})
|
||||
|
||||
require.ErrorIs(t, receiveExecResult(t, result), sendErr)
|
||||
require.ErrorIs(t, syscall.Kill(processPID, 0), syscall.ESRCH)
|
||||
}
|
||||
|
||||
func startExecTest(
|
||||
t *testing.T,
|
||||
command *ExecRequest_Command,
|
||||
configure ...func(*execTestStream),
|
||||
) (*RPC, *execTestStream, <-chan error) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
stream := newExecTestStream(ctx)
|
||||
for _, configureStream := range configure {
|
||||
configureStream(stream)
|
||||
}
|
||||
rpc, err := New(nil)
|
||||
require.NoError(t, err)
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
result <- rpc.Exec(stream)
|
||||
}()
|
||||
stream.requests <- &ExecRequest{
|
||||
Type: &ExecRequest_Command_{Command: command},
|
||||
}
|
||||
return rpc, stream, result
|
||||
}
|
||||
|
||||
func receiveExecResponse(t *testing.T, stream *execTestStream) *ExecResponse {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case response := <-stream.responses:
|
||||
return response
|
||||
case <-time.After(execTestTimeout):
|
||||
t.Fatal("timed out waiting for exec response")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func receiveExecResult(t *testing.T, result <-chan error) error {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case err := <-result:
|
||||
return err
|
||||
case <-time.After(execTestTimeout):
|
||||
t.Fatal("timed out waiting for Exec to return")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func waitForExecTestPID(path string) (int, error) {
|
||||
deadline := time.Now().Add(execTestTimeout)
|
||||
for time.Now().Before(deadline) {
|
||||
//nolint:gosec // path is created under t.TempDir by the test
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return strconv.Atoi(string(data))
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
return 0, context.DeadlineExceeded
|
||||
}
|
||||
|
|
@ -2,13 +2,17 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"google.golang.org/grpc"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type RPC struct {
|
||||
grpcServer *grpc.Server
|
||||
listener net.Listener
|
||||
execs *xsync.Map[string, *os.Process]
|
||||
|
||||
UnimplementedAgentServer
|
||||
}
|
||||
|
|
@ -17,6 +21,7 @@ func New(listener net.Listener) (*RPC, error) {
|
|||
rpc := &RPC{
|
||||
grpcServer: grpc.NewServer(),
|
||||
listener: listener,
|
||||
execs: xsync.NewMap[string, *os.Process](),
|
||||
}
|
||||
|
||||
RegisterAgentServer(rpc.grpcServer, rpc)
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ func (agent *VDAgent) Run(ctx context.Context) error {
|
|||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case newClipboardState := <-clipboardCh:
|
||||
if err := agent.processClipboardState(newClipboardState); err != nil {
|
||||
if err := agent.processClipboardState(newClipboardState.Bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
agent.lastClipboardState = newClipboardState
|
||||
agent.lastClipboardState = newClipboardState.Bytes
|
||||
default:
|
||||
// Nothing, proceed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ option go_package = "github.com/cirruslabs/tart-guest-agent/internal/rpc";
|
|||
|
||||
service Agent {
|
||||
rpc Exec(stream ExecRequest) returns (stream ExecResponse);
|
||||
rpc Signal(SignalRequest) returns (google.protobuf.Empty);
|
||||
rpc ResolveIP(ResolveIPRequest) returns (ResolveIPResponse);
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ message ExecRequest {
|
|||
bool detach = 6;
|
||||
map<string, string> env = 7;
|
||||
string workdir = 8;
|
||||
string user = 9;
|
||||
}
|
||||
|
||||
oneof type {
|
||||
|
|
@ -33,10 +35,15 @@ message ExecResponse {
|
|||
int32 code = 1;
|
||||
}
|
||||
|
||||
message Started {
|
||||
string exec_id = 1;
|
||||
}
|
||||
|
||||
oneof type {
|
||||
Exit exit = 1;
|
||||
IOChunk standard_output = 2;
|
||||
IOChunk standard_error = 3;
|
||||
Started started = 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,3 +63,14 @@ message ResolveIPRequest {
|
|||
message ResolveIPResponse {
|
||||
string ip = 1;
|
||||
}
|
||||
|
||||
message SignalRequest {
|
||||
enum Signal {
|
||||
SIGNAL_UNSPECIFIED = 0;
|
||||
SIGNAL_SIGTERM = 1;
|
||||
SIGNAL_SIGKILL = 2;
|
||||
}
|
||||
|
||||
string exec_id = 1;
|
||||
Signal signal = 2;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue