任务创建、执行与结果获取



如何使用

简介:

本示例展示如何通过 Tasks 接口,完整管理一次自动化任务的生命周期。

脚本依次完成任务创建(tasksCreate)、任务入库确认(tasksGet),并通过轮询方式持续查询任务状态,直到任务执行结束(RUNNING → STOPPED_*),最终输出任务执行结果与关键信息。

示例内置标准化的步骤日志(开始 / 结束 / 耗时统计),并在失败场景下自动采集前台应用、Activity、电量、设备信息及截图留证,便于问题定位与结果审计。

适用于 自动化测试调度、批量任务执行、平台集成与执行结果监控 等场景,可直接复制运行。

运行前:

  1. 安装 Total Control(专业版)
    • 下载并安装 Total Control 11.0 (Update 20) 官方版本
    • 需要使用专业版(Pro),如果当前是精简版,有一次 3 天的专业版试用机会
  2. 设备准备
    • 至少连接一台 Android 设备
    • 在 Total Control UI 中勾选该设备
    • getDevices() 只会返回已选设备
  3. 文件准备:必须提前准备以下文件:任务脚本文件(必须存在)
    • 路径示例:E:/file/test.js
    • 内容示例:
      sleep(10000);
      print('-tasks-test-');
  4. 将示例代码保存为 JS 文件,例如:D:/scripts/task_create_submit_poll.js
  5. 确认被执行脚本存在,在且路径正确:E:/file/test.js
  6. 打开脚本终端:主面板 → 脚本 → 脚本终端
  7. 执行:
    sigmaLoad("D:/scripts/task_create_submit_poll.js");
    

注意:

  • asksCreate() 会校验脚本路径必须存在,否则 "Script path not found"
  • tasks 的“执行器”不一定马上运行;如果 planTime 在未来会保持 SCHEDULED(-6)
  • 本示例用轮询 tasksGet 获取最终 status(-2/-3/-5 等)


源代码

/**
 * ============================================================
 * [TASK] Create -> Submit -> Get results 
 * ============================================================
 * 目标:
 *  1) 创建任务 tasksCreate(...)
 *  2) 确认任务已入库 tasksGet(...)
 *  3) 轮询 tasksGet(...) 等待任务结束(RUNNING -> STOPPED_*)
 *  4) 打印结果(status / errorMessage / execDevice / sigmaTestStatus 等)
 *
 * 输出要求:
 *  - 每一步打印「开始/结束/耗时」
 *  - 失败:写上下文(前台包名+activity+电量+设备信息)+ 截图 + 写日志文件(若 fs 可用)
 *
 * 关键注意:
 *  - tasksCreate() 会校验脚本路径必须存在,否则 "Script path not found"
 *  - tasks 的“执行器”不一定马上运行;如果 planTime 在未来会保持 SCHEDULED(-6)
 *  - 本示例用轮询 tasksGet 获取最终 status(-2/-3/-5 等)
 */

// ===================== 引入与注入 =====================
// var tcConst = global.tcConst || {};
// var sigmaConst = global.sigmaConst || {};

var { getDevices } = require("sigma/device");
require("sigma/app");
require("sigma/input");
require("sigma/image");

// tasks 模块
var tasks = require("sigma/tasks");

// ===================== 配置区 =====================
var LOG_DIR = "D:/tc_logs";
var IMG_TYPE = (sigmaConst.IMG_JPG || tcConst.IMG_JPG || sigmaConst.IMG_PNG || tcConst.IMG_PNG);

// 任务名必须唯一(建议带时间戳)
function pad2(n) { return (n < 10 ? "0" : "") + n; }
function ts() {
  var d = new Date();
  return (
    d.getFullYear() +
    pad2(d.getMonth() + 1) +
    pad2(d.getDate()) + "_" +
    pad2(d.getHours()) +
    pad2(d.getMinutes()) +
    pad2(d.getSeconds())
  );
}

var TASK_NAME = "demo_task_" + ts();

