Implement SPICE vdagent with clipboard sharing functionality (#3)
* Implement SPICE vdagent clipboard copy-paste functionality * Install libx11-dev * Fix typo in "sucessfully" * Simply print the capabilities as an integer
This commit is contained in:
parent
b59f2c51e8
commit
dbffcb0b08
|
|
@ -1,4 +0,0 @@
|
|||
load("github.com/cirrus-modules/golang@main", "lint_task")
|
||||
|
||||
def main(ctx):
|
||||
return [lint_task()]
|
||||
26
.cirrus.yml
26
.cirrus.yml
|
|
@ -1,9 +1,29 @@
|
|||
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:
|
||||
|
|
@ -24,6 +44,9 @@ task:
|
|||
depends_on:
|
||||
- Lint
|
||||
- Tests
|
||||
prepare_script:
|
||||
- apt-get update
|
||||
- apt-get install -y libx11-dev
|
||||
install_script:
|
||||
- echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list
|
||||
- apt-get update
|
||||
|
|
@ -45,6 +68,9 @@ task:
|
|||
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
||||
FURY_TOKEN: ENCRYPTED[!97fe4497d9aca60a3d64904883b81e21f19706c6aedda625c97f62f67ec46b8efa74c55699956158bbf0a23726e7d9f6!]
|
||||
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
||||
prepare_script:
|
||||
- apt-get update
|
||||
- apt-get install -y libx11-dev
|
||||
install_script:
|
||||
- echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list
|
||||
- apt-get update
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -8,6 +8,7 @@ require (
|
|||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.design/x/clipboard v0.7.0
|
||||
golang.org/x/sys v0.32.0
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
|
@ -18,6 +19,9 @@ require (
|
|||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
||||
golang.org/x/image v0.6.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
48
go.sum
48
go.sum
|
|
@ -1,3 +1,4 @@
|
|||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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=
|
||||
|
|
@ -21,16 +22,63 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
|||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo=
|
||||
golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E=
|
||||
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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
|
||||
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
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.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/diskresizer"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/logginglevel"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/spice/vdagent"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/tart"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -14,6 +15,8 @@ import (
|
|||
"syscall"
|
||||
)
|
||||
|
||||
var resizeDisk bool
|
||||
var runVdagent bool
|
||||
var debug bool
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
|
|
@ -33,6 +36,11 @@ func NewRootCommand() *cobra.Command {
|
|||
RunE: run,
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&resizeDisk, "resize-disk", false, "resize disk")
|
||||
cmd.Flags().BoolVar(&runVdagent, "run-vdagent", false, "run vdagent")
|
||||
|
||||
cmd.Flags().BoolVar(&debug, "debug", false, "enable debug logging")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +55,7 @@ func run(cmd *cobra.Command, args []string) error {
|
|||
communicationPoint)
|
||||
|
||||
// Perform disk resizing
|
||||
if resizeDisk {
|
||||
if err := diskresizer.Resize(); err != nil {
|
||||
if errors.Is(err, diskresizer.ErrUnsupported) || errors.Is(err, diskresizer.ErrAlreadyResized) {
|
||||
zap.S().Infof("skipping disk resizing: %v", err)
|
||||
|
|
@ -54,7 +63,19 @@ func run(cmd *cobra.Command, args []string) error {
|
|||
zap.S().Warnf("failed to resize disk: %v", err)
|
||||
}
|
||||
} else {
|
||||
zap.S().Infof("sucessfully resized the disk")
|
||||
zap.S().Infof("successfully resized the disk")
|
||||
}
|
||||
}
|
||||
|
||||
if runVdagent {
|
||||
vdAgent, err := vdagent.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := vdAgent.Run(cmd.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait indefinitely
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package vd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type VDAgentMessage struct {
|
||||
VDAgentMessageInner
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type VDAgentMessageInner struct {
|
||||
Protocol uint32
|
||||
Type uint32
|
||||
Opaque uint64
|
||||
Size uint32
|
||||
}
|
||||
|
||||
func ReadVDAgentMessage(r io.Reader) (*VDAgentMessage, error) {
|
||||
var vdiAgentMessage VDAgentMessage
|
||||
|
||||
if err := binary.Read(r, binary.LittleEndian, &vdiAgentMessage.VDAgentMessageInner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vdiAgentMessage.Data = make([]byte, vdiAgentMessage.Size)
|
||||
|
||||
if _, err := io.ReadFull(r, vdiAgentMessage.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &vdiAgentMessage, nil
|
||||
}
|
||||
|
||||
func (vdiAgentMessage *VDAgentMessage) Encode() ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
if err := binary.Write(buffer, binary.LittleEndian, vdiAgentMessage.VDAgentMessageInner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := buffer.Write(vdiAgentMessage.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (vdiAgentMessage VDAgentMessage) String() string {
|
||||
return fmt.Sprintf("%#+v", vdiAgentMessage.VDAgentMessageInner)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package vd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type VDAgentAnnounceCapabilities struct {
|
||||
Request uint32
|
||||
Caps uint32
|
||||
}
|
||||
|
||||
func ReadVDAgentAnnounceCapabilities(buf []byte) (*VDAgentAnnounceCapabilities, error) {
|
||||
var vdAgentAnnounceCapabilities VDAgentAnnounceCapabilities
|
||||
|
||||
if err := binary.Read(bytes.NewReader(buf), binary.LittleEndian, &vdAgentAnnounceCapabilities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &vdAgentAnnounceCapabilities, nil
|
||||
}
|
||||
|
||||
func (vdAgentAnnounceCapabilities VDAgentAnnounceCapabilities) Encode() ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
if err := binary.Write(buffer, binary.LittleEndian, &vdAgentAnnounceCapabilities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (vdAgentAnnounceCapabilities VDAgentAnnounceCapabilities) String() string {
|
||||
return fmt.Sprintf("VDAgentAnnounceCapabilities(request=%d, capabilities=%d)",
|
||||
vdAgentAnnounceCapabilities.Request, vdAgentAnnounceCapabilities.Caps)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package vd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type VDAgentClipboard struct {
|
||||
VDAgentClipboardInner
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type VDAgentClipboardInner struct {
|
||||
Selection uint8
|
||||
_ [3]uint8
|
||||
Type uint32
|
||||
}
|
||||
|
||||
func DecodeVDAgentClipboard(buf []byte) (*VDAgentClipboard, error) {
|
||||
var vdAgentClipboard VDAgentClipboard
|
||||
|
||||
r := bufio.NewReader(bytes.NewReader(buf))
|
||||
|
||||
err := binary.Read(r, binary.LittleEndian, &vdAgentClipboard.VDAgentClipboardInner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vdAgentClipboard.Data, err = io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &vdAgentClipboard, nil
|
||||
}
|
||||
|
||||
func (vdAgentClipboard VDAgentClipboard) Encode() ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
if err := binary.Write(buffer, binary.LittleEndian, &vdAgentClipboard.VDAgentClipboardInner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := buffer.Write(vdAgentClipboard.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (vdAgentClipboard VDAgentClipboard) String() string {
|
||||
return fmt.Sprintf("VDAgentClipboard(selection=%d, type=%d, data=%d bytes)",
|
||||
vdAgentClipboard.Selection, vdAgentClipboard.Type, len(vdAgentClipboard.Data))
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package vd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type VDAgentClipboardGrab struct {
|
||||
Selection uint8
|
||||
_ [3]uint8
|
||||
Type uint32
|
||||
}
|
||||
|
||||
func DecodeVDAgentClipboardGrab(r io.Reader) (*VDAgentClipboardGrab, error) {
|
||||
var vdAgentClipboardGrab VDAgentClipboardGrab
|
||||
|
||||
if err := binary.Read(r, binary.LittleEndian, &vdAgentClipboardGrab); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &vdAgentClipboardGrab, nil
|
||||
}
|
||||
|
||||
func (vdAgentClipboardGrab VDAgentClipboardGrab) Encode() ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
if err := binary.Write(buffer, binary.LittleEndian, &vdAgentClipboardGrab); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (vdAgentClipboardGrab VDAgentClipboardGrab) String() string {
|
||||
return fmt.Sprintf("VDAgentClipboardGrab(selection=%d, type=%d)",
|
||||
vdAgentClipboardGrab.Selection, vdAgentClipboardGrab.Type)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package vd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type VDAgentClipboardRequest struct {
|
||||
Selection uint8
|
||||
_ [3]uint8
|
||||
Type uint32
|
||||
}
|
||||
|
||||
func DecodeVDAgentClipboardRequest(r io.Reader) (*VDAgentClipboardRequest, error) {
|
||||
var vdAgentClipboardRequest VDAgentClipboardRequest
|
||||
|
||||
if err := binary.Read(r, binary.LittleEndian, &vdAgentClipboardRequest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &vdAgentClipboardRequest, nil
|
||||
}
|
||||
|
||||
func (vdAgentClipboardRequest VDAgentClipboardRequest) Encode() ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
if err := binary.Write(buffer, binary.LittleEndian, &vdAgentClipboardRequest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (vdAgentClipboardRequest VDAgentClipboardRequest) String() string {
|
||||
return fmt.Sprintf("VDAgentClipboardRequest(selection=%d, type=%d)",
|
||||
vdAgentClipboardRequest.Selection, vdAgentClipboardRequest.Type)
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package vd
|
||||
|
||||
// Protocols
|
||||
const VD_AGENT_PROTOCOL = 1
|
||||
|
||||
// Ports
|
||||
const (
|
||||
VDP_CLIENT_PORT = iota + 1
|
||||
VDP_SERVER_PORT
|
||||
VDP_END_PORT
|
||||
)
|
||||
|
||||
// Message types
|
||||
const (
|
||||
VD_AGENT_MOUSE_STATE = iota + 1
|
||||
VD_AGENT_MONITORS_CONFIG
|
||||
VD_AGENT_REPLY
|
||||
VD_AGENT_CLIPBOARD
|
||||
VD_AGENT_DISPLAY_CONFIG
|
||||
VD_AGENT_ANNOUNCE_CAPABILITIES
|
||||
VD_AGENT_CLIPBOARD_GRAB
|
||||
VD_AGENT_CLIPBOARD_REQUEST
|
||||
VD_AGENT_CLIPBOARD_RELEASE
|
||||
VD_AGENT_FILE_XFER_START
|
||||
VD_AGENT_FILE_XFER_STATUS
|
||||
VD_AGENT_FILE_XFER_DATA
|
||||
VD_AGENT_CLIENT_DISCONNECTED
|
||||
VD_AGENT_MAX_CLIPBOARD
|
||||
VD_AGENT_AUDIO_VOLUME_SYNC
|
||||
VD_AGENT_GRAPHICS_DEVICE_INFO
|
||||
VD_AGENT_END_MESSAGE
|
||||
)
|
||||
|
||||
// Capabilities
|
||||
const (
|
||||
VD_AGENT_CAP_MOUSE_STATE = 1 << iota
|
||||
VD_AGENT_CAP_MONITORS_CONFIG
|
||||
VD_AGENT_CAP_REPLY
|
||||
VD_AGENT_CAP_CLIPBOARD
|
||||
VD_AGENT_CAP_DISPLAY_CONFIG
|
||||
VD_AGENT_CAP_CLIPBOARD_BY_DEMAND
|
||||
VD_AGENT_CAP_CLIPBOARD_SELECTION
|
||||
VD_AGENT_CAP_SPARSE_MONITORS_CONFIG
|
||||
VD_AGENT_CAP_GUEST_LINEEND_LF
|
||||
VD_AGENT_CAP_GUEST_LINEEND_CRLF
|
||||
VD_AGENT_CAP_MAX_CLIPBOARD
|
||||
VD_AGENT_CAP_AUDIO_VOLUME_SYNC
|
||||
VD_AGENT_CAP_MONITORS_CONFIG_POSITION
|
||||
VD_AGENT_CAP_FILE_XFER_DISABLED
|
||||
VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS
|
||||
VD_AGENT_CAP_GRAPHICS_DEVICE_INFO
|
||||
VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB
|
||||
VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL
|
||||
VD_AGENT_END_CAP
|
||||
)
|
||||
|
||||
// Clipboard selections
|
||||
const (
|
||||
VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD = iota
|
||||
VD_AGENT_CLIPBOARD_SELECTION_PRIMARY
|
||||
VD_AGENT_CLIPBOARD_SELECTION_SECONDARY
|
||||
)
|
||||
|
||||
// Clipboard content types
|
||||
const (
|
||||
VD_AGENT_CLIPBOARD_NONE = iota
|
||||
VD_AGENT_CLIPBOARD_UTF8_TEXT
|
||||
VD_AGENT_CLIPBOARD_IMAGE_PNG
|
||||
VD_AGENT_CLIPBOARD_IMAGE_BMP
|
||||
VD_AGENT_CLIPBOARD_IMAGE_TIFF
|
||||
VD_AGENT_CLIPBOARD_IMAGE_JPG
|
||||
VD_AGENT_CLIPBOARD_FILE_LIST
|
||||
)
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
package vdagent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/spice/vd"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/spice/vdi"
|
||||
"go.uber.org/zap"
|
||||
"golang.design/x/clipboard"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const serialPortPath = "/dev/tty.com.redhat.spice.0"
|
||||
|
||||
type VDAgent struct {
|
||||
serialPort *os.File
|
||||
vdi *vdi.VDI
|
||||
lastClipboardState []byte
|
||||
}
|
||||
|
||||
func New() (*VDAgent, error) {
|
||||
sp, err := os.OpenFile(serialPortPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := clipboard.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &VDAgent{
|
||||
serialPort: sp,
|
||||
vdi: vdi.New(sp),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (agent *VDAgent) Run(ctx context.Context) error {
|
||||
clipboardCh := clipboard.Watch(ctx, clipboard.FmtText)
|
||||
|
||||
for {
|
||||
// Check for cancellation and clipboard changes
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case newClipboardState := <-clipboardCh:
|
||||
if err := agent.processClipboardState(newClipboardState); err != nil {
|
||||
return err
|
||||
}
|
||||
agent.lastClipboardState = newClipboardState
|
||||
default:
|
||||
// Nothing, proceed
|
||||
}
|
||||
|
||||
if err := agent.serialPort.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vdiAgentMessage, err := vd.ReadVDAgentMessage(agent.vdi)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
switch vdiAgentMessage.Type {
|
||||
case vd.VD_AGENT_ANNOUNCE_CAPABILITIES:
|
||||
vdAgentAnnounceCapabilities, err := vd.ReadVDAgentAnnounceCapabilities(vdiAgentMessage.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("I: VD_AGENT_ANNOUNCE_CAPABILITIES: %s", vdAgentAnnounceCapabilities)
|
||||
|
||||
if vdAgentAnnounceCapabilities.Request == 0 {
|
||||
// No need to send our capabilities
|
||||
break
|
||||
}
|
||||
|
||||
// Send our capabilities
|
||||
ourCapabilities := vd.VDAgentAnnounceCapabilities{
|
||||
Request: 0,
|
||||
Caps: vd.VD_AGENT_CAP_CLIPBOARD_BY_DEMAND | vd.VD_AGENT_CAP_CLIPBOARD_SELECTION,
|
||||
}
|
||||
ourCapabilitiesBytes, err := ourCapabilities.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ourAgentMessage := vd.VDAgentMessage{
|
||||
VDAgentMessageInner: vd.VDAgentMessageInner{
|
||||
Protocol: vd.VD_AGENT_PROTOCOL,
|
||||
Type: vd.VD_AGENT_ANNOUNCE_CAPABILITIES,
|
||||
Size: uint32(len(ourCapabilitiesBytes)),
|
||||
},
|
||||
Data: ourCapabilitiesBytes,
|
||||
}
|
||||
ourAgentMessageBytes, err := ourAgentMessage.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := agent.vdi.Write(ourAgentMessageBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("O: VD_AGENT_ANNOUNCE_CAPABILITIES")
|
||||
case vd.VD_AGENT_CLIPBOARD_GRAB:
|
||||
vdAgentClipboardGrab, err := vd.DecodeVDAgentClipboardGrab(bytes.NewReader(vdiAgentMessage.Data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("I: VD_AGENT_CLIPBOARD_GRAB (%d bytes): %s",
|
||||
len(vdiAgentMessage.Data), vdAgentClipboardGrab)
|
||||
|
||||
ourClipboardRequest := vd.VDAgentClipboardRequest{
|
||||
Type: 1,
|
||||
}
|
||||
ourClipboardRequestBytes, err := ourClipboardRequest.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ourAgentMessage := vd.VDAgentMessage{
|
||||
VDAgentMessageInner: vd.VDAgentMessageInner{
|
||||
Protocol: vd.VD_AGENT_PROTOCOL,
|
||||
Type: vd.VD_AGENT_CLIPBOARD_REQUEST,
|
||||
Size: uint32(len(ourClipboardRequestBytes)),
|
||||
},
|
||||
Data: ourClipboardRequestBytes,
|
||||
}
|
||||
ourAgentMessageBytes, err := ourAgentMessage.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := agent.vdi.Write(ourAgentMessageBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("O: VD_AGENT_CLIPBOARD_REQUEST")
|
||||
case vd.VD_AGENT_CLIPBOARD:
|
||||
// Receive clipboard
|
||||
vdAgentClipboard, err := vd.DecodeVDAgentClipboard(vdiAgentMessage.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("I: VD_AGENT_CLIPBOARD: %s", vdAgentClipboard)
|
||||
|
||||
clipboard.Write(clipboard.FmtText, vdAgentClipboard.Data)
|
||||
case vd.VD_AGENT_CLIPBOARD_REQUEST:
|
||||
vdAgentClipboardRequest, err := vd.DecodeVDAgentClipboardRequest(bytes.NewReader(vdiAgentMessage.Data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("I: VD_AGENT_CLIPBOARD_REQUEST: %s", vdAgentClipboardRequest)
|
||||
|
||||
// Send clipboard
|
||||
ourAgentClipboard := vd.VDAgentClipboard{
|
||||
VDAgentClipboardInner: vd.VDAgentClipboardInner{
|
||||
Selection: vd.VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD,
|
||||
Type: vd.VD_AGENT_CLIPBOARD_UTF8_TEXT,
|
||||
},
|
||||
Data: clipboard.Read(clipboard.FmtText),
|
||||
}
|
||||
ourAgentClipboardBytes, err := ourAgentClipboard.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ourAgentMessage := vd.VDAgentMessage{
|
||||
VDAgentMessageInner: vd.VDAgentMessageInner{
|
||||
Protocol: vd.VD_AGENT_PROTOCOL,
|
||||
Type: vd.VD_AGENT_CLIPBOARD,
|
||||
Size: uint32(len(ourAgentClipboardBytes)),
|
||||
},
|
||||
Data: ourAgentClipboardBytes,
|
||||
}
|
||||
ourAgentMessageBytes, err := ourAgentMessage.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := agent.vdi.Write(ourAgentMessageBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("O: VD_AGENT_CLIPBOARD")
|
||||
default:
|
||||
zap.S().Debugf("I: unhandled message type: %d", vdiAgentMessage.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (agent *VDAgent) processClipboardState(newClipboardState []byte) error {
|
||||
if bytes.Equal(agent.lastClipboardState, newClipboardState) {
|
||||
// Nothing changed since the last VD_AGENT_CLIPBOARD_GRAB from us
|
||||
return nil
|
||||
}
|
||||
|
||||
ourGrab := vd.VDAgentClipboardGrab{
|
||||
Selection: vd.VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD,
|
||||
Type: vd.VD_AGENT_CLIPBOARD_UTF8_TEXT,
|
||||
}
|
||||
ourGrabBytes, err := ourGrab.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ourAgentMessage := vd.VDAgentMessage{
|
||||
VDAgentMessageInner: vd.VDAgentMessageInner{
|
||||
Protocol: vd.VD_AGENT_PROTOCOL,
|
||||
Type: vd.VD_AGENT_CLIPBOARD_GRAB,
|
||||
Size: uint32(len(ourGrabBytes)),
|
||||
},
|
||||
Data: ourGrabBytes,
|
||||
}
|
||||
ourAgentMessageBytes, err := ourAgentMessage.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := agent.vdi.Write(ourAgentMessageBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Debugf("O: VD_AGENT_CLIPBOARD_GRAB")
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package vdi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/spice/vd"
|
||||
"io"
|
||||
)
|
||||
|
||||
type VDI struct {
|
||||
inner io.ReadWriter
|
||||
remaining uint64
|
||||
}
|
||||
|
||||
type chunkHeader struct {
|
||||
Port uint32
|
||||
Size uint32
|
||||
}
|
||||
|
||||
func New(inner io.ReadWriter) *VDI {
|
||||
return &VDI{
|
||||
inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
func (vdi *VDI) Read(buf []byte) (int, error) {
|
||||
// Read payload
|
||||
readPayload:
|
||||
if vdi.remaining > 0 {
|
||||
toRead := min(len(buf), int(vdi.remaining))
|
||||
|
||||
n, err := vdi.inner.Read(buf[:toRead])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
vdi.remaining -= uint64(n)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Read header
|
||||
var vdiChunkHeader chunkHeader
|
||||
|
||||
if err := binary.Read(vdi.inner, binary.LittleEndian, &vdiChunkHeader); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
vdi.remaining = uint64(vdiChunkHeader.Size)
|
||||
|
||||
goto readPayload
|
||||
}
|
||||
|
||||
func (vdi *VDI) Write(buf []byte) (int, error) {
|
||||
// Write header
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
vdiChunkHeader := chunkHeader{
|
||||
Port: vd.VDP_CLIENT_PORT,
|
||||
Size: uint32(len(buf)),
|
||||
}
|
||||
if err := binary.Write(buffer, binary.LittleEndian, &vdiChunkHeader); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Write payload
|
||||
return vdi.inner.Write(append(buffer.Bytes(), buf...))
|
||||
}
|
||||
Loading…
Reference in New Issue