108 lines
3.3 KiB
Go
108 lines
3.3 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package ocr
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// bounded_buffer never retains more than limit bytes. Child-output overflow
|
|
// cancels its process immediately instead of leaving a stopped pipe reader.
|
|
type bounded_buffer struct {
|
|
data bytes.Buffer
|
|
limit int
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
overflow bool
|
|
}
|
|
|
|
func (buffer *bounded_buffer) Write(data []byte) (int, error) {
|
|
if err := buffer.ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(data) > buffer.limit-buffer.Len() {
|
|
buffer.overflow = true
|
|
if buffer.cancel != nil {
|
|
buffer.cancel()
|
|
}
|
|
return 0, ErrLimit
|
|
}
|
|
return buffer.data.Write(data)
|
|
}
|
|
|
|
func (buffer *bounded_buffer) Bytes() []byte { return buffer.data.Bytes() }
|
|
func (buffer *bounded_buffer) Len() int { return buffer.data.Len() }
|
|
|
|
func (engine *Engine) run(parent context.Context, input []byte, limit int, args ...string) ([]byte, []byte, error) {
|
|
return run_process(parent, engine.command, input, limit, args...)
|
|
}
|
|
|
|
func run_process(parent context.Context, factory func(context.Context, ...string) *exec.Cmd, input []byte, limit int, args ...string) ([]byte, []byte, error) {
|
|
ctx, cancel := context.WithCancel(parent)
|
|
defer cancel()
|
|
command := factory(ctx, args...)
|
|
// This reader targets Linux/Xorg. Own the whole process group, including
|
|
// descendants that might inherit pipes from a wrapper executable.
|
|
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
kill := func() error {
|
|
if command.Process == nil {
|
|
return os.ErrProcessDone
|
|
}
|
|
err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL)
|
|
if errors.Is(err, syscall.ESRCH) {
|
|
return os.ErrProcessDone
|
|
}
|
|
return err
|
|
}
|
|
command.Cancel = kill
|
|
command.WaitDelay = time.Second
|
|
command.Env = process_environment(command.Env)
|
|
command.Stdin = bytes.NewReader(input)
|
|
output := &bounded_buffer{limit: limit, ctx: ctx, cancel: cancel}
|
|
diagnostics := &bounded_buffer{limit: maxStderrBytes, ctx: ctx, cancel: cancel}
|
|
command.Stdout, command.Stderr = output, diagnostics
|
|
err := command.Run()
|
|
// WaitDelay closes inherited pipes after a bound, but os/exec only kills the
|
|
// direct child then. Retire any remaining group before admitting another job.
|
|
if command.Process != nil {
|
|
_ = kill()
|
|
}
|
|
if parent.Err() != nil {
|
|
return nil, nil, parent.Err()
|
|
}
|
|
if output.overflow || diagnostics.overflow {
|
|
return nil, nil, ErrLimit
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, exec.ErrNotFound) || errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
|
return nil, nil, ErrUnavailable
|
|
}
|
|
// Never forward native stderr: it can contain captured text or paths.
|
|
return nil, diagnostics.Bytes(), ErrRecognition
|
|
}
|
|
return output.Bytes(), diagnostics.Bytes(), nil
|
|
}
|
|
|
|
func process_environment(environment []string) []string {
|
|
if environment == nil {
|
|
environment = os.Environ()
|
|
}
|
|
result := make([]string, 0, len(environment)+2)
|
|
for _, entry := range environment {
|
|
if !strings.HasPrefix(entry, "LC_ALL=") && !strings.HasPrefix(entry, "OMP_THREAD_LIMIT=") {
|
|
result = append(result, entry)
|
|
}
|
|
}
|
|
// Stable CLI diagnostics; avoid using every core for a desktop OCR request.
|
|
return append(result, "LC_ALL=C", "OMP_THREAD_LIMIT=1")
|
|
}
|