"use strict"; const { AsyncShutdown } = ChromeUtils.import( "resource://gre/modules/AsyncShutdown.jsm" ); const { ExtensionCommon } = ChromeUtils.import( "resource://gre/modules/ExtensionCommon.jsm" ); const { NativeManifests } = ChromeUtils.import( "resource://gre/modules/NativeManifests.jsm" ); const { FileUtils } = ChromeUtils.import( "resource://gre/modules/FileUtils.jsm" ); const { Schemas } = ChromeUtils.import("resource://gre/modules/Schemas.jsm"); const { Subprocess, SubprocessImpl } = ChromeUtils.import( "resource://gre/modules/Subprocess.jsm", null ); const { NativeApp } = ChromeUtils.import( "resource://gre/modules/NativeMessaging.jsm" ); const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); let registry = null; if (AppConstants.platform == "win") { var { MockRegistry } = ChromeUtils.import( "resource://testing-common/MockRegistry.jsm" ); registry = new MockRegistry(); registerCleanupFunction(() => { registry.shutdown(); }); } const REGPATH = "Software\\Mozilla\\NativeMessagingHosts"; const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json"; const TYPE_SLUG = AppConstants.platform === "linux" ? "native-messaging-hosts" : "NativeMessagingHosts"; let dir = FileUtils.getDir("TmpD", ["NativeManifests"]); dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); let userDir = dir.clone(); userDir.append("user"); userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); let globalDir = dir.clone(); globalDir.append("global"); globalDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); OS.File.makeDir(OS.Path.join(userDir.path, TYPE_SLUG)); OS.File.makeDir(OS.Path.join(globalDir.path, TYPE_SLUG)); let dirProvider = { getFile(property) { if (property == "XREUserNativeManifests") { return userDir.clone(); } else if (property == "XRESysNativeManifests") { return globalDir.clone(); } return null; }, }; Services.dirsvc.registerProvider(dirProvider); registerCleanupFunction(() => { Services.dirsvc.unregisterProvider(dirProvider); dir.remove(true); }); function writeManifest(path, manifest) { if (typeof manifest != "string") { manifest = JSON.stringify(manifest); } return OS.File.writeAtomic(path, manifest); } let PYTHON; add_task(async function setup() { await Schemas.load(BASE_SCHEMA); const env = Cc["@mozilla.org/process/environment;1"].getService( Ci.nsIEnvironment ); try { PYTHON = await Subprocess.pathSearch(env.get("PYTHON")); } catch (e) { notEqual( PYTHON, null, `Can't find a suitable python interpreter ${e.message}` ); } }); let global = this; // Test of NativeManifests.lookupApplication() begin here... let context = { extension: { id: "extension@tests.mozilla.org", }, envType: "addon_parent", url: null, jsonStringify(...args) { return JSON.stringify(...args); }, cloneScope: global, logError() {}, preprocessors: {}, callOnClose: () => {}, forgetOnClose: () => {}, }; class MockContext extends ExtensionCommon.BaseContext { constructor(extensionId) { let fakeExtension = { id: extensionId }; super("addon_parent", fakeExtension); this.sandbox = Cu.Sandbox(global); } get cloneScope() { return global; } get principal() { return Cu.getObjectPrincipal(this.sandbox); } } let templateManifest = { name: "test", description: "this is only a test", path: "/bin/cat", type: "stdio", allowed_extensions: ["extension@tests.mozilla.org"], }; function lookupApplication(app, ctx) { return NativeManifests.lookupManifest("stdio", app, ctx); } add_task(async function test_nonexistent_manifest() { let result = await lookupApplication("test", context); equal( result, null, "lookupApplication returns null for non-existent application" ); }); const USER_TEST_JSON = OS.Path.join(userDir.path, TYPE_SLUG, "test.json"); add_task(async function test_nonexistent_manifest_with_registry_entry() { if (registry) { registry.setValue( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, `${REGPATH}\\test`, "", USER_TEST_JSON ); } await OS.File.remove(USER_TEST_JSON); let { messages, result } = await promiseConsoleOutput(() => lookupApplication("test", context) ); equal( result, null, "lookupApplication returns null for non-existent manifest" ); let noSuchFileErrors = messages.filter(logMessage => logMessage.message.includes( "file is referenced in the registry but does not exist" ) ); if (registry) { equal( noSuchFileErrors.length, 1, "lookupApplication logs a non-existent manifest file pointed to by the registry" ); } else { equal( noSuchFileErrors.length, 0, "lookupApplication does not log about registry on non-windows platforms" ); } }); add_task(async function test_good_manifest() { await writeManifest(USER_TEST_JSON, templateManifest); if (registry) { registry.setValue( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, `${REGPATH}\\test`, "", USER_TEST_JSON ); } let result = await lookupApplication("test", context); notEqual(result, null, "lookupApplication finds a good manifest"); equal( result.path, USER_TEST_JSON, "lookupApplication returns the correct path" ); deepEqual( result.manifest, templateManifest, "lookupApplication returns the manifest contents" ); }); add_task(async function test_invalid_json() { await writeManifest(USER_TEST_JSON, "this is not valid json"); let result = await lookupApplication("test", context); equal(result, null, "lookupApplication ignores bad json"); }); add_task(async function test_invalid_name() { let manifest = Object.assign({}, templateManifest); manifest.name = "../test"; await writeManifest(USER_TEST_JSON, manifest); let result = await lookupApplication("test", context); equal(result, null, "lookupApplication ignores an invalid name"); }); add_task(async function test_name_mismatch() { let manifest = Object.assign({}, templateManifest); manifest.name = "not test"; await writeManifest(USER_TEST_JSON, manifest); let result = await lookupApplication("test", context); let what = AppConstants.platform == "win" ? "registry key" : "json filename"; equal( result, null, `lookupApplication ignores mistmatch between ${what} and name property` ); }); add_task(async function test_missing_props() { const PROPS = ["name", "description", "path", "type", "allowed_extensions"]; for (let prop of PROPS) { let manifest = Object.assign({}, templateManifest); delete manifest[prop]; await writeManifest(USER_TEST_JSON, manifest); let result = await lookupApplication("test", context); equal(result, null, `lookupApplication ignores missing ${prop}`); } }); add_task(async function test_invalid_type() { let manifest = Object.assign({}, templateManifest); manifest.type = "bogus"; await writeManifest(USER_TEST_JSON, manifest); let result = await lookupApplication("test", context); equal(result, null, "lookupApplication ignores invalid type"); }); add_task(async function test_no_allowed_extensions() { let manifest = Object.assign({}, templateManifest); manifest.allowed_extensions = []; await writeManifest(USER_TEST_JSON, manifest); let result = await lookupApplication("test", context); equal( result, null, "lookupApplication ignores manifest with no allowed_extensions" ); }); const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, TYPE_SLUG, "test.json"); let globalManifest = Object.assign({}, templateManifest); globalManifest.description = "This manifest is from the systemwide directory"; add_task(async function good_manifest_system_dir() { await OS.File.remove(USER_TEST_JSON); await writeManifest(GLOBAL_TEST_JSON, globalManifest); if (registry) { registry.setValue( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, `${REGPATH}\\test`, "", null ); registry.setValue( Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, `${REGPATH}\\test`, "", GLOBAL_TEST_JSON ); } let where = AppConstants.platform == "win" ? "registry location" : "directory"; let result = await lookupApplication("test", context); notEqual( result, null, `lookupApplication finds a manifest in the system-wide ${where}` ); equal( result.path, GLOBAL_TEST_JSON, `lookupApplication returns path in the system-wide ${where}` ); deepEqual( result.manifest, globalManifest, `lookupApplication returns manifest contents from the system-wide ${where}` ); }); add_task(async function test_user_dir_precedence() { await writeManifest(USER_TEST_JSON, templateManifest); if (registry) { registry.setValue( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, `${REGPATH}\\test`, "", USER_TEST_JSON ); } // global test.json and LOCAL_MACHINE registry key on windows are // still present from the previous test let result = await lookupApplication("test", context); notEqual( result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide locations" ); equal( result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist" ); deepEqual( result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist" ); }); // Test shutdown handling in NativeApp add_task(async function test_native_app_shutdown() { const SCRIPT = String.raw` import signal import struct import sys signal.signal(signal.SIGTERM, signal.SIG_IGN) stdin = getattr(sys.stdin, 'buffer', sys.stdin) stdout = getattr(sys.stdout, 'buffer', sys.stdout) while True: rawlen = stdin.read(4) if len(rawlen) == 0: signal.pause() msglen = struct.unpack('@I', rawlen)[0] msg = stdin.read(msglen) stdout.write(struct.pack('@I', msglen)) stdout.write(msg) `; let scriptPath = OS.Path.join(userDir.path, TYPE_SLUG, "wontdie.py"); let manifestPath = OS.Path.join(userDir.path, TYPE_SLUG, "wontdie.json"); const ID = "native@tests.mozilla.org"; let manifest = { name: "wontdie", description: "test async shutdown of native apps", type: "stdio", allowed_extensions: [ID], }; if (AppConstants.platform == "win") { await OS.File.writeAtomic(scriptPath, SCRIPT); let batPath = OS.Path.join(userDir.path, TYPE_SLUG, "wontdie.bat"); let batBody = `@ECHO OFF\n${PYTHON} -u "${scriptPath}" %*\n`; await OS.File.writeAtomic(batPath, batBody); await OS.File.setPermissions(batPath, { unixMode: 0o755 }); manifest.path = batPath; await writeManifest(manifestPath, manifest); registry.setValue( Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, `${REGPATH}\\wontdie`, "", manifestPath ); } else { await OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`); await OS.File.setPermissions(scriptPath, { unixMode: 0o755 }); manifest.path = scriptPath; await writeManifest(manifestPath, manifest); } let mockContext = new MockContext(ID); let app = new NativeApp(mockContext, "wontdie"); // send a message and wait for the reply to make sure the app is running let MSG = "test"; let recvPromise = new Promise(resolve => { let listener = (what, msg) => { equal(msg, MSG, "Received test message"); app.off("message", listener); resolve(); }; app.on("message", listener); }); let buffer = NativeApp.encodeMessage(mockContext, MSG); app.send(new StructuredCloneHolder(buffer)); await recvPromise; app._cleanup(); info("waiting for async shutdown"); Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); AsyncShutdown.profileBeforeChange._trigger(); Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); let procs = await SubprocessImpl.Process.getWorker().call("getProcesses", []); equal(procs.size, 0, "native process exited"); });