[Service] Introduce worker isolation 85/242985/13
authorDongHyun Song <dh81.song@samsung.com>
Wed, 2 Sep 2020 08:16:19 +0000 (17:16 +0900)
committerDongHyun Song <dh81.song@samsung.com>
Tue, 8 Sep 2020 08:40:48 +0000 (08:40 +0000)
node-vm, previous way to isolate service applications, has a big
problem of global scope sharing with Tizen webapis. Becuase tizen
webapi objects are registered on global scope, with DeviceAPIRouter
overrieded APIs are shared by each service application calling.
In addition, require() is also running on global scope, even though
apps are calling require() in their sandbox context, imported
modules are running global scope.

Thus, isolation of global scope is most important for global wrt-
service. Node worker is very proper way to isolate each service
application. With node worker, v8::Isolatee and v8::IsolateData
are created separately. Then, each service app is able to have
each independent global object.

Reference patch:
  https://review.tizen.org/230796/

Change-Id: Ic4008ed7a8331327eeb84facba55418e971e2271
Signed-off-by: DongHyun Song <dh81.song@samsung.com>
wrt_app/common/service_manager.ts
wrt_app/common/service_runner.ts [new file with mode: 0644]
wrt_app/service/access_control_manager.ts
wrt_app/service/device_api_router.ts
wrt_app/service/main.ts

index 330e44162561ec164ea1f904ca62b05554ceb3a1..80f1ec6fe2dc4cbfd66528a55bd27b67cdb3b1b6 100644 (file)
-const Module = require('module');
-import { TimerManager } from '../service/timer_manager';
-import * as XWalkExtension from './wrt_xwalk_extension';
-import * as vm from 'vm';
+import { Worker, isMainThread } from 'worker_threads';
 import { wrt } from '../browser/wrt';
-import { DeviceAPIRouter } from '../service/device_api_router';
 
