From 72ef20a4e7642d87fc90c5ecbfa0fb9cd13ad9bc Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 15:23:33 +0200 Subject: [PATCH] Follow a conversion with convert {ask}, each call under 25 s, and leave the SDK's call timeout alone (issue 1600) --- modules/brother/cmd/brother/convert.go | 30 ++++++++++- modules/brother/cmd/brother/convert_test.go | 55 ++++++++++++++++++++- modules/brother/cmd/brother/deviceproc.go | 4 +- modules/brother/cmd/brother/printer.go | 3 -- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/modules/brother/cmd/brother/convert.go b/modules/brother/cmd/brother/convert.go index ba7e1f21..0cf0d4e5 100644 --- a/modules/brother/cmd/brother/convert.go +++ b/modules/brother/cmd/brother/convert.go @@ -16,11 +16,15 @@ import ( "io" "net/http" "strings" + "time" "unicode/utf8" "git.novox.be/novox/mesh-sdk/go/objectlink" ) +// ConvertFollow is how long the device process follows one conversion, in calls of under 25 s each. +const ConvertFollow = 3 * time.Minute + // ConvertTypes are the types document-converter takes (clause 7). var ConvertTypes = []string{"docx", "doc", "xlsx", "xls", "pptx", "ppt", "odt", "ods", "odp", "rtf", "txt", "html"} @@ -157,12 +161,34 @@ func (w *deviceWorker) toPDF(ctx context.Context, objects Objects, key string, d if err != nil { return nil, "", fmt.Errorf("the document (%s) could not be converted: %w", typ, err) } + // The converter answers within 25 s, the tool runner cutting every call at 30 s; a conversion not done by then is + // answered as converting with its ask id, and is read again with convert {ask}, each call under 25 s, for as long + // as the device process follows a conversion. + follow := w.ConvertFollow + if follow <= 0 { + follow = ConvertFollow + } + until := time.Now().Add(follow) var ans struct { SHA256 string `json:"sha256"` Error string `json:"error"` + State string `json:"state"` + Ask string `json:"ask"` } - if err := json.Unmarshal(raw, &ans); err != nil { - return nil, "", fmt.Errorf("the converter's answer is not JSON: %w", err) + for { + ans.State, ans.Ask, ans.SHA256, ans.Error = "", "", "", "" + if err := json.Unmarshal(raw, &ans); err != nil { + return nil, "", fmt.Errorf("the converter's answer is not JSON: %w", err) + } + if ans.State != "converting" { + break + } + if ans.Ask == "" || !time.Now().Before(until) { + return nil, "", fmt.Errorf("the document (%s) was still converting after %s (ask %s): nothing is printed", typ, follow, ans.Ask) + } + if raw, err = w.Convert(map[string]any{"ask": ans.Ask}); err != nil { + return nil, "", fmt.Errorf("the document (%s) could not be converted: %w", typ, err) + } } if ans.Error != "" { return nil, "", fmt.Errorf("the document (%s) could not be converted: %s", typ, ans.Error) diff --git a/modules/brother/cmd/brother/convert_test.go b/modules/brother/cmd/brother/convert_test.go index 443264ae..63c3fd94 100644 --- a/modules/brother/cmd/brother/convert_test.go +++ b/modules/brother/cmd/brother/convert_test.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "net/url" + "os" "strconv" "strings" "testing" @@ -47,8 +48,10 @@ func withConverter(r *printerRig, pdf []byte) *converterFake { c := &converterFake{r: r, pdf: pdf} r.worker.Convert = func(body map[string]any) (json.RawMessage, error) { c.calls = append(c.calls, body) - target, _ := url.Parse(body["target"].(string)) - r.objects.Put(context.Background(), strings.TrimPrefix(target.Path, "/"), "application/pdf", c.pdf) + if body["target"] != nil { // a start puts the PDF; a read of an ask (convert {ask}) puts nothing + target, _ := url.Parse(body["target"].(string)) + r.objects.Put(context.Background(), strings.TrimPrefix(target.Path, "/"), "application/pdf", c.pdf) + } if c.answer != nil { return c.answer(body, c.pdf) } @@ -215,3 +218,51 @@ func TestTheDeviceProcessInvokesTheConverter(t *testing.T) { } t.Fatal("no device process") } + +// 1600 (review round 1): a conversion answered as converting is followed with convert {ask}, each call its own, until +// it is done; one still converting after the device process's bound is not printed, and the process.CallTimeout of the +// SDK is not touched. +func TestAConversionAnsweredAsConvertingIsFollowedWithItsAsk(t *testing.T) { + fake := newIPPFake(t, 61, 5) + r := newRig(t, `{"printer":"192.0.2.10"}`, "pw") + doc := docx(t) + key := sha256Hex(doc) + r.objects.Put(t.Context(), key, "application/octet-stream", doc) + pdf := pdfOf(700) + conv := withConverter(r, pdf) + rounds := 0 + conv.answer = func(body map[string]any, stored []byte) (json.RawMessage, error) { + if body["ask"] != nil { + rounds++ + if rounds < 3 { + return json.RawMessage(`{"state": "converting", "ask": "` + body["ask"].(string) + `"}`), nil + } + return json.Marshal(map[string]any{"size": len(stored), "sha256": sha256Hex(stored)}) + } + return json.RawMessage(`{"state": "converting", "ask": "a1b2c3a1b2c3a1b2c3a1b2c3"}`), nil + } + if _, err := r.call(t, "printer.print", map[string]any{"object": key}); err != nil { + t.Fatal(err) + } + if len(conv.calls) != 4 || conv.calls[1]["ask"] != "a1b2c3a1b2c3a1b2c3a1b2c3" || conv.calls[1]["source"] != nil { + t.Errorf("calls = %v", conv.calls) + } + if _, sent := fake.request(t); string(sent) != string(pdf) { + t.Error("the converted PDF was not printed") + } + + // Always converting: given up at the bound, nothing printed. + fake.body = nil + r.worker.ConvertFollow = 30 * time.Millisecond + conv.answer = func(body map[string]any, _ []byte) (json.RawMessage, error) { + time.Sleep(5 * time.Millisecond) + return json.RawMessage(`{"state": "converting", "ask": "a1b2c3a1b2c3a1b2c3a1b2c3"}`), nil + } + if _, err := r.call(t, "printer.print", map[string]any{"object": key}); err == nil || !strings.Contains(err.Error(), "still converting") || fake.body != nil { + t.Errorf("error = %v, printed %v", err, fake.body != nil) + } + src, _ := os.ReadFile("deviceproc.go") + if strings.Contains(string(src), "CallTimeout") { + t.Error("deviceproc.go sets the SDK's call timeout") + } +} diff --git a/modules/brother/cmd/brother/deviceproc.go b/modules/brother/cmd/brother/deviceproc.go index dc6f8e67..6411cfae 100644 --- a/modules/brother/cmd/brother/deviceproc.go +++ b/modules/brother/cmd/brother/deviceproc.go @@ -71,6 +71,8 @@ type deviceWorker struct { Capture func(ctx context.Context, req ScanRequest) ([]Capture, error) // Convert calls the seat document-converter's convert verb as this process's own account (clause 7); nil: none. Convert func(body map[string]any) (json.RawMessage, error) + // ConvertFollow is how long a conversion is followed; zero is ConvertFollow (a test makes it shorter). + ConvertFollow time.Duration // Fetcher is the guarded fetch of a web link (fetch.go); it reads its facts when it is first needed. Fetcher func() (*Fetcher, error) // answerPart is the raw size of a document part in an answer; zero is AnswerPartSize (a test makes it larger). @@ -612,8 +614,6 @@ func runDevice(ctx context.Context, account process.Account, w *deviceWorker) er } defer conn.Close() w.Answers = conn.State(AnswersState) - // A conversion takes longer than the default call bound (30 s); the tool side waits ConvertWait for the converter. - process.CallTimeout = ConvertCallWait w.Convert = func(body map[string]any) (json.RawMessage, error) { return conn.Call("seat:document-converter.convert", body) } diff --git a/modules/brother/cmd/brother/printer.go b/modules/brother/cmd/brother/printer.go index 7192aab8..d2b33511 100644 --- a/modules/brother/cmd/brother/printer.go +++ b/modules/brother/cmd/brother/printer.go @@ -36,9 +36,6 @@ const ( // DeviceProcessID is the device process's id as module.json's resource "device" names it: the controller // delivers its bus account as the own secret broker-device. DeviceProcessID = "device" - // ConvertCallWait is how long the device process waits for document-converter.convert (the tool side of - // only-office waits two minutes for the converter). - ConvertCallWait = 130 * time.Second // PrintWait is how long printer.print waits for the device process: a fetch and a conversion come before the job. PrintWait = 4 * time.Minute // AnswerWait is how long a verb waits for the device process; ScanWait how long a scan does.