Files

182 lines
6.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}