// 脚本必须存在、且扩展名必须 .js/.tst/.scp
// 例:var SCRIPT_PATH = "E:/file/test.js";
var SCRIPT_PATH = "E:/file/test.js";
// 执行次数(iteration/iterCount)>=1
var ITERATION = 1;

// 执行时间策略:
// 1) 立刻创建(now):time = undefined, repeat = undefined
// 2) 指定一次性时间:time = "YYYY-MM-DD HH:mm[:ss]"
// 3) 周期:time="HH:mm[:ss]" + repeat=[0..6]
var TIME_MODE = "now"; // "now" | "oneshot" | "weekly"

// oneshot 示例(未来时间)
// var ONE_SHOT_TIME = "2026-01-14 23:50";

// weekly 示例:每周一三五 08:30
// var WEEKLY_TIME = "08:30";
// var WEEKLY_REPEAT = [1,3,5];

// 轮询等待最大时长(毫秒)
var WAIT_TIMEOUT_MS = 3 * 60 * 1000; // 3 分钟
var POLL_INTERVAL_MS = 1500;

// ===================== 小工具 =====================
function nowMs() { return Date.now(); }
function sleep(ms) {
  if (typeof global.sleep === "function") return global.sleep(ms);
  var t = Date.now();
  while (Date.now() - t < ms) {}
}
function safeFilePart(s) { return String(s).replace(/[^\w\-.]+/g, "_"); }
function devName(d) { return d.name || (d.getName && d.getName()) || d.SN || "UnknownDevice"; }
function safeGet(fn, fallback) { try { return fn(); } catch (e) { return fallback; } }

// fs(有则写日志/锁)
var fs = null;
try { fs = require("fs"); } catch (e) { fs = null; }

function ensureDir(path) {
  if (!fs) return false;
  try { if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive: true }); return true; }
  catch (e) { return false; }
}
function appendFile(path, text) {
  if (!fs) return false;
  try { fs.appendFileSync(path, text, { encoding: "utf8" }); return true; }
  catch (e) { return false; }
}

// ===================== 日志 ctx =====================
function makeCtx() {
  ensureDir(LOG_DIR);
  var logFile = LOG_DIR + "/TASK__" + safeFilePart(TASK_NAME) + "__" + ts() + ".log";
  var canWrite = ensureDir(LOG_DIR) && !!fs;
  return {
    logFile: logFile,
    canWrite: canWrite,
    log: function (line) {
      var msg = "[TASK] " + line;
      print(msg);
      if (this.canWrite) appendFile(this.logFile, msg + "\n");
    }
  };
}

// ===================== stepRun:开始/结束/耗时 =====================
function stepRun(ctx, stepName, fn) {
  var start = nowMs();
  ctx.log(">> START  " + stepName);
  try {
    var ret = fn();
    ctx.log("<< END    " + stepName + "  (cost " + (nowMs() - start) + " ms)");
    return { ok: true, ret: ret };
  } catch (e) {
    ctx.log("<< FAIL   " + stepName + "  (cost " + (nowMs() - start) + " ms)  err=" + String(e && e.message ? e.message : e));
    return { ok: false, err: String(e && e.message ? e.message : e) };
  }
}