-interface ContextMap {
-  [id: string]: vm.Context;
+interface WorkerMap {
+  [id: string]: any;
 }
+let workers: WorkerMap = {};
+let serviceType: string = wrt.getServiceModel();;
+let runner: any;
 
-interface ContextOption {
-  [key: string]: any;
+function isStandalone() {
+  return serviceType === 'STANDALONE';
 }
 
-let sandbox: ContextMap = {};
-let internal_handler: ContextOption = {};
-let service_type: string = wrt.getServiceModel?.() ?? 'UI';
-
-function requestStopService(id: string) {
-  console.log(`${id} will be closed`);
-  setTimeout(() => wrt.stopService(id), 500);
-}
-
-function callFunctionInContext(name: string, id: string) {
-  try {
-    const script = `if (typeof ${name} === 'function') { ${name}(); }`;
-    vm.runInContext(script, sandbox[id]);
-  } catch (e) {
-    console.log(`${name} has exception: ${e}`);
-    if (wrt.tv) {
-      requestStopService(id);
+export function startService(id: string, filename: string) {
+  console.log(`startService - ${id}`);
+  if (isStandalone()) {
+    runner = require('../common/service_runner');
+    runner.start(id, filename);
+  } else {
+    if (isMainThread) {
+      let startService = __dirname + '/service_runner.js';
+      workers[id] = new Worker(startService, { workerData: { id: id, filename: filename } });
     }
   }
 }
 
-export function startService(id: string, filename?: string) {
-  if (sandbox[id] === undefined) {
-    XWalkExtension.initialize();
-    XWalkExtension.setRuntimeMessageHandler((type, data) => {
-      if (type === 'tizen://exit') {
-        requestStopService(id);
-      }
-    });
-    sandbox[id] = {
-      console: console,
-      module: new Module,
-      require: require,
-      tizen: global.tizen,
-      webapis: wrt.tv ? global.webapis : global.webapis = {},
-    };
-    sandbox[id].module.exports.onStop = () => {
-      callFunctionInContext('module.exports.onExit', id);
-    };
-    let ids = id.split(':');
-    let caller_app_id = ids[1] ?? '';
-    sandbox[id].webapis.getCallerAppId = () => {
-      return caller_app_id;
-    }
-    let service_id = ids[0];
-    sandbox[id].webapis.getServiceId = () => {
-      return service_id;
-    }
-    sandbox[id].webapis.getPackageId = () => {
-      let app_info = global.tizen.application.getAppInfo(service_id);
-      if (app_info)
-        return app_info.packageId;
-      return ids[0].split('.')[0];
-    }
-
-    if (service_type !== 'UI') {
-      const permissions = wrt.getPrivileges(id);
-      console.log(`permissions : ${permissions}`);
-      const AccessControlManager = require('../service/access_control_manager');
-      AccessControlManager.initialize(permissions, sandbox[id]);
-    }
-    for (let key in global)
-      sandbox[id][key] = global[key];
-
-    internal_handler[id] = {};
-    internal_handler[id].timer_manager = new TimerManager();
-    const timer_api = internal_handler[id].timer_manager.getTimerAPI();
-    for (let key in timer_api)
-      sandbox[id][key] = timer_api[key];
-
-    let object_list = [ 'Error', 'EvalError', 'RangeError', 'ReferenceError',
-        'SyntaxError', 'TypeError', 'URIError', 'Number', 'BigInt', 'Math', 'Date',
-        'String', 'RegExp', 'Array', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray',
-        'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array',
-        'Float64Array', 'BigInt64Array', 'BigUint64Array', 'Map', 'Set', 'WeakMap',
-        'WeakSet', 'ArrayBuffer', 'DataView', 'JSON', 'Promise', 'Reflect', 'Proxy',
-        'Intl', 'WebAssembly', 'Boolean', 'Function', 'Object', 'Symbol' ];
-    for (let prop of object_list)
-      sandbox[id][prop] = global[prop];
-
-    let options: ContextOption = {};
-    let code;
-    if (service_type !== 'UI') {
-      options.filename = id;
-      if (wrt.tv) {
-        let extension_resolver = function (module: any, file_path: string) {
-          console.log(`resolved path: ${file_path}`);
-          let content = (wrt.tv as NativeWRTjs.TVExtension).decryptFile(id, file_path);
-          if (content) {
-            // Remove BOM
-            if (content.charCodeAt(0)  === 0xFEFF)
-              content = content.slice(1);
-            module._compile(content, file_path);
-          }
-        };
-        sandbox[id].require.extensions['.js.spm'] = extension_resolver;
-        sandbox[id].require.extensions['.spm'] = extension_resolver;
-      }
-      filename = wrt.getStartServiceFile(id);
-      console.log(`start global service file: ${filename}`);
-    }
-    code = `const app = require('${filename}')`;
-    if (service_type === 'DAEMON') {
-      internal_handler[id].deivce_api_router = new DeviceAPIRouter(sandbox[id]);
+export function stopService(id: string) {
+  console.log(`stopService - ${id}`);
+  if (isStandalone()) {
+    if (runner) {
+      runner.stop(id);
     }
-    vm.runInNewContext(code, sandbox[id], options);
-  }
-
-  if (sandbox[id]['started'] === undefined) {
-    sandbox[id]['started'] = true;
-    sandbox[id]['stopped'] = undefined;
-    callFunctionInContext('app.onStart', id);
-    if (service_type !== 'UI')
-      wrt.finishStartingService(id);
   } else {
-    console.log(id + ' service has been started.');
+    workers[id].postMessage('stopService');
   }
-  callFunctionInContext('app.onRequest', id);
-}
-
-export function stopService(id: string) {
-  console.log('stopService')
-  if (sandbox[id]['stopped']) {
-    console.log(id + ' service has been already stopped.');
-    return;
-  }
-
-  sandbox[id]['stopped'] = true;
-  sandbox[id]['started'] = undefined;
-  callFunctionInContext('app.onStop', id);
-
-  internal_handler[id].timer_manager.releaseRemainingTimers();
-  for (let key in sandbox[id])
-    delete sandbox[id][key];
-  delete sandbox[id];
-  for (let key in internal_handler[id])
-    delete internal_handler[id][key];
-  delete internal_handler[id];
-
-  if (Object.keys(sandbox).length === 0)
-    XWalkExtension.cleanup();
 }
diff --git a/wrt_app/common/service_runner.ts b/wrt_app/common/service_runner.ts
new file mode 100644 (file)
index 0000000..7b53192
--- /dev/null
@@ -0,0 +1,103 @@
+import './init';
+import * as XWalkExtension from './wrt_xwalk_extension';
+import { DeviceAPIRouter } from '../service/device_api_router';
+import { isMainThread, parentPort, workerData } from 'worker_threads';
+import { wrt } from '../browser/wrt';
+
+let serviceType: string = wrt.getServiceModel();
+
+function isServiceApplication() {
+  return serviceType !== 'UI';
+}
+
+function isGloablService() {
+  return serviceType === 'DAEMON';
+}
+
+function registerExtensionResolver(id: string) {
+  if (wrt.tv) {
+    let extensionResolver = (module: any, file_path: string) => {
+      console.log(`resolved path: ${file_path}`);
+      let content = (wrt.tv as NativeWRTjs.TVExtension).decryptFile(id, file_path);
+      if (content) {
+        // Remove BOM
+        if (content.charCodeAt(0) === 0xFEFF)
+          content = content.slice(1);
+        module._compile(content, file_path);
+      }
+    };
+    require.extensions['.js.spm'] = extensionResolver;
+    require.extensions['.spm'] = extensionResolver;
+  }
+}
+
+let app: any = null;
+export function start(id: string, filename: string) {
+  XWalkExtension.initialize();
+  XWalkExtension.setRuntimeMessageHandler((type, data) => {
+    if (type === 'tizen://exit') {
+      console.log(`${id} will be closed by ${type}`);
+      setTimeout(() => wrt.stopService(id), 500);
+    }
+  });
+
+  console.log('serviceType : '+serviceType)
+  new DeviceAPIRouter(id, isGloablService());
+
+  if (isServiceApplication()) {
+    registerExtensionResolver(id);
+    filename = wrt.getStartServiceFile(id);
+    console.log(`start global service file: ${filename}`);
+  }
+
+  // FIXME: this is for awaking up uv loop.
+  // uv loop is sleeping for a few second with tizen webapis's aync callback
+  setInterval(() => {}, 100);
+  try {
+    app = require(filename);
+    if (app.onStart !== undefined) {
+      app.onStart();
+    }
+    if (app.onRequest !== undefined) {
+      app.onRequest();
+    }
+    if (isGloablService()) {
+      wrt.finishStartingService(id);
+    }
+  } catch (e) {
+    console.log(`exception on start: ${e}`);
+    setTimeout(() => wrt.stopService(id), 500);
+  }
+}
+
+export function stop(id: string) {
+  try {
+    if (app.onStop !== undefined) {
+      app.onStop();
+    } else if (app.onExit !== undefined) {
+      app.onExit();
+    }
+  } catch (e) {
+    console.log(`exception on stop: ${e}`);
+  }
+  setTimeout(() => process.exit(), 500);
+}
+
+function run() {
+  let id = workerData.id;
+  let filename = workerData.filename;
+  start(id, filename);
+
+  if (!parentPort)
+    return;
+  parentPort.on('message', (msg) => {
+    console.log(`message received : ${msg}`);
+    if (msg === 'stopService') {
+      stop(id);
+    }
+  });
+}
+
+if (!isMainThread) {
+  run();
+}
index 0a00ac5b6ad9c4cf0ede9cfc6285d328b5f12df0..120fe1c00da84038614f9229b28aa14856e15435 100644 (file)
@@ -1,4 +1,3 @@
-import * as vm from 'vm';
 
 function checkSystemInfoApiPrivilege(func: any, permissions: string[]) {
   let override_func  = func;
@@ -11,8 +10,8 @@ function checkSystemInfoApiPrivilege(func: any, permissions: string[]) {
   }
 }
 
-export function initialize(permissions: string[], sandbox: vm.Context) {
-  let tizen = sandbox.tizen;
+export function initialize(permissions: string[]) {
+  let tizen = global.tizen;
   if (!permissions.includes("http://tizen.org/privilege/alarm")) {
     tizen.alarm.add =
     tizen.alarm.remove =
index a64c54917203ae4d2e10a17b4ff56fb7e740db07..b7f645b69971b1fc59aa649d209ea2ee86c42b16 100644 (file)
@@ -2,7 +2,6 @@ import { wrt } from '../browser/wrt';
 
 export class DeviceAPIRouter {
   currentApplication: any;
-  sandbox: any;
   funcCurrentApplication: any;
   funcRequestedAppcontrol: any;
   funcGetAppInfo: any;
@@ -10,37 +9,75 @@ export class DeviceAPIRouter {
   funcGetSharedUri: any;
   funcGetMetadata: any;
   funcGetPackageInfo: any;
-  funcPathResolve: any;
 
-  constructor(sandbox: any) {
-    this.sandbox = sandbox;
-    this.RefineApplicationApis();
-    this.RefinePackageApis();
-    this.RefineFilesystemApis()
+  id: string;
+  serviceId: string;
+  packageId: string;
+  callerAppId: string;
+
+  constructor(id: string, isGlobal: boolean) {
+    this.id = id;
+    let ids = id.split(':');
+    this.serviceId = ids[0];
+    this.callerAppId = ids[1] ?? '';
+    this.packageId = this.serviceId.split('.')[0];
+
+    this.initWebapis();
+    if (isGlobal) {
+      this.refineApplicationApis();
+      this.refinePackageApis();
+      this.refineFilesystemApis()
+      this.initAccessControlManager();
+    }
+  }
+
+  initWebapis() {
+    global.webapis = global.webapis ?? {};
+
+    global.webapis.getCallerAppId = () => {
+      return this.callerAppId;
+    }
+    global.webapis.getServiceId = () => {
+      return this.serviceId;
+    }
+    let app_info = global.tizen.application.getAppInfo(this.serviceId);
+    if (app_info) {
+      this.packageId = app_info.packageId;
+    }
+    global.webapis.getPackageId = () => {
+      return this.packageId;
+    }
   }
 
-  GetServiceId() {
-    return this.sandbox.webapis.getServiceId();
+  initAccessControlManager() {
+    const permissions = wrt.getPrivileges(this.id);
+    console.log(`permissions : ${permissions}`);
+    const AccessControlManager = require('./access_control_manager');
+    AccessControlManager.initialize(permissions);
   }
 
-  GetPackageId() {
-    return this.sandbox.webapis.getPackageId();
+  getServiceId() {
+    return global.webapis.getServiceId();
   }
 
-  RefineApplicationApis() {
+  getPackageId() {
+    return global.webapis.getPackageId();
+  }
+
+  refineApplicationApis() {
     // tizen.application.getCurrentApplication()
     this.funcCurrentApplication = global.tizen.application.getCurrentApplication;
     global.tizen.application.getCurrentApplication = () => {
-      console.log(`Routing - getCurrentApplication() : ${this.GetServiceId()}`);
+      console.log(`Routing - getCurrentApplication() : ${this.getServiceId()}`);
       if (this.currentApplication)
         return this.currentApplication;
       this.currentApplication = this.funcCurrentApplication();
       // tizen.application.getCurrentApplication().getRequestedAppControl()
       this.funcRequestedAppcontrol = this.currentApplication.getRequestedAppControl;
       this.currentApplication.getRequestedAppControl = () => {
-        console.log(`Routing - getRequestedAppControl() : ${this.GetServiceId()}`);
+        console.log(`Routing - getRequestedAppControl() : ${this.getServiceId()}`);
         if (wrt.tv)
-          wrt.tv.setCurrentApplication(this.GetServiceId());
+          wrt.tv.setCurrentApplication(this.getServiceId());
         return this.funcRequestedAppcontrol();
       }
       return this.currentApplication;
@@ -50,7 +87,7 @@ export class DeviceAPIRouter {
     global.tizen.application.getAppInfo = (app_id?: string) => {
       console.log(`Routing - getAppInfo()`);
       if (!app_id)
-        app_id = this.GetServiceId();
+        app_id = this.getServiceId();
       return this.funcGetAppInfo(app_id);
     }
     // tizen.application.getAppCerts()
@@ -58,7 +95,7 @@ export class DeviceAPIRouter {
     global.tizen.application.getAppCerts = (app_id?: string) => {
       console.log(`Routing - getAppCerts()`);
       if (!app_id)
-        app_id = this.GetServiceId();
+        app_id = this.getServiceId();
       return this.funcGetAppcerts(app_id);
     }
     // tizen.application.getAppSharedURI()
@@ -66,7 +103,7 @@ export class DeviceAPIRouter {
     global.tizen.application.getAppSharedURI = (app_id?: string) => {
       console.log(`Routing - getAppSharedURI()`);
       if (!app_id)
-        app_id = this.GetServiceId();
+        app_id = this.getServiceId();
       return this.funcGetSharedUri(app_id);
     }
     // tizen.application.getAppMetaData()
@@ -74,31 +111,44 @@ export class DeviceAPIRouter {
     global.tizen.application.getAppMetaData = (app_id?: string) => {
       console.log(`Routing - getAppMetaData()`);
       if (!app_id)
-        app_id = this.GetServiceId();
+        app_id = this.getServiceId();
       return this.funcGetMetadata(app_id);
     }
   }
 
-  RefinePackageApis() {
+  refinePackageApis() {
     // tizen.package.getPackageInfo()
     this.funcGetPackageInfo = global.tizen.package.getPackageInfo;
     global.tizen.package.getPackageInfo = (package_id?: string) => {
       console.log(`Routing - getPackageInfo()`);
       if (!package_id)
-        package_id = this.GetPackageId();
+        package_id = this.getPackageId();
       return this.funcGetPackageInfo(package_id);
     }
   }
 
-  RefineFilesystemApis() {
-    // tizen.filesystem.resolve
-    this.funcPathResolve = global.tizen.filesystem.resolve;
-    global.tizen.filesystem.resolve = (location: string, onSuccess: Function,
-        onError?: Function, mode?: string) => {
-      console.log(`Routing - resolve(${location})`);
-      let service_id = this.GetServiceId();
-      location = wrt.resolveVirtualRoot(service_id, location);
-      this.funcPathResolve(location, onSuccess, onError, mode ?? 'rw');
+  injectVirtualRootResolver(func: Function) {
+    return (...args: any[]) => {
+      console.log(args);
+      args[0] = wrt.resolveVirtualRoot(this.getServiceId(), args[0]);
+      console.log(args[0]);
+      func.apply(global.tizen.filesystem, args);
     }
   }
+
+  refineFilesystemApis() {
+    global.tizen.filesystem.resolve = this.injectVirtualRootResolver(global.tizen.filesystem.resolve);
+    global.tizen.filesystem.listDirectory = this.injectVirtualRootResolver(global.tizen.filesystem.listDirectory);
+    global.tizen.filesystem.createDirectory = this.injectVirtualRootResolver(global.tizen.filesystem.createDirectory);
+    global.tizen.filesystem.createDirectory = this.injectVirtualRootResolver(global.tizen.filesystem.createDirectory);
+    global.tizen.filesystem.deleteDirectory = this.injectVirtualRootResolver(global.tizen.filesystem.deleteDirectory);
+    global.tizen.filesystem.openFile = this.injectVirtualRootResolver(global.tizen.filesystem.openFile);
+    global.tizen.filesystem.deleteFile = this.injectVirtualRootResolver(global.tizen.filesystem.deleteFile);
+    global.tizen.filesystem.moveFile = this.injectVirtualRootResolver(global.tizen.filesystem.moveFile);
+    global.tizen.filesystem.copyFile = this.injectVirtualRootResolver(global.tizen.filesystem.copyFile);
+    global.tizen.filesystem.isFile = this.injectVirtualRootResolver(global.tizen.filesystem.isFile);
+    global.tizen.filesystem.toURI = this.injectVirtualRootResolver(global.tizen.filesystem.toURI);
+    global.tizen.filesystem.isDirectory = this.injectVirtualRootResolver(global.tizen.filesystem.isDirectory);
+    global.tizen.filesystem.pathExists = this.injectVirtualRootResolver(global.tizen.filesystem.pathExists);
+  }
 }
index 3aa42eec3c1e44c161d0596f08aff05d84d6eb87..6fa2f41dbb553ce804d600318d5f12bb052a7e32 100755 (executable)
@@ -23,7 +23,7 @@ import * as BuiltinService from './builtins/builtin_handler';
 
 wrt.on('start-service', (event: any, internal_id: string) => {
   console.log('start service app : ' + internal_id);
-  ServiceManager.startService(internal_id);
+  ServiceManager.startService(internal_id, '');
 });
 
 wrt.on('stop-service', (event: any, internal_id: string) => {