| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- package dualrun
- import (
- "bytes"
- "context"
- "embed"
- "encoding/json"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "time"
- )
- //go:embed oracle_probe.php
- var oracleAssets embed.FS
- // OracleRunner 负责"起一个 PHP 进程、真跑一次结课、把观测值 JSON 带回来"。
- //
- // 为什么用子进程而不是同进程调用:
- // - legacy 是 PHP 应用,无法在 Go 进程内调用;
- // - **每次进程 = 每次新连接**,天然满足"每个 case 新连接"这条硬约束
- // (Q5:MEMORY 引擎临时表的行不参与回滚,靠 ROLLBACK 清不掉);
- // - 进程隔离后,PHP 侧的 notice/异常不会污染 Go 侧状态。
- type OracleRunner struct {
- cfg Config
- // probePath 是内嵌探针释放到磁盘后的路径(每轮 runner 一个,避免并发写)。
- probePath string
- }
- // NewOracleRunner 把内嵌的 PHP 探针释放到临时目录。
- func NewOracleRunner(cfg Config) (*OracleRunner, error) {
- dir, err := os.MkdirTemp("", "ac15-oracle-*")
- if err != nil {
- return nil, err
- }
- src, err := oracleAssets.ReadFile("oracle_probe.php")
- if err != nil {
- return nil, err
- }
- path := filepath.Join(dir, "oracle_probe.php")
- if err := os.WriteFile(path, src, 0o600); err != nil {
- return nil, err
- }
- return &OracleRunner{cfg: cfg, probePath: path}, nil
- }
- // Close 清理释放的探针。
- func (o *OracleRunner) Close() {
- if o.probePath != "" {
- _ = os.RemoveAll(filepath.Dir(o.probePath))
- }
- }
- // Run 执行一条 case 的 oracle 侧观测。
- //
- // 入参:ctx;c 待跑用例;frozenAt 冻结点(会被截断到整秒)。
- // 出参:OracleResult(含路径证据与"异常前写入值");error 仅表示"进程/解析层面失败"。
- func (o *OracleRunner) Run(ctx context.Context, c Case, frozenAt time.Time) (*OracleResult, error) {
- mut, err := json.Marshal(c.Mutations)
- if err != nil {
- return nil, err
- }
- cmd := exec.CommandContext(ctx, o.cfg.PHPBinary,
- "-d", "date.timezone=Asia/Shanghai",
- "-d", "opcache.enable_cli=0",
- "-d", "memory_limit=1024M",
- o.probePath,
- )
- cmd.Env = append(os.Environ(),
- "AC15_LEGACY_REPO="+o.cfg.LegacyRepo,
- "AC15_DB_HOST="+o.cfg.Host,
- "AC15_DB_PORT="+strconv.Itoa(o.cfg.Port),
- "AC15_DB_NAME="+o.cfg.LegacyDBName,
- "AC15_DB_USER="+o.cfg.User,
- "AC15_DB_PASS="+o.cfg.Pass,
- "AC15_RECORD_ID="+c.RecordID,
- "AC15_IS_SUPPLEMENT="+boolEnv(c.IsSupplement),
- "AC15_FROZEN_EPOCH="+strconv.FormatInt(TruncateToSecond(frozenAt).Unix(), 10),
- "AC15_MUTATIONS="+string(mut),
- )
- var stdout, stderr bytes.Buffer
- cmd.Stdout = &stdout
- cmd.Stderr = &stderr
- if err := cmd.Run(); err != nil {
- return nil, fmt.Errorf("php 进程失败:%v;stderr=%s", err, truncate(stderr.String(), 800))
- }
- raw := bytes.TrimSpace(stdout.Bytes())
- // 探针可能被 PHP 的 notice 污染 stdout,取最后一个 '{' 起始的 JSON 对象
- if i := bytes.LastIndexByte(raw, '{'); i > 0 {
- raw = raw[i:]
- }
- var res OracleResult
- if err := json.Unmarshal(raw, &res); err != nil {
- return nil, fmt.Errorf("解析探针输出失败:%v;原始输出=%s", err, truncate(string(raw), 800))
- }
- if !res.OK {
- return &res, fmt.Errorf("探针报错 %s: %s", res.Error, res.ErrorDetail)
- }
- return &res, nil
- }
- func boolEnv(b bool) string {
- if b {
- return "1"
- }
- return "0"
- }
- func truncate(s string, n int) string {
- if len(s) <= n {
- return s
- }
- return s[:n] + "…"
- }
|