Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
# Checkout with full history to enable commit listing
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down Expand Up @@ -65,7 +65,7 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v6
Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
arch: [amd64, arm64]
steps:
# Checkout the specific commit to test
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
ref: ${{ matrix.commit }}

Expand All @@ -116,6 +116,6 @@ jobs:
name: Check License Lines
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7

- uses: kt3k/license_checker@v1.0.6
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
manifest-file: .release-please-manifest.json

- if: ${{ steps.release.outputs.release_created }}
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Go
if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-go@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/repo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
name: Conventional Commits
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v6
Expand Down
47 changes: 25 additions & 22 deletions cmds/dutagent/dutagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,22 @@ package main

import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"connectrpc.com/connect"
"github.com/BlindspotSoftware/dutctl/internal/buildinfo"
"github.com/BlindspotSoftware/dutctl/internal/dutagent"
"github.com/BlindspotSoftware/dutctl/pkg/dut"
"github.com/BlindspotSoftware/dutctl/protobuf/gen/dutctl/v1/dutctlv1connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"gopkg.in/yaml.v3"

pb "github.com/BlindspotSoftware/dutctl/protobuf/gen/dutctl/v1"
Expand Down Expand Up @@ -155,6 +152,9 @@ func printInitErr(err error) {
log.Print(err)
}

// readHeaderTimeout bounds how long the server waits to read request headers.
const readHeaderTimeout = 10 * time.Second

// startRPCService starts the RPC service, that ideally listens for incoming
// connections forever. It always returns an non-nil error.
func (agt *agent) startRPCService() error {
Expand All @@ -167,12 +167,17 @@ func (agt *agent) startRPCService() error {
path, handler := dutctlv1connect.NewDeviceServiceHandler(service)
mux.Handle(path, handler)

//nolint:gosec
return http.ListenAndServe(
agt.address,
// Use h2c so we can serve HTTP/2 without TLS.
h2c.NewHandler(mux, &http2.Server{}),
)
// Serve HTTP/2 without TLS (h2c)
srv := &http.Server{
Addr: agt.address,
Handler: mux,
ReadHeaderTimeout: readHeaderTimeout,
}
srv.Protocols = new(http.Protocols)
srv.Protocols.SetHTTP1(true)
srv.Protocols.SetUnencryptedHTTP2(true)

return srv.ListenAndServe()
}

func (agt *agent) registerWithServer() error {
Expand Down Expand Up @@ -211,19 +216,17 @@ func spawnClient(agendURL string) dutctlv1connect.RelayServiceClient {

// TODO: refactor into pkg and reuse in dutctl and dutserver.
func newInsecureClient() *http.Client {
transport := &http.Transport{}
transport.Protocols = new(http.Protocols)
transport.Protocols.SetUnencryptedHTTP2(true)

return &http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
// If you're also using this client for non-h2c traffic, you may want
// to delegate to tls.Dial if the network isn't TCP or the addr isn't
// in an allowlist.

//nolint:noctx
return net.Dial(network, addr)
},
// TODO: Don't forget timeouts!
},
Transport: transport,
// TODO: Don't forget timeouts! http.Client.Timeout must not be used here:
// it bounds the entire exchange including the response body, which would
// abort long-lived streaming RPCs. Instead use per-RPC context deadlines
// on unary calls and/or transport timeouts (DialContext,
// TLSHandshakeTimeout, ResponseHeaderTimeout, IdleConnTimeout).
}
}

Expand Down
27 changes: 11 additions & 16 deletions cmds/dutctl/dutctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
package main

