summaryrefslogtreecommitdiffstats
path: root/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs
blob: 0c2f277a23ec66f2b4cfc4bc1851101cc7afe804 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

XPCOMUtils.defineLazyGetter(lazy, "log", () => {
  let { ConsoleAPI } = ChromeUtils.importESModule(
    "resource://gre/modules/Console.sys.mjs"
  );
  let consoleOptions = {
    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
    // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
    maxLogLevel: "error",
    maxLogLevelPref: "toolkit.backgroundtasks.loglevel",
    prefix: "BackgroundTasksManager",
  };
  return new ConsoleAPI(consoleOptions);
});

XPCOMUtils.defineLazyGetter(lazy, "DevToolsStartup", () => {
  return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
    Ci.nsICommandLineHandler
  ).wrappedJSObject;
});

// The default timing settings can be overriden by the preferences
// toolkit.backgroundtasks.defaultTimeoutSec and
// toolkit.backgroundtasks.defaultMinTaskRuntimeMS for all background tasks
// and individually per module by
// export const backgroundTaskTimeoutSec = X;
// export const backgroundTaskMinRuntimeMS = Y;
let timingSettings = {
  minTaskRuntimeMS: 500,
  maxTaskRuntimeSec: 600, // 10 minutes.
};

// Map resource://testing-common/ to the shared test modules directory.  This is
// a transliteration of `register_modules_protocol_handler` from
// https://searchfox.org/mozilla-central/rev/f081504642a115cb8236bea4d8250e5cb0f39b02/testing/xpcshell/head.js#358-389.
function registerModulesProtocolHandler() {
  let _TESTING_MODULES_URI = Services.env.get(
    "XPCSHELL_TESTING_MODULES_URI",
    ""
  );
  if (!_TESTING_MODULES_URI) {
    return false;
  }

  let protocolHandler = Services.io
    .getProtocolHandler("resource")
    .QueryInterface(Ci.nsIResProtocolHandler);

  protocolHandler.setSubstitution(
    "testing-common",
    Services.io.newURI(_TESTING_MODULES_URI)
  );
  // Log loudly so that when testing, we always actually use the
  // console logging mechanism and therefore deterministically load that code.
  lazy.log.error(
    `Substitution set: resource://testing-common aliases ${_TESTING_MODULES_URI}`
  );

  return true;
}

function locationsForBackgroundTaskNamed(name) {
  const subModules = [
    "resource:///modules", // App-specific first.
    "resource://gre/modules", // Toolkit/general second.
  ];

  if (registerModulesProtocolHandler()) {
    subModules.push("resource://testing-common"); // Test-only third.
  }

  let locations = [];
  for (const subModule of subModules) {
    let URI = `${subModule}/backgroundtasks/BackgroundTask_${name}.sys.mjs`;
    locations.push(URI);
  }

  return locations;
}

/**
 * Find an ES module named like `backgroundtasks/BackgroundTask_${name}.sys.mjs`,
 * import it, and return the whole module.
 *
 * When testing, allow to load from `XPCSHELL_TESTING_MODULES_URI`,
 * which is registered at `resource://testing-common`, the standard
 * location for test-only modules.
 *
 * @return {Object} The imported module.
 * @throws NS_ERROR_NOT_AVAILABLE if a background task with the given `name` is
 * not found.
 */
function findBackgroundTaskModule(name) {
  for (const URI of locationsForBackgroundTaskNamed(name)) {
    lazy.log.debug(`Looking for background task at URI: ${URI}`);

    try {
      const taskModule = ChromeUtils.importESModule(URI);
      lazy.log.info(`Found background task at URI: ${URI}`);
      return taskModule;
    } catch (ex) {
      if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
        throw ex;
      }
    }
  }

  lazy.log.warn(`No backgroundtask named '${name}' registered`);
  throw new Components.Exception(
    `No backgroundtask named '${name}' registered`,
    Cr.NS_ERROR_NOT_AVAILABLE
  );
}

export class BackgroundTasksManager {
  get helpInfo() {
    const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
      Ci.nsIBackgroundTasks
    );

    if (bts.isBackgroundTaskMode) {
      return lazy.DevToolsStartup.jsdebuggerHelpInfo;
    }

