diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d8e7f86..d54077e 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: 1.25 - name: Build run: go build -v ./... diff --git a/README.md b/README.md index 4dec2a0..90bebb3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # go-lprlib ![Test](https://github.com/documatrix/go-lprlib/actions/workflows/go.yml/badge.svg) -This repository contains an implementation of the LPR protocol (send & receive) in go +This repository contains an implementation of the [LPR protocol](https://datatracker.ietf.org/doc/html/rfc1179) (send & receive) in go diff --git a/go.mod b/go.mod index ad4dc3d..f03c41e 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/documatrix/go-lprlib -go 1.18 +go 1.25 require ( github.com/stretchr/testify v1.8.0 - golang.org/x/text v0.3.8 + golang.org/x/text v0.31.0 ) require ( diff --git a/go.sum b/go.sum index c525c97..137fa24 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lpr_daemon.go b/lpr_daemon.go index d0edc5a..bbbfa9f 100644 --- a/lpr_daemon.go +++ b/lpr_daemon.go @@ -1,7 +1,6 @@ package lprlib import ( - "context" "errors" "fmt" "io" @@ -33,10 +32,6 @@ type QueueState func(queue string, list string, long bool) string type ExternalIDCallbackFunc func() uint64 -func init() { - rand.Seed(time.Now().UnixMicro()) -} - // LprDaemon structure type LprDaemon struct { finishedConns chan *LprConnection @@ -287,10 +282,6 @@ type LprConnection struct { // SaveName The File name of the new file SaveName string - // ctx is the lpr daemon's context. - // The connection must be closed once the context is canceled. - ctx context.Context - // daemon contains a reference to the LprDaemon daemon *LprDaemon @@ -374,22 +365,51 @@ func (lpr *LprConnection) RunConnection() { if err != nil { logErrorf("failed to create trace file: %v", err) } - defer traceFile.Close() + + defer func() { + err := traceFile.Close() + if err != nil { + logErrorf("failed to close trace file %s: %v", traceFile.Name(), err) + } + }() + logDebugf("Created trace file %s", traceFile.Name()) - traceFile.WriteString(fmt.Sprintf("LPR connection trace %s\n", time.Now())) + + _, err = fmt.Fprintf(traceFile, "LPR connection trace %s\n", time.Now()) + if err != nil { + logErrorf("failed to write to trace file %s: %v", traceFile.Name(), err) + } } for lpr.Status != Error && lpr.Status != End { command, err := lpr.ReadCommand() if traceFile != nil { - traceFile.WriteString(fmt.Sprintf("received message %d:\n", len(command))) + _, traceErr := fmt.Fprintf(traceFile, "received message %d:\n", len(command)) + if traceErr != nil { + logErrorf("failed to write to trace file %s: %v", traceFile.Name(), traceErr) + } + if err != nil { - traceFile.WriteString(fmt.Sprintf("error: %v\n", err)) + _, traceErr = fmt.Fprintf(traceFile, "error: %v\n", err) + if traceErr != nil { + logErrorf("failed to write to trace file %s: %v", traceFile.Name(), traceErr) + } } else { - traceFile.WriteString("-----\n") - traceFile.Write(command) - traceFile.WriteString("\n-----\n") + _, traceErr = traceFile.WriteString("-----\n") + if traceErr != nil { + logErrorf("failed to write to trace file %s: %v", traceFile.Name(), traceErr) + } + + _, traceErr = traceFile.Write(command) + if traceErr != nil { + logErrorf("failed to write to trace file %s: %v", traceFile.Name(), traceErr) + } + + _, traceErr = traceFile.WriteString("\n-----\n") + if traceErr != nil { + logErrorf("failed to write to trace file %s: %v", traceFile.Name(), traceErr) + } } } diff --git a/lpr_daemon_test.go b/lpr_daemon_test.go index e70b838..01eee5f 100644 --- a/lpr_daemon_test.go +++ b/lpr_daemon_test.go @@ -156,7 +156,7 @@ func TestDaemonLargeFileConnection(t *testing.T) { conn := <-lprd.FinishedConnections() - out, err = ioutil.ReadFile(conn.SaveName) + out, err = os.ReadFile(conn.SaveName) if err != nil { t.Error(err) } else { @@ -243,7 +243,7 @@ func TestDaemonMultipleConnection(t *testing.T) { i := 0 for conn := range lprd.FinishedConnections() { - out, err = ioutil.ReadFile(conn.SaveName) + out, err = os.ReadFile(conn.SaveName) if err != nil { t.Error(err) } else { @@ -288,7 +288,7 @@ func generateTempFile(dir, prefix, text string) (string, error) { var err error var file *os.File - file, err = ioutil.TempFile(dir, prefix) + file, err = os.CreateTemp(dir, prefix) if err != nil { return "", err } @@ -413,6 +413,9 @@ func TestDaemonClose(t *testing.T) { conn := <-lprd.FinishedConnections() require.Equal(t, End, conn.Status) + err = os.Remove(conn.SaveName) + require.Nil(t, err) + // no new connection may be opened lprs = LprSend{} err = lprs.Init("127.0.0.1", name, port, "raw", "TestUser", time.Minute) @@ -457,7 +460,7 @@ func TestDaemonFileSize(t *testing.T) { con := <-lprd.FinishedConnections() require.Equal(t, End, con.Status) - out, err = ioutil.ReadFile(con.SaveName) + out, err = os.ReadFile(con.SaveName) require.Nil(t, err) err = os.Remove(con.SaveName) require.Nil(t, err) @@ -501,7 +504,7 @@ func TestDaemonFileSize(t *testing.T) { con = <-lprd.FinishedConnections() require.Equal(t, End, con.Status) - out, err = ioutil.ReadFile(con.SaveName) + out, err = os.ReadFile(con.SaveName) require.Nil(t, err) err = os.Remove(con.SaveName) require.Nil(t, err) diff --git a/lpr_status.go b/lpr_status.go index 9020671..3a3eeaf 100644 --- a/lpr_status.go +++ b/lpr_status.go @@ -1,15 +1,21 @@ package lprlib import ( + "errors" "fmt" "io" "net" "time" ) -// GetStatus Reads the Status from the printer -func GetStatus(hostname string, port uint16, queue string, long bool, timeout time.Duration) (string, error) { - +// GetStatus Reads the status of the given queue on the given host (and port). +// The timeout parameter specifies the maximum time to wait for the connection +// and for each read/write operation. If timeout is 0, a default of 2 seconds +// is used. The long parameter specifies whether to request a long listing +// (true) or a short listing (false). The ignoreForcefulClose parameter controls +// if the read-status should ignore forceful connection closures by the server (which +// sometimes happens). +func GetStatus(hostname string, port uint16, queue string, long bool, timeout time.Duration, ignoreForcefulClose bool) (string, error) { // Set default Port if port == 0 { port = 515 @@ -88,6 +94,20 @@ func GetStatus(hostname string, port uint16, queue string, long bool, timeout ti if err == io.EOF { break } else { + if ignoreForcefulClose { + opErr := &net.OpError{} + + if errors.As(err, &opErr) { + // Check for connection reset errors at the syscall level + // This works cross-platform: ECONNRESET on Unix-like systems, + // WSAECONNRESET on Windows + if isConnResetErr(opErr) { + logDebugf("Ignoring forceful connection closure by server") + break + } + } + } + return "", &LprError{"Error while reading status: " + err.Error()} } } diff --git a/lpr_status_other.go b/lpr_status_other.go new file mode 100644 index 0000000..391a8f9 --- /dev/null +++ b/lpr_status_other.go @@ -0,0 +1,14 @@ +//go:build !windows +// +build !windows + +package lprlib + +import ( + "errors" + "net" + "syscall" +) + +func isConnResetErr(err *net.OpError) bool { + return errors.Is(err.Err, syscall.ECONNRESET) +} diff --git a/lpr_status_test.go b/lpr_status_test.go index b1e0a35..9c2698d 100644 --- a/lpr_status_test.go +++ b/lpr_status_test.go @@ -2,6 +2,7 @@ package lprlib import ( "log" + "net" "testing" "time" @@ -34,19 +35,19 @@ func TestGetStatus(t *testing.T) { time.Sleep(1 * time.Second) - status, err := GetStatus("127.0.0.1", port, rawQueue, false, 2*time.Second) + status, err := GetStatus("127.0.0.1", port, rawQueue, false, 2*time.Second, false) require.Nil(t, err) require.NotEmpty(t, status) lprd.GetQueueState = getShortQueueState - status, err = GetStatus("127.0.0.1", port, rawQueue, false, 2*time.Second) + status, err = GetStatus("127.0.0.1", port, rawQueue, false, 2*time.Second, false) require.Nil(t, err) require.Equal(t, shortQueueState, status) lprd.GetQueueState = getLongQueueState - status, err = GetStatus("127.0.0.1", port, rawQueue, true, 2*time.Second) + status, err = GetStatus("127.0.0.1", port, rawQueue, true, 2*time.Second, false) require.Nil(t, err) require.Equal(t, longQueueState, status) @@ -63,3 +64,33 @@ func TestGetStatus(t *testing.T) { lprd.Close() } + +func TestGetStatus_ServerClosesImmediatelyAfterCommand(t *testing.T) { + listener, err := net.Listen("tcp", net.JoinHostPort("localhost", "0")) + require.NoError(t, err) + + defer listener.Close() + + go func() { + conn, err := listener.Accept() + require.NoError(t, err) + + defer conn.Close() + + // Read command byte + buf := make([]byte, 1024) + _, err = conn.Read(buf) + require.NoError(t, err) + + // Close connection immediately + err = conn.(*net.TCPConn).SetLinger(0) + require.NoError(t, err) + }() + + status, err := GetStatus("localhost", uint16(listener.Addr().(*net.TCPAddr).Port), "raw", false, 2*time.Second, true) + require.NoError(t, err) + require.Empty(t, status) + + _, err = GetStatus("localhost", uint16(listener.Addr().(*net.TCPAddr).Port), "raw", false, 2*time.Second, false) + require.Error(t, err) +} diff --git a/lpr_status_windows.go b/lpr_status_windows.go new file mode 100644 index 0000000..40b5b79 --- /dev/null +++ b/lpr_status_windows.go @@ -0,0 +1,11 @@ +package lprlib + +import ( + "errors" + "net" + "syscall" +) + +func isConnResetErr(err *net.OpError) bool { + return errors.Is(err.Err, syscall.ECONNRESET) || errors.Is(err.Err, syscall.WSAECONNRESET) +}