import (
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"

Expand All @@ -22,7 +20,6 @@ import (
"github.com/BlindspotSoftware/dutctl/internal/output"
"github.com/BlindspotSoftware/dutctl/pkg/lock"
"github.com/BlindspotSoftware/dutctl/protobuf/gen/dutctl/v1/dutctlv1connect"
"golang.org/x/net/http2"
)

const usageAbstract = `dutctl - The client application of the DUT Control system.
Expand Down Expand Up @@ -128,7 +125,6 @@ type application struct {

func (app *application) setupRPCClient() {
client := dutctlv1connect.NewDeviceServiceClient(
// Instead of http.DefaultClient, use the HTTP/2 protocol without TLS
newInsecureClient(),
fmt.Sprintf("http://%s", app.serverAddr),
connect.WithGRPC(),
Expand All @@ -138,19 +134,18 @@ func (app *application) setupRPCClient() {
}

func newInsecureClient() *http.Client {
// Use the HTTP/2 protocol without TLS (h2c)
transport := &http.Transport{}
transport.Protocols = new(http.Protocols)
transport.Protocols.SetUnencryptedHTTP2(true)

return &http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
// If you're also using this client for non-h2c traffic, you may want
// to delegate to tls.Dial if the network isn't TCP or the addr isn't
// in an allowlist.

//nolint:noctx
return net.Dial(network, addr)
},
// Don't forget timeouts!
},
Transport: transport,
// TODO: Don't forget timeouts! http.Client.Timeout must not be used here:
// it bounds the entire exchange including the response body, which would
// abort long-lived streaming RPCs. Instead use per-RPC context deadlines
// on unary calls and/or transport timeouts (DialContext,
// TLSHandshakeTimeout, ResponseHeaderTimeout, IdleConnTimeout).
}
}

Expand Down
23 changes: 15 additions & 8 deletions cmds/exp/dutserver/dutserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ import (
"os"
"os/signal"
"syscall"
"time"

"github.com/BlindspotSoftware/dutctl/protobuf/gen/dutctl/v1/dutctlv1connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)

const (
Expand Down Expand Up @@ -76,6 +75,9 @@ func (svr *server) watchInterrupt() {
}()
}

// readHeaderTimeout bounds how long the server waits to read request headers.
const readHeaderTimeout = 10 * time.Second

// startRPCService starts the RPC service, that ideally listens for incoming
// connections forever. It always returns an non-nil error.
func (svr *server) startRPCService() error {
Expand All @@ -94,12 +96,17 @@ func (svr *server) startRPCService() error {
path, handler = dutctlv1connect.NewRelayServiceHandler(service)
mux.Handle(path, handler)

//nolint:gosec
return http.ListenAndServe(
svr.address,
// Use h2c so we can serve HTTP/2 without TLS.
h2c.NewHandler(mux, &http2.Server{}),
)
// Serve HTTP/2 without TLS (h2c)
srv := &http.Server{
Addr: svr.address,
Handler: mux,
ReadHeaderTimeout: readHeaderTimeout,
}
srv.Protocols = new(http.Protocols)
srv.Protocols.SetHTTP1(true)
srv.Protocols.SetUnencryptedHTTP2(true)

return srv.ListenAndServe()
}

// start orchestrates the dutagent execution.
Expand Down
26 changes: 11 additions & 15 deletions cmds/exp/dutserver/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,18 @@ package main

import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"maps"
"net"
"net/http"
"slices"
"sync"

"connectrpc.com/connect"
"github.com/BlindspotSoftware/dutctl/pkg/lock"
"github.com/BlindspotSoftware/dutctl/protobuf/gen/dutctl/v1/dutctlv1connect"
"golang.org/x/net/http2"

pb "github.com/BlindspotSoftware/dutctl/protobuf/gen/dutctl/v1"
)
Expand Down Expand Up @@ -390,19 +387,18 @@ func spawnClient(agendURL string) dutctlv1connect.DeviceServiceClient {

// TODO: refactor into pkg and reuse in dutctl and dutserver.
func newInsecureClient() *http.Client {
// Use the HTTP/2 protocol without TLS (h2c).
transport := &http.Transport{}
transport.Protocols = new(http.Protocols)
transport.Protocols.SetUnencryptedHTTP2(true)

return &http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
// If you're also using this client for non-h2c traffic, you may want
// to delegate to tls.Dial if the network isn't TCP or the addr isn't
// in an allowlist.

//nolint:noctx
return net.Dial(network, addr)
},
// TODO: Don't forget timeouts!
},
Transport: transport,
// TODO: Don't forget timeouts! http.Client.Timeout must not be used here:
// it bounds the entire exchange including the response body, which would
// abort long-lived streaming RPCs. Instead use per-RPC context deadlines
// on unary calls and/or transport timeouts (DialContext,
// TLSHandshakeTimeout, ResponseHeaderTimeout, IdleConnTimeout).
}
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ require (
github.com/stianeikeland/go-rpio/v4 v4.6.0
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
golang.org/x/crypto v0.51.0
golang.org/x/net v0.53.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)
Expand All @@ -32,6 +31,7 @@ require (
github.com/olekukonko/tablewriter v1.0.9 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
13 changes: 8 additions & 5 deletions pkg/dut/dut.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import (
)

var (
ErrDeviceNotFound = errors.New("no such device")
ErrCommandNotFound = errors.New("no such command")
ErrNoPassthroughForArgs = errors.New("arguments provided but command has no passthrough module to receive them")
ErrDeviceNotFound = errors.New("no such device")
ErrCommandNotFound = errors.New("no such command")
ErrNoReceiverForArgs = errors.New("arguments provided but command has neither a passthrough module nor declared arguments to receive them")
)

// Devlist is a list of devices-under-test.
Expand Down Expand Up @@ -124,8 +124,11 @@ func (c *Command) countPassthrough() int {
// receive their statically configured Args with template references substituted
// using runtimeArgs. The returned slice has the same length and ordering as c.Modules.
func (c *Command) ModuleArgs(runtimeArgs []string) ([][]string, error) {
if len(runtimeArgs) > 0 && !c.HasPassthrough() {
return nil, ErrNoPassthroughForArgs
// Runtime args may be consumed either by a passthrough module or by
// command-level templating (declared c.Args substituted via ${name}).
// Only reject when neither can receive them.
if len(runtimeArgs) > 0 && !c.HasPassthrough() && len(c.Args) == 0 {
return nil, ErrNoReceiverForArgs
}

result := make([][]string, len(c.Modules))
Expand Down
6 changes: 3 additions & 3 deletions pkg/dut/dut_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func TestModuleArgs(t *testing.T) {
}},
runtimeArgs: []string{"a"},
want: nil,
err: ErrNoPassthroughForArgs,
err: ErrNoReceiverForArgs,
},
{
name: "mixed passthrough and non-passthrough",
Expand Down Expand Up @@ -222,7 +222,7 @@ func TestModuleArgs(t *testing.T) {
cmd: Command{},
runtimeArgs: []string{"a"},
want: nil,
err: ErrNoPassthroughForArgs,
err: ErrNoReceiverForArgs,
},
{
name: "error when runtime args provided but no passthrough module",
Expand All @@ -231,7 +231,7 @@ func TestModuleArgs(t *testing.T) {
}},
runtimeArgs: []string{"a"},
want: nil,
err: ErrNoPassthroughForArgs,
err: ErrNoReceiverForArgs,
},
{
name: "passthrough module with no runtime args",
Expand Down