// ===================== 失败上下文 + 截图 =====================
function writeFailContext(ctx, d, stepName, errText) {
  ctx.log("---- FAIL CONTEXT (" + devName(d) + ") BEGIN ----");
  ctx.log("step=" + stepName);
  ctx.log("error=" + (errText || ""));
  ctx.log("foregroundApp=" + (safeGet(function () { return d.getForegroundApp(); }, "") || ""));
  ctx.log("activity=" + (safeGet(function () { return d.getActivity(); }, "") || ""));
  ctx.log("battery=" + (safeGet(function () { return d.battery; }, "") || ""));
  ctx.log("manufacturer=" + safeGet(function () { return d.manufacturer; }, ""));
  ctx.log("model=" + safeGet(function () { return d.model; }, ""));
  ctx.log("SN=" + safeGet(function () { return d.SN; }, ""));
  ctx.log("IP=" + safeGet(function () { return d.IP; }, ""));
  ctx.log("DPI=" + safeGet(function () { return d.DPI; }, ""));
  ctx.log("resolution=" + safeGet(function () { return d.width; }, "") + "x" + safeGet(function () { return d.height; }, ""));
  ctx.log("androidVersionRelease=" + safeGet(function () { return d.androidVersionRelease; }, ""));
  ctx.log("androidVersionSdkInt=" + safeGet(function () { return d.androidVersionSdkInt; }, ""));
  ctx.log("---- FAIL CONTEXT (" + devName(d) + ") END ----");
}

function screenshotOne(ctx, d, tag) {
  var file = LOG_DIR + "/" + safeFilePart(devName(d)) + "__" + safeFilePart(tag) + "__" + ts() + ".jpg";
  var ret = d.screenshot(file, IMG_TYPE);
  if (ret === 0) ctx.log("!! Screenshot saved: " + file);
  else ctx.log("!! Screenshot failed (" + devName(d) + "): " + lastError());
}

// ===================== task status helper =====================
function statusToText(code) {
  var S = tasks.STATUS || {};
  // 常见值(文档里)
  if (code === 0) return "FILE_NOT_FOUND(0)";
  if (code === S.RUNNING || code === -1) return "RUNNING(-1)";
  if (code === S.STOPPED_NORMAL || code === -2) return "STOPPED_NORMAL(-2)";
  if (code === S.STOPPED_ERROR || code === -3) return "STOPPED_ERROR(-3)";
  if (code === S.PAUSED || code === -4) return "PAUSED(-4)";
  if (code === S.USER_TERMINATED || code === -5) return "USER_TERMINATED(-5)";
  if (code === S.SCHEDULED || code === -6) return "SCHEDULED(-6)";
  return String(code);
}

function isTerminalStatus(code) {
  return code === -2 || code === -3 || code === -5 || code === 0;
}

