mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-14 20:48:17 +08:00
182 lines
6.0 KiB
Go
182 lines
6.0 KiB
Go
package cli
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"time"
|
||
|
||
"github.com/matthewlu070111/smart-srun/core/internal/application"
|
||
"github.com/matthewlu070111/smart-srun/core/internal/control"
|
||
"github.com/matthewlu070111/smart-srun/core/internal/daemon"
|
||
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
||
)
|
||
|
||
func newKey() string { return "cli-" + rand.Text() }
|
||
|
||
func commandFlags() *flag.FlagSet {
|
||
f := flag.NewFlagSet("srunnet", flag.ContinueOnError)
|
||
f.SetOutput(io.Discard) // Never echo an unknown argument that might be a secret.
|
||
return f
|
||
}
|
||
|
||
func onlineDetect(ctx context.Context, client onlineClient, args []string, stdin io.Reader, stdout, stderr *os.File) int {
|
||
usage := func() int {
|
||
return onlineError(stderr, domain.Errorf(domain.CodeInvalidArgument,
|
||
"用法:detect env|acid|operators|identity --access-mode wired|wifi --iface 接口 [--base-url URL --ac-id ID --ssid SSID --user-id 账号 --json --no-wait];detect operator|verify --stdin [--json --no-wait];detect status|cancel 任务ID"))
|
||
}
|
||
if len(args) == 0 {
|
||
return usage()
|
||
}
|
||
if args[0] == "wifi" {
|
||
return onlineWifi(ctx, client, args[1:], stdin, stdout, stderr)
|
||
}
|
||
if args[0] == "status" || args[0] == "cancel" {
|
||
if len(args) != 2 {
|
||
return usage()
|
||
}
|
||
method := "action.get"
|
||
if args[0] == "cancel" {
|
||
method = "action.cancel"
|
||
}
|
||
return printCall(ctx, client, method, daemon.ActionParams{ActionID: args[1]}, stdout, stderr)
|
||
}
|
||
methods := map[string]string{"env": "detect.environment", "acid": "detect.acid", "operators": "detect.operators", "identity": "detect.identity", "operator": "detect.verify", "verify": "detect.verify"}
|
||
method := methods[args[0]]
|
||
if method == "" {
|
||
return usage()
|
||
}
|
||
f := commandFlags()
|
||
asJSON, noWait, input, readOnly := f.Bool("json", false, ""), f.Bool("no-wait", false, ""), f.Bool("stdin", false, ""), f.Bool("read-only", false, "")
|
||
var p daemon.VerificationParams
|
||
f.StringVar(&p.BaseURL, "base-url", "", "")
|
||
f.StringVar(&p.AccessMode, "access-mode", "", "")
|
||
f.StringVar(&p.Iface, "iface", "", "")
|
||
f.StringVar(&p.SSID, "ssid", "", "")
|
||
f.StringVar(&p.ACID, "ac-id", "", "")
|
||
f.StringVar(&p.UserID, "user-id", "", "")
|
||
f.StringVar(&p.School, "school", "", "")
|
||
if f.Parse(args[1:]) != nil || f.NArg() != 0 {
|
||
return usage()
|
||
}
|
||
if *readOnly {
|
||
if args[0] != "operator" {
|
||
return usage()
|
||
}
|
||
method = "detect.identity"
|
||
}
|
||
if *input {
|
||
mixed := false
|
||
f.Visit(func(v *flag.Flag) {
|
||
if v.Name != "json" && v.Name != "no-wait" && v.Name != "stdin" && v.Name != "read-only" {
|
||
mixed = true
|
||
}
|
||
})
|
||
if mixed {
|
||
return usage()
|
||
}
|
||
if file, ok := stdin.(*os.File); ok {
|
||
if info, err := file.Stat(); err == nil && info.Mode()&os.ModeCharDevice != 0 {
|
||
return onlineError(stderr, domain.Errorf(domain.CodeInvalidArgument, "--stdin 需要管道或 JSON 文件输入"))
|
||
}
|
||
}
|
||
data, err := io.ReadAll(io.LimitReader(stdin, (16<<10)+1))
|
||
if err != nil || len(data) > 16<<10 {
|
||
return onlineError(stderr, domain.Errorf(domain.CodeInvalidArgument, "探测 JSON 输入过长或无法读取"))
|
||
}
|
||
if method == "detect.identity" || method == "detect.verify" {
|
||
err = control.DecodeParams(data, &p)
|
||
} else {
|
||
err = control.DecodeParams(data, &p.DetectACIDParams)
|
||
}
|
||
if err != nil {
|
||
return onlineError(stderr, err)
|
||
}
|
||
} else if method == "detect.verify" {
|
||
return usage()
|
||
}
|
||
if p.Session != "" {
|
||
return onlineError(stderr, domain.Errorf(domain.CodeInvalidArgument, "CLI 不接受 Web 会话参数"))
|
||
}
|
||
if p.Iface == "" || (p.AccessMode != "wired" && p.AccessMode != "wifi") || (p.AccessMode == "wifi" && p.SSID == "") || (method != "detect.environment" && p.BaseURL == "") {
|
||
return usage()
|
||
}
|
||
if p.IdempotencyKey == "" {
|
||
p.IdempotencyKey = newKey()
|
||
}
|
||
var params any = p.DetectACIDParams
|
||
if method == "detect.identity" || method == "detect.verify" {
|
||
params = p
|
||
} else if p.UserID != "" {
|
||
return usage()
|
||
}
|
||
return runTask(ctx, client, method, params, *asJSON, *noWait, 225*time.Second, stdout, stderr)
|
||
}
|
||
|
||
// runTask keeps submission separate from success. A completed discovery can
|
||
// still have found no answer; scripts receive that result and a nonzero exit.
|
||
func runTask(ctx context.Context, client onlineClient, method string, params any, asJSON, noWait bool, budget time.Duration, stdout, stderr *os.File) int {
|
||
ctx, stop := context.WithTimeout(ctx, budget)
|
||
defer stop()
|
||
if err := client.ensure(ctx); err != nil {
|
||
return onlineError(stderr, err)
|
||
}
|
||
raw, err := client.call(ctx, method, params)
|
||
if err != nil {
|
||
return onlineError(stderr, err)
|
||
}
|
||
var receipt daemon.SubmitResult
|
||
if err = json.Unmarshal(raw, &receipt); err != nil || receipt.ActionID == "" {
|
||
return onlineError(stderr, domain.Errorf(domain.CodeProtocolInvalid, "服务未返回有效的动作编号"))
|
||
}
|
||
if noWait {
|
||
if asJSON {
|
||
return writeJSON(stdout, stderr, receipt)
|
||
}
|
||
fmt.Fprintf(stdout, "任务 %s 已提交;运行 srunnet detect status %s 查询结果。\n", receipt.ActionID, receipt.ActionID)
|
||
return ExitOK
|
||
}
|
||
fmt.Fprintf(stderr, "任务 %s 已提交,等待执行结果。\n", receipt.ActionID)
|
||
view, err := waitTask(ctx, client, receipt.ActionID, stderr)
|
||
if err != nil {
|
||
return onlineError(stderr, err)
|
||
}
|
||
if asJSON {
|
||
if code := writeJSON(stdout, stderr, view); code != ExitOK {
|
||
return code
|
||
}
|
||
} else {
|
||
fmt.Fprintf(stdout, "%s:%s(%s)\n", view.ID, view.Message, view.State)
|
||
if len(view.Result) > 0 {
|
||
if code := writeJSON(stdout, stderr, view.Result); code != ExitOK {
|
||
return code
|
||
}
|
||
}
|
||
}
|
||
if application.State(view.State) != application.StateSucceeded {
|
||
if view.Code != "" {
|
||
return ExitCodeFor(domain.Errorf(view.Code, "任务未完成"))
|
||
}
|
||
if view.State == string(application.StateCancelled) {
|
||
return ExitCancelled
|
||
}
|
||
return ExitActionFailed
|
||
}
|
||
var finding struct {
|
||
OK *bool `json:"ok"`
|
||
}
|
||
if len(view.Result) > 0 {
|
||
if json.Unmarshal(view.Result, &finding) != nil {
|
||
return onlineError(stderr, domain.Errorf(domain.CodeProtocolInvalid, "探测结果格式无效"))
|
||
}
|
||
if finding.OK != nil && !*finding.OK {
|
||
return ExitActionFailed
|
||
}
|
||
}
|
||
return ExitOK
|
||
}
|