[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 330e441..80f1ec6 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 0a00ac5..120fe1c 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 a64c549..b7f645b 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 3aa42ee..6fa2f41 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) => {