// ===================== main =====================
function main() {
  var ctx = makeCtx();

  // 可选:拿到设备,主要用于失败时上下文/截图
  var devices = getDevices();
  var list = [];
  if (devices && devices.length) devices.forEach(function (d) { list.push(d); });
  ctx.log("Selected devices: " + list.length);

  // Step0:准备脚本路径存在性提示
  stepRun(ctx, "Step0 Check script path string", function () {
    ctx.log("TASK_NAME=" + TASK_NAME);
    ctx.log("SCRIPT_PATH=" + SCRIPT_PATH);
    ctx.log("ITERATION=" + ITERATION);
    ctx.log("TIME_MODE=" + TIME_MODE);
  });

  // Step1:Create task
  var s1 = stepRun(ctx, "Step1 tasksCreate()", function () {
    var ret;

    if (TIME_MODE === "now") {
      // 立即(time/repeat 省略)
      ret = tasks.tasksCreate(TASK_NAME, SCRIPT_PATH, ITERATION);
    } else if (TIME_MODE === "oneshot") {
      // 一次性定时(自行把 ONE_SHOT_TIME 配出来)
      ret = tasks.tasksCreate(TASK_NAME, SCRIPT_PATH, ITERATION, ONE_SHOT_TIME);
    } else if (TIME_MODE === "weekly") {
      // 周期(自行把 WEEKLY_TIME/WEEKLY_REPEAT 配出来)
      ret = tasks.tasksCreate(TASK_NAME, SCRIPT_PATH, ITERATION, WEEKLY_TIME, WEEKLY_REPEAT);
    } else {
      throw new Error("Unknown TIME_MODE: " + TIME_MODE);
    }

    if (ret !== true && typeof ret !== "number") {
      // tasksCreate 文档:成功 true;失败 false(配合 lastError)
      throw new Error("tasksCreate FAIL: " + lastError());
    }

    ctx.log("tasksCreate OK, ret=" + ret);
    return ret;
  });

  if (!s1.ok) {
    ctx.log("[CREATE] Task create FAIL: " + TASK_NAME);
    ctx.log("[CREATE] err=" + s1.err);

    // 失败时:对每台设备补一份上下文+截图
    for (var i = 0; i < list.length; i++) {
      writeFailContext(ctx, list[i], "tasksCreate", s1.err);
      screenshotOne(ctx, list[i], "FAIL__tasksCreate");
    }

    // 额外:打印最近任务帮助排查
    var s1b = stepRun(ctx, "Step1b tasksList(recent 5)", function () {
      var recent = tasks.tasksList({ limit: 5, orderBy: "updatedAt DESC" });
      ctx.log("recent tasks:\n" + JSON.stringify(recent, null, 2));
    });

    return;
  }

  // Step2:Get task record (submit/confirm)
  var taskRow = null;
  var s2 = stepRun(ctx, "Step2 tasksGet(taskName)", function () {
    var t = tasks.tasksGet(TASK_NAME);
    if (!t) throw new Error("tasksGet returned null (not found)");
    taskRow = t;
    // tasksGet 可能返回 object 或 [object](不同实现),这里统一一下
    if (Array.isArray(taskRow)) taskRow = taskRow[0];
    ctx.log("task snapshot:\n" + JSON.stringify(taskRow, null, 2));
    return taskRow;
  });
  if (!s2.ok) return;

  // Step3:Poll until terminal
  var s3 = stepRun(ctx, "Step3 Poll task until terminal or timeout", function () {
    var deadline = nowMs() + WAIT_TIMEOUT_MS;
    var lastStatus = null;

    while (nowMs() < deadline) {
      var t = tasks.tasksGet(TASK_NAME);
      if (Array.isArray(t)) t = t[0];
      if (!t) throw new Error("tasksGet null during polling");

      var st = t.status;
      if (st !== lastStatus) {
        ctx.log("poll: status=" + statusToText(st) + " startTime=" + t.startTime + " endTime=" + t.endTime);
        lastStatus = st;
      }

      if (isTerminalStatus(st)) {
        taskRow = t;
        return t;
      }

      sleep(POLL_INTERVAL_MS);
    }

    // 超时:可能仍是 SCHEDULED/RUNNING
    throw new Error("Timeout waiting task finish. lastStatus=" + statusToText(lastStatus));
  });

  // Step4:Print result summary
  stepRun(ctx, "Step4 Print result summary", function () {
    var t = taskRow;
    if (Array.isArray(t)) t = t[0];

    ctx.log("===== RESULT =====");
    ctx.log("taskName=" + t.taskName);
    ctx.log("taskID=" + t.taskID);
    ctx.log("status=" + statusToText(t.status));
    ctx.log("errorMessage=" + (t.errorMessage || ""));
    ctx.log("execDevice=" + (t.execDevice || "-"));
    ctx.log("startTime=" + t.startTime);
    ctx.log("endTime=" + t.endTime);
    ctx.log("sigmaTestStatus=" + (t.sigmaTestStatus || t.sigma_test_status || ""));

    // 若失败:补一份设备上下文+截图
    if (t.status === -3 || t.status === 0) {
      ctx.log("Task ended with error, collecting device evidence...");
      for (var i = 0; i < list.length; i++) {
        writeFailContext(ctx, list[i], "taskResult(" + statusToText(t.status) + ")", t.errorMessage || "");
        screenshotOne(ctx, list[i], "FAIL__taskResult");
      }
    }
  });

  // Step5:Optional - show recent list
  stepRun(ctx, "Step5 tasksList(recent 5)", function () {
    var recent = tasks.tasksList({ limit: 5, orderBy: "updatedAt DESC" });
    ctx.log("recent tasks:\n" + JSON.stringify(recent, null, 2));
  });

  ctx.log("DONE.");
}

main();
TCHelp