package external import ( "context" "encoding/json" "fmt" "github.com/vladimir/goherence/internal/module" "github.com/vladimir/goherence/internal/ssh" ) // Module — обёртка над внешним (сторонним) модулем: реализует тот же // интерфейс module.Module, что и builtin-модули, но вместо чистого Go-кода // доставляет и запускает на хосте subprocess, обмениваясь с ним JSON // по stdin/stdout. Executor не отличает Module от builtin — он получает // его из того же Registry, резолвя по имени. type Module struct { Manifest Manifest BinPath map[string]string // arch ("linux/amd64") → путь к локальному бинарнику } func (m *Module) Name() string { return m.Manifest.Name } func (m *Module) Run(ctx context.Context, in module.Input, conn *ssh.Conn) (module.Result, error) { if m.Manifest.SchemaVersion != module.SchemaVersion { return module.Result{}, fmt.Errorf( "модуль %s: schema_version %d не совпадает с ожидаемой %d — "+ "обнови модуль или goherence", m.Manifest.Name, m.Manifest.SchemaVersion, module.SchemaVersion) } remoteBin, err := ensureDeployed(conn, m) if err != nil { return module.Result{}, err } inputJSON, err := json.Marshal(in) if err != nil { return module.Result{}, fmt.Errorf("сериализация input для %s: %w", m.Manifest.Name, err) } res, err := conn.RunWithInput(remoteBin, inputJSON) if err != nil { return module.Result{}, fmt.Errorf("запуск внешнего модуля %s: %w", m.Manifest.Name, err) } if res.ExitCode != 0 { return module.Result{}, fmt.Errorf( "внешний модуль %s завершился с кодом %d, stderr: %s", m.Manifest.Name, res.ExitCode, res.Stderr) } var out module.Result if err := json.Unmarshal([]byte(res.Stdout), &out); err != nil { return module.Result{}, fmt.Errorf( "внешний модуль %s вернул невалидный JSON: %w (stdout: %s)", m.Manifest.Name, err, res.Stdout) } return out, nil }