351 lines
10 KiB
JavaScript
351 lines
10 KiB
JavaScript
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
|
|
/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetters(lazy, {
|
|
XreDirProvider: [
|
|
"@mozilla.org/xre/directory-provider;1",
|
|
"nsIXREDirProvider",
|
|
],
|
|
});
|
|
|
|
ChromeUtils.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.components.taskscheduler.loglevel",
|
|
prefix: "TaskScheduler",
|
|
};
|
|
return new ConsoleAPI(consoleOptions);
|
|
});
|
|
|
|
/**
|
|
* Task generation and management for macOS, using `launchd` via `launchctl`.
|
|
*
|
|
* Implements the API exposed in TaskScheduler.sys.mjs
|
|
* Not intended for external use, this is in a separate module to ship the code only
|
|
* on macOS, and to expose for testing.
|
|
*/
|
|
export var MacOSImpl = {
|
|
async registerTask(id, command, intervalSeconds, options) {
|
|
lazy.log.info(
|
|
`registerTask(${id}, ${command}, ${intervalSeconds}, ${JSON.stringify(
|
|
options
|
|
)})`
|
|
);
|
|
|
|
let uid = await this._uid();
|
|
lazy.log.debug(`registerTask: uid=${uid}`);
|
|
|
|
let label = this._formatLabelForThisApp(id, options);
|
|
|
|
// We ignore `options.disabled`, which is test only.
|
|
//
|
|
// The `Disabled` key prevents `launchd` from registering the task, with
|
|
// exit code 133 and error message "Service is disabled". If we really want
|
|
// this flow in the future, there is `launchctl disable ...`, but it's
|
|
// fraught with peril: the disabled status is stored outside of any plist,
|
|
// and it persists even after the task is deleted. Monkeying with the
|
|
// disabled status will likely prevent users from disabling these tasks
|
|
// forcibly, should it come to that. All told, fraught.
|
|
//
|
|
// For the future: there is the `RunAtLoad` key, should we want to run the
|
|
// task once immediately.
|
|
let plist = {};
|
|
plist.Label = label;
|
|
plist.ProgramArguments = [command];
|
|
if (options.args) {
|
|
plist.ProgramArguments.push(...options.args);
|
|
}
|
|
plist.StartInterval = intervalSeconds;
|
|
if (options.workingDirectory) {
|
|
plist.WorkingDirectory = options.workingDirectory;
|
|
}
|
|
|
|
let str = this._formatLaunchdPlist(plist);
|
|
let path = this._formatPlistPath(label);
|
|
|
|
await IOUtils.write(path, new TextEncoder().encode(str));
|
|
lazy.log.debug(`registerTask: wrote ${path}`);
|
|
|
|
try {
|
|
let bootout = await lazy.Subprocess.call({
|
|
command: "/bin/launchctl",
|
|
arguments: ["bootout", `gui/${uid}/${label}`],
|
|
stderr: "stdout",
|
|
});
|
|
|
|
lazy.log.debug(
|
|
"registerTask: bootout stdout",
|
|
await bootout.stdout.readString()
|
|
);
|
|
|
|
let { exitCode } = await bootout.wait();
|
|
lazy.log.debug(`registerTask: bootout returned ${exitCode}`);
|
|
|
|
let bootstrap = await lazy.Subprocess.call({
|
|
command: "/bin/launchctl",
|
|
arguments: ["bootstrap", `gui/${uid}`, path],
|
|
stderr: "stdout",
|
|
});
|
|
|
|
lazy.log.debug(
|
|
"registerTask: bootstrap stdout",
|
|
await bootstrap.stdout.readString()
|
|
);
|
|
|
|
({ exitCode } = await bootstrap.wait());
|
|
lazy.log.debug(`registerTask: bootstrap returned ${exitCode}`);
|
|
|
|
if (exitCode != 0) {
|
|
throw new Components.Exception(
|
|
`Failed to run launchctl bootstrap: ${exitCode}`,
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
}
|
|
} catch (e) {
|
|
// Try to clean up.
|
|
await IOUtils.remove(path, { ignoreAbsent: true });
|
|
throw e;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
async deleteTask(id, options) {
|
|
lazy.log.info(`deleteTask(${id})`);
|
|
|
|
let label = this._formatLabelForThisApp(id, options);
|
|
return this._deleteTaskByLabel(label);
|
|
},
|
|
|
|
async _deleteTaskByLabel(label) {
|
|
let path = this._formatPlistPath(label);
|
|
lazy.log.debug(`_deleteTaskByLabel: removing ${path}`);
|
|
await IOUtils.remove(path, { ignoreAbsent: true });
|
|
|
|
let uid = await this._uid();
|
|
lazy.log.debug(`_deleteTaskByLabel: uid=${uid}`);
|
|
|
|
let bootout = await lazy.Subprocess.call({
|
|
command: "/bin/launchctl",
|
|
arguments: ["bootout", `gui/${uid}/${label}`],
|
|
stderr: "stdout",
|
|
});
|
|
|
|
let { exitCode } = await bootout.wait();
|
|
lazy.log.debug(`_deleteTaskByLabel: bootout returned ${exitCode}`);
|
|
lazy.log.debug(
|
|
`_deleteTaskByLabel: bootout stdout`,
|
|
await bootout.stdout.readString()
|
|
);
|
|
|
|
return !exitCode;
|
|
},
|
|
|
|
// For internal and testing use only.
|
|
async _listAllLabelsForThisApp() {
|
|
let proc = await lazy.Subprocess.call({
|
|
command: "/bin/launchctl",
|
|
arguments: ["list"],
|
|
stderr: "stdout",
|
|
});
|
|
|
|
let { exitCode } = await proc.wait();
|
|
if (exitCode != 0) {
|
|
throw new Components.Exception(
|
|
`Failed to run /bin/launchctl list: ${exitCode}`,
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
}
|
|
|
|
let stdout = await proc.stdout.readString();
|
|
|
|
let lines = stdout.split(/\r\n|\n|\r/);
|
|
let labels = lines
|
|
.map(line => line.split("\t").pop()) // Lines are like "-\t0\tlabel".
|
|
.filter(this._labelMatchesThisApp);
|
|
|
|
lazy.log.debug(`_listAllLabelsForThisApp`, labels);
|
|
return labels;
|
|
},
|
|
|
|
async deleteAllTasks() {
|
|
lazy.log.info(`deleteAllTasks()`);
|
|
|
|
let labelsToDelete = await this._listAllLabelsForThisApp();
|
|
|
|
let deleted = 0;
|
|
let failed = 0;
|
|
for (const label of labelsToDelete) {
|
|
try {
|
|
if (await this._deleteTaskByLabel(label)) {
|
|
deleted += 1;
|
|
} else {
|
|
failed += 1;
|
|
}
|
|
} catch (e) {
|
|
failed += 1;
|
|
}
|
|
}
|
|
|
|
let result = { deleted, failed };
|
|
lazy.log.debug(`deleteAllTasks: returning ${JSON.stringify(result)}`);
|
|
},
|
|
|
|
async taskExists(id, options) {
|
|
const label = this._formatLabelForThisApp(id, options);
|
|
const path = this._formatPlistPath(label);
|
|
return IOUtils.exists(path);
|
|
},
|
|
|
|
/**
|
|
* Turn an object into a macOS plist.
|
|
*
|
|
* Properties of type array-of-string, dict-of-string, string,
|
|
* number, and boolean are supported.
|
|
*
|
|
* @param options object to turn into macOS plist.
|
|
* @returns plist as an XML DOM object.
|
|
*/
|
|
_toLaunchdPlist(options) {
|
|
const doc = new DOMParser().parseFromString("<plist></plist>", "text/xml");
|
|
const root = doc.documentElement;
|
|
root.setAttribute("version", "1.0");
|
|
|
|
let dict = doc.createElement("dict");
|
|
root.appendChild(dict);
|
|
|
|
for (let [k, v] of Object.entries(options)) {
|
|
let key = doc.createElement("key");
|
|
key.textContent = k;
|
|
dict.appendChild(key);
|
|
|
|
if (Array.isArray(v)) {
|
|
let array = doc.createElement("array");
|
|
dict.appendChild(array);
|
|
|
|
for (let vv of v) {
|
|
let string = doc.createElement("string");
|
|
string.textContent = vv;
|
|
array.appendChild(string);
|
|
}
|
|
} else if (typeof v === "object") {
|
|
let d = doc.createElement("dict");
|
|
dict.appendChild(d);
|
|
|
|
for (let [kk, vv] of Object.entries(v)) {
|
|
key = doc.createElement("key");
|
|
key.textContent = kk;
|
|
d.appendChild(key);
|
|
|
|
let string = doc.createElement("string");
|
|
string.textContent = vv;
|
|
d.appendChild(string);
|
|
}
|
|
} else if (typeof v === "number") {
|
|
let number = doc.createElement(
|
|
Number.isInteger(v) ? "integer" : "real"
|
|
);
|
|
number.textContent = v;
|
|
dict.appendChild(number);
|
|
} else if (typeof v === "string") {
|
|
let string = doc.createElement("string");
|
|
string.textContent = v;
|
|
dict.appendChild(string);
|
|
} else if (typeof v === "boolean") {
|
|
let bool = doc.createElement(v ? "true" : "false");
|
|
dict.appendChild(bool);
|
|
}
|
|
}
|
|
|
|
return doc;
|
|
},
|
|
|
|
/**
|
|
* Turn an object into a macOS plist encoded as a string.
|
|
*
|
|
* Properties of type array-of-string, dict-of-string, string,
|
|
* number, and boolean are supported.
|
|
*
|
|
* @param options object to turn into macOS plist.
|
|
* @returns plist as a string.
|
|
*/
|
|
_formatLaunchdPlist(options) {
|
|
let doc = this._toLaunchdPlist(options);
|
|
|
|
let serializer = new XMLSerializer();
|
|
return serializer.serializeToString(doc);
|
|
},
|
|
|
|
_formatLabelForThisApp(id) {
|
|
let installHash = lazy.XreDirProvider.getInstallHash();
|
|
return `${AppConstants.MOZ_MACBUNDLE_ID}.${installHash}.${id}`;
|
|
},
|
|
|
|
_labelMatchesThisApp(label) {
|
|
let installHash = lazy.XreDirProvider.getInstallHash();
|
|
return (
|
|
label &&
|
|
label.startsWith(`${AppConstants.MOZ_MACBUNDLE_ID}.${installHash}.`)
|
|
);
|
|
},
|
|
|
|
_formatPlistPath(label) {
|
|
let file = Services.dirsvc.get("Home", Ci.nsIFile);
|
|
file.append("Library");
|
|
file.append("LaunchAgents");
|
|
file.append(`${label}.plist`);
|
|
return file.path;
|
|
},
|
|
|
|
_cachedUid: -1,
|
|
|
|
async _uid() {
|
|
if (this._cachedUid >= 0) {
|
|
return this._cachedUid;
|
|
}
|
|
|
|
// There are standard APIs for determining our current UID, but this
|
|
// is easy and parallel to the general tactics used by this module.
|
|
let proc = await lazy.Subprocess.call({
|
|
command: "/usr/bin/id",
|
|
arguments: ["-u"],
|
|
stderr: "stdout",
|
|
});
|
|
|
|
let stdout = await proc.stdout.readString();
|
|
|
|
let { exitCode } = await proc.wait();
|
|
if (exitCode != 0) {
|
|
throw new Components.Exception(
|
|
`Failed to run /usr/bin/id: ${exitCode}`,
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
}
|
|
|
|
try {
|
|
this._cachedUid = Number.parseInt(stdout);
|
|
return this._cachedUid;
|
|
} catch (e) {
|
|
throw new Components.Exception(
|
|
`Failed to parse /usr/bin/id output as integer: ${stdout}`,
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
}
|
|
},
|
|
};
|