    return "";
  }

  handle(commandLine) {
    const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
      Ci.nsIBackgroundTasks
    );

    if (!bts.isBackgroundTaskMode) {
      lazy.log.info(
        `${Services.appinfo.processID}: !isBackgroundTaskMode, exiting`
      );
      return;
    }

    const name = bts.backgroundTaskName();
    lazy.log.info(
      `${Services.appinfo.processID}: Preparing to run background task named '${name}'` +
        ` (with ${commandLine.length} arguments)`
    );

    if (!("@mozilla.org/devtools/startup-clh;1" in Cc)) {
      return;
    }

    // Check this before the devtools startup flow handles and removes it.
    const CASE_INSENSITIVE = false;
    if (
      commandLine.findFlag("jsdebugger", CASE_INSENSITIVE) < 0 &&
      commandLine.findFlag("start-debugger-server", CASE_INSENSITIVE) < 0
    ) {
      lazy.log.info(
        `${Services.appinfo.processID}: No devtools flag found; not preparing devtools thread`
      );
      return;
    }

    const waitFlag =
      commandLine.findFlag("wait-for-jsdebugger", CASE_INSENSITIVE) != -1;
    if (waitFlag) {
      function onDevtoolsThreadReady(subject, topic, data) {
        lazy.log.info(
          `${Services.appinfo.processID}: Setting breakpoints for background task named '${name}'` +
            ` (with ${commandLine.length} arguments)`
        );

        const threadActor = subject.wrappedJSObject;
        threadActor.setBreakpointOnLoad(locationsForBackgroundTaskNamed(name));

        Services.obs.removeObserver(onDevtoolsThreadReady, topic);
      }

      Services.obs.addObserver(onDevtoolsThreadReady, "devtools-thread-ready");
    }

    const DevToolsStartup = Cc[
      "@mozilla.org/devtools/startup-clh;1"
    ].getService(Ci.nsICommandLineHandler);
    DevToolsStartup.handle(commandLine);
  }

  async runBackgroundTaskNamed(name, commandLine) {
    function addMarker(markerName) {
      return ChromeUtils.addProfilerMarker(markerName, undefined, name);
    }
    addMarker("BackgroundTasksManager:AfterRunBackgroundTaskNamed");

    lazy.log.info(
      `${Services.appinfo.processID}: Running background task named '${name}'` +
        ` (with ${commandLine.length} arguments)`
    );
    lazy.log.debug(
      `${Services.appinfo.processID}: Background task using profile` +
        ` '${Services.dirsvc.get("ProfD", Ci.nsIFile).path}'`
    );

    let exitCode = EXIT_CODE.NOT_FOUND;
    try {
      let taskModule = findBackgroundTaskModule(name);
      addMarker("BackgroundTasksManager:AfterFindRunBackgroundTask");

      // Get timing configuration. First check for default preferences,
      // then for per module overrides.
      timingSettings.minTaskRuntimeMS = Services.prefs.getIntPref(
        "toolkit.backgroundtasks.defaultMinTaskRuntimeMS",
        timingSettings.minTaskRuntimeMS
      );
      if (taskModule.backgroundTaskMinRuntimeMS) {
        timingSettings.minTaskRuntimeMS = taskModule.backgroundTaskMinRuntimeMS;
      }
      timingSettings.maxTaskRuntimeSec = Services.prefs.getIntPref(
        "toolkit.backgroundtasks.defaultTimeoutSec",
        timingSettings.maxTaskRuntimeSec
      );
      if (taskModule.backgroundTaskTimeoutSec) {
        timingSettings.maxTaskRuntimeSec = taskModule.backgroundTaskTimeoutSec;
      }

      try {
        let minimumReached = false;
        let minRuntime = new Promise(resolve =>
          lazy.setTimeout(() => {
            minimumReached = true;
            resolve(true);
          }, timingSettings.minTaskRuntimeMS)
        );
        exitCode = await Promise.race([
          new Promise(resolve =>
            lazy.setTimeout(() => {
              lazy.log.error(`Background task named '${name}' timed out`);
              resolve(EXIT_CODE.TIMEOUT);
            }, timingSettings.maxTaskRuntimeSec * 1000)
          ),
          taskModule.runBackgroundTask(commandLine),
        ]);
        if (!minimumReached) {
          lazy.log.debug(
            `Backgroundtask named '${name}' waiting for minimum runtime.`
          );
          await minRuntime;
        }
        lazy.log.info(
          `Backgroundtask named '${name}' completed with exit code ${exitCode}`
        );
      } catch (e) {
        lazy.log.error(`Backgroundtask named '${name}' threw exception`, e);
        exitCode = EXIT_CODE.EXCEPTION;
      }
    } finally {
      addMarker("BackgroundTasksManager:AfterAwaitRunBackgroundTask");

      lazy.log.info(`Invoking Services.startup.quit(..., ${exitCode})`);
      Services.startup.quit(Ci.nsIAppStartup.eForceQuit, exitCode);
    }

    return exitCode;
  }

  classID = Components.ID("{4d48c536-e16f-4699-8f9c-add4f28f92f0}");
  QueryInterface = ChromeUtils.generateQI([
    "nsIBackgroundTasksManager",
    "nsICommandLineHandler",
  ]);
}

/**
 * Background tasks should standard exit code conventions where 0 denotes
 * success and non-zero denotes failure and/or an error.  In addition, since
 * background tasks have limited channels to communicate with consumers, the
 * special values `NOT_FOUND` (integer 2) and `THREW_EXCEPTION` (integer 3) are
 * distinguished.
 *
 * If you extend this to add background task-specific exit codes, use exit codes
 * greater than 10 to allow for additional shared exit codes to be added here.
 * Exit codes should be between 0 and 127 to be safe across platforms.
 */
export const EXIT_CODE = {
  /**
   * The task succeeded.
   *
   * The `runBackgroundTask(...)` promise resolved to 0.
   */
  SUCCESS: 0,

  /**
   * The task with the specified name could not be found or imported.
   *
   * The corresponding `runBackgroundTask` method could not be found.
   */
  NOT_FOUND: 2,

  /**
   * The task failed with an uncaught exception.
   *
   * The `runBackgroundTask(...)` promise rejected with an exception.
   */
  EXCEPTION: 3,

  /**
   * The task took too long and timed out.
   *
   * The default timeout is controlled by the pref:
   * "toolkit.backgroundtasks.defaultTimeoutSec", but tasks can override this
   * by exporting a non-zero `backgroundTaskTimeoutSec` value.
   */
  TIMEOUT: 4,

  /**
   * The last exit code reserved by this structure.  Use codes larger than this
   * code for background task-specific exit codes.
   */
  LAST_RESERVED: 10,
};