From ef4f2a47252fd805c1a87d16a0804bd11a43eac3 Mon Sep 17 00:00:00 2001 From: Gerhard Gruber Date: Mon, 24 Nov 2025 17:02:02 +0100 Subject: [PATCH 1/8] Added flag to support a connection reset while reading queue status Some servers close the connection forcefully when asking for a queue status --- README.md | 2 +- lpr_status.go | 24 +++++++++++++++++++++--- lpr_status_test.go | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 7 deletions(-) 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/lpr_status.go b/lpr_status.go index 9020671..d8f9a27 100644 --- a/lpr_status.go +++ b/lpr_status.go @@ -1,15 +1,22 @@ package lprlib import ( + "errors" "fmt" "io" "net" + "strings" "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 +95,17 @@ 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) { + if strings.Contains(opErr.Error(), "connection was forcibly closed by the remote host") || strings.Contains(opErr.Error(), "connection reset by peer") { + logDebugf("Ignoring forceful connection closure by server") + break + } + } + } + return "", &LprError{"Error while reading status: " + err.Error()} } } diff --git a/lpr_status_test.go b/lpr_status_test.go index b1e0a35..472288c 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_ServerClosesImmediatlyAfterCommand(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) +} From 75accd0cae770a8f697625a4328a9ccf81221960 Mon Sep 17 00:00:00 2001 From: Gerhard Gruber Date: Mon, 24 Nov 2025 17:22:50 +0100 Subject: [PATCH 2/8] Updated dependencies --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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= From 7a7ecff7207331a4414820c1baa08956c61ed2ff Mon Sep 17 00:00:00 2001 From: Gerhard Gruber Date: Tue, 25 Nov 2025 12:31:09 +0100 Subject: [PATCH 3/8] Update lpr_status_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lpr_status_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lpr_status_test.go b/lpr_status_test.go index 472288c..9c2698d 100644 --- a/lpr_status_test.go +++ b/lpr_status_test.go @@ -65,7 +65,7 @@ func TestGetStatus(t *testing.T) { lprd.Close() } -func TestGetStatus_ServerClosesImmediatlyAfterCommand(t *testing.T) { +func TestGetStatus_ServerClosesImmediatelyAfterCommand(t *testing.T) { listener, err := net.Listen("tcp", net.JoinHostPort("localhost", "0")) require.NoError(t, err) From 75b2697f27aa3dd528bb78228ba12b3dcacb2f84 Mon Sep 17 00:00:00 2001 From: Gerhard Gruber Date: Tue, 25 Nov 2025 12:33:38 +0100 Subject: [PATCH 4/8] update github workflow --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ./... From 785c4a854810942ea8ae8cab9bfe7d0d57393c5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:37:36 +0000 Subject: [PATCH 5/8] Replace string matching with syscall error type checking for connection resets Co-authored-by: gerhardgruber <1944892+gerhardgruber@users.noreply.github.com> --- .gitignore | 1 + lpr_status.go | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a1338d6..48231bb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ +*[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] diff --git a/lpr_status.go b/lpr_status.go index d8f9a27..ba793b6 100644 --- a/lpr_status.go +++ b/lpr_status.go @@ -5,7 +5,7 @@ import ( "fmt" "io" "net" - "strings" + "syscall" "time" ) @@ -99,7 +99,10 @@ func GetStatus(hostname string, port uint16, queue string, long bool, timeout ti opErr := &net.OpError{} if errors.As(err, &opErr) { - if strings.Contains(opErr.Error(), "connection was forcibly closed by the remote host") || strings.Contains(opErr.Error(), "connection reset by peer") { + // Check for connection reset errors at the syscall level + // This works cross-platform: ECONNRESET on Unix-like systems, + // WSAECONNRESET on Windows + if errors.Is(opErr, syscall.ECONNRESET) { logDebugf("Ignoring forceful connection closure by server") break } From 7a385e2ef8e260d8395cfc4f7e882089c7218995 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:38:48 +0000 Subject: [PATCH 6/8] Fix error checking to use opErr.Err instead of opErr Co-authored-by: gerhardgruber <1944892+gerhardgruber@users.noreply.github.com> --- lpr_status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lpr_status.go b/lpr_status.go index ba793b6..bd92711 100644 --- a/lpr_status.go +++ b/lpr_status.go @@ -102,7 +102,7 @@ func GetStatus(hostname string, port uint16, queue string, long bool, timeout ti // Check for connection reset errors at the syscall level // This works cross-platform: ECONNRESET on Unix-like systems, // WSAECONNRESET on Windows - if errors.Is(opErr, syscall.ECONNRESET) { + if errors.Is(opErr.Err, syscall.ECONNRESET) { logDebugf("Ignoring forceful connection closure by server") break } From b6bff466d5d07939f90c4fff3ba4ec9ef58a2b6e Mon Sep 17 00:00:00 2001 From: Gerhard Gruber Date: Tue, 25 Nov 2025 16:30:46 +0100 Subject: [PATCH 7/8] small improvements --- .gitignore | 1 - lpr_daemon.go | 26 ++++++++++++++------------ lpr_daemon_test.go | 13 ++++++++----- lpr_status.go | 3 +-- lpr_status_other.go | 14 ++++++++++++++ lpr_status_windows.go | 11 +++++++++++ 6 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 lpr_status_other.go create mode 100644 lpr_status_windows.go diff --git a/.gitignore b/.gitignore index 48231bb..a1338d6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,3 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ -*[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] diff --git a/lpr_daemon.go b/lpr_daemon.go index d0edc5a..7aabb89 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 @@ -376,16 +367,27 @@ func (lpr *LprConnection) RunConnection() { } defer traceFile.Close() 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) 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 bd92711..3a3eeaf 100644 --- a/lpr_status.go +++ b/lpr_status.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net" - "syscall" "time" ) @@ -102,7 +101,7 @@ func GetStatus(hostname string, port uint16, queue string, long bool, timeout ti // Check for connection reset errors at the syscall level // This works cross-platform: ECONNRESET on Unix-like systems, // WSAECONNRESET on Windows - if errors.Is(opErr.Err, syscall.ECONNRESET) { + if isConnResetErr(opErr) { logDebugf("Ignoring forceful connection closure by server") break } 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_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) +} From a95026fbc7e14ce2ac9b0e27dcf843fde1523174 Mon Sep 17 00:00:00 2001 From: Gerhard Gruber Date: Tue, 25 Nov 2025 16:38:10 +0100 Subject: [PATCH 8/8] added more error checking --- lpr_daemon.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/lpr_daemon.go b/lpr_daemon.go index 7aabb89..bbbfa9f 100644 --- a/lpr_daemon.go +++ b/lpr_daemon.go @@ -365,7 +365,14 @@ 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()) _, err = fmt.Fprintf(traceFile, "LPR connection trace %s\n", time.Now()) @@ -389,9 +396,20 @@ func (lpr *LprConnection) RunConnection() { 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) + } } }