meteor/meteor

View on GitHub
tools/static-assets/server/boot.js

Summary

Maintainability
D
1 day
Test Coverage
var Fiber = require("fibers");
var fs = require("fs");
var path = require("path");
var Future = require("fibers/future");
var sourcemap_support = require('source-map-support');

var bootUtils = require('./boot-utils.js');
var files = require('./mini-files');
var npmRequire = require('./npm-require.js').require;
var Profile = require('./profile').Profile;

// This code is duplicated in tools/main.js.
var MIN_NODE_VERSION = 'v14.0.0';

var hasOwn = Object.prototype.hasOwnProperty;

//  For now it's a function to ensure we don't get a falsy value.
//  Once we figure out the best place to create this EV (maybe it's here),
//  it won't need to be a function anymore.

global._isFibersEnabled = function () {
  return !process.env.DISABLE_FIBERS;
};

if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
  process.stderr.write(
    'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n');
  process.exit(1);
}

// read our control files
var serverJsonPath = path.resolve(process.argv[2]);
var serverDir = path.dirname(serverJsonPath);
var serverJson = require("./server-json.js");
var configJson =
  JSON.parse(fs.readFileSync(path.resolve(serverDir, 'config.json'), 'utf8'));

var programsDir = path.dirname(serverDir);
var buildDir = path.dirname(programsDir);
var starJson = JSON.parse(fs.readFileSync(path.join(buildDir, "star.json")));

// Set up environment
__meteor_bootstrap__ = {
  startupHooks: [],
  serverDir: serverDir,
  configJson: configJson
};

__meteor_runtime_config__ = {
  meteorRelease: configJson.meteorRelease,
  gitCommitHash: starJson.gitCommitHash
};

if (!process.env.APP_ID) {
  process.env.APP_ID = configJson.appId;
}

// Map from load path to its source map.
var parsedSourceMaps = {};

const meteorDebugFuture =
  process.env.METEOR_INSPECT_BRK ? new Future : null;

function maybeWaitForDebuggerToAttach() {
  if (meteorDebugFuture) {
    const { pause } = require("./debug");
    const pauseThresholdMs = 50;
    const pollIntervalMs = 500;
    const waitStartTimeMs = +new Date;
    const waitLimitMinutes = 5;
    const waitLimitMs = waitLimitMinutes * 60 * 1000;

    // This setTimeout not only waits for the debugger to attach, but also
    // keeps the process alive by preventing the event loop from running
    // empty while the main Fiber yields.
    setTimeout(function poll() {
      const pauseStartTimeMs = +new Date;

      if (pauseStartTimeMs - waitStartTimeMs > waitLimitMs) {
        console.error(
          `Debugger did not attach after ${waitLimitMinutes} minutes; continuing.`
        );

        meteorDebugFuture.return();

      } else {
        // This pause function contains a debugger keyword that will only
        // act as a breakpoint once a debugging client has attached to the
        // process, so we keep calling pause() until the first time it
        // takes at least pauseThresholdMs, which indicates that a client
        // must be attached. The only other signal of a client attaching
        // is an unreliable "Debugger attached" message printed to stderr
        // by native C++ code, which requires the parent process to listen
        // for that message and then process.send a message back to this
        // process. By comparison, this polling strategy tells us exactly
        // what we want to know: "Is the debugger keyword enabled yet?"
        pause();

        if (new Date - pauseStartTimeMs > pauseThresholdMs) {
          // If the pause() function call took a meaningful amount of
          // time, we can conclude the debugger keyword must be active,
          // which means a debugging client must be connected, which means
          // we should stop polling and let the main Fiber continue.
          meteorDebugFuture.return();

        } else {
          // If the pause() function call didn't take a meaningful amount
          // of time to execute, then the debugger keyword must not have
          // caused a pause, which means a debugging client must not be
          // connected, which means we should keep polling.
          setTimeout(poll, pollIntervalMs);
        }
      }
    }, pollIntervalMs);

    // The polling will continue while we wait here.
    meteorDebugFuture.wait();
  }
}

// Read all the source maps into memory once.
serverJson.load.forEach(function (fileInfo) {
  if (fileInfo.sourceMap) {
    var rawSourceMap = fs.readFileSync(
      path.resolve(serverDir, fileInfo.sourceMap), 'utf8');
    // Parse the source map only once, not each time it's needed. Also remove
    // the anti-XSSI header if it's there.
    var parsedSourceMap = JSON.parse(rawSourceMap.replace(/^\)\]\}'/, ''));
    // source-map-support doesn't ever look at the sourcesContent field, so
    // there's no point in keeping it in memory.
    delete parsedSourceMap.sourcesContent;
    var url;
    if (fileInfo.sourceMapRoot) {
      // Add the specified root to any root that may be in the file.
      parsedSourceMap.sourceRoot = path.join(
        fileInfo.sourceMapRoot, parsedSourceMap.sourceRoot || '');
    }
    parsedSourceMaps[path.resolve(__dirname, fileInfo.path)] = parsedSourceMap;
  }
});

function retrieveSourceMap(pathForSourceMap) {
  if (hasOwn.call(parsedSourceMaps, pathForSourceMap)) {
    return { map: parsedSourceMaps[pathForSourceMap] };
  }
  return null;
}

var origWrapper = sourcemap_support.wrapCallSite;
var wrapCallSite = function (frame) {
  var frame = origWrapper(frame);
  var wrapGetter = function (name) {
    var origGetter = frame[name];
    frame[name] = function (arg) {
      // replace a custom location domain that we set for better UX in Chrome
      // DevTools (separate domain group) in source maps.
      var source = origGetter(arg);
      if (! source)
        return source;
      return source.replace(/(^|\()meteor:\/\/..app\//, '$1');
    };
  };
  wrapGetter('getScriptNameOrSourceURL');
  wrapGetter('getEvalOrigin');

  return frame;
};
sourcemap_support.install({
  // Use the source maps specified in program.json instead of parsing source
  // code for them.
  retrieveSourceMap: retrieveSourceMap,
  // For now, don't fix the source line in uncaught exceptions, because we
  // haven't fixed handleUncaughtExceptions in source-map-support to properly
  // locate the source files.
  handleUncaughtExceptions: false,
  wrapCallSite: wrapCallSite
});

// As a replacement to the old keepalives mechanism, check for a running
// parent every few seconds. Exit if the parent is not running.
//
// Two caveats to this strategy:
// * Doesn't catch the case where the parent is CPU-hogging (but maybe we
//   don't want to catch that case anyway, since the bundler not yielding
//   is what caused #2536).
// * Could be fooled by pid re-use, i.e. if another process comes up and
//   takes the parent process's place before the child process dies.
var startCheckForLiveParent = function (parentPid) {
  if (parentPid) {
    if (! bootUtils.validPid(parentPid)) {
      console.error("METEOR_PARENT_PID must be a valid process ID.");
      process.exit(1);
    }

    setInterval(function () {
      try {
        process.kill(parentPid, 0);
      } catch (err) {
        console.error("Parent process is dead! Exiting.");
        process.exit(1);
      }
    }, 3000);
  }
};

var specialArgPaths = {
  "packages/modules-runtime.js": function () {
    return {
      npmRequire: npmRequire,
      Profile: Profile
    };
  },

  "packages/dynamic-import.js": function (file) {
    var dynamicImportInfo = {};
    var clientArchs = configJson.clientArchs ||
      Object.keys(configJson.clientPaths);

    clientArchs.forEach(function (arch) {
      dynamicImportInfo[arch] = {
        dynamicRoot: path.join(programsDir, arch, "dynamic")
      };
    });

    dynamicImportInfo.server = {
      dynamicRoot: path.join(serverDir, "dynamic")
    };

    return { dynamicImportInfo: dynamicImportInfo };
  }
};

var loadServerBundles = Profile("Load server bundles", function () {
  var infos = [];

  serverJson.load.forEach(function (fileInfo) {
    var code = fs.readFileSync(path.resolve(serverDir, fileInfo.path));
    var nonLocalNodeModulesPaths = [];

    function addNodeModulesPath(path) {
      nonLocalNodeModulesPaths.push(
        files.pathResolve(serverDir, path)
      );
    }

    if (typeof fileInfo.node_modules === "string") {
      addNodeModulesPath(fileInfo.node_modules);
    } else if (fileInfo.node_modules) {
      Object.keys(fileInfo.node_modules).forEach(function (path) {
        const info = fileInfo.node_modules[path];
        if (! info.local) {
          addNodeModulesPath(path);
        }
      });
    }

    // Add dev_bundle/server-lib/node_modules.
    addNodeModulesPath("node_modules");

    function statOrNull(path) {
      try {
        return fs.statSync(path);
      } catch (e) {
        return null;
      }
    }

    var Npm = {
      /**
       * @summary Require a package that was specified using
       * `Npm.depends()`.
       * @param  {String} name The name of the package to require.
       * @locus Server
       * @memberOf Npm
       */
      require: Profile(function getBucketName(name) {
        return "Npm.require(" + JSON.stringify(name) + ")";
      }, function (name, error) {
        if (nonLocalNodeModulesPaths.length > 0) {
          var fullPath;

          // Replace all backslashes with forward slashes, just in case
          // someone passes a Windows-y module identifier.
          name = name.split("\\").join("/");

          nonLocalNodeModulesPaths.some(function (nodeModuleBase) {
            var packageBase = files.convertToOSPath(files.pathResolve(
              nodeModuleBase,
              name.split("/", 1)[0]
            ));

            if (statOrNull(packageBase)) {
              return fullPath = files.convertToOSPath(
                files.pathResolve(nodeModuleBase, name)
              );
            }
          });

          if (fullPath) {
            return require(fullPath);
          }
        }

        var resolved = require.resolve(name);
        if (resolved === name && ! path.isAbsolute(resolved)) {
          // If require.resolve(id) === id and id is not an absolute
          // identifier, it must be a built-in module like fs or http.
          return require(resolved);
        }

        throw error || new Error(
          "Cannot find module " + JSON.stringify(name)
        );
      })
    };

    var getAsset = function (assetPath, encoding, callback) {
      var promiseResolver, promise;
      if (! callback) {
        promise = new Promise((resolve, reject) => {
          promiseResolver = function (error, result) {
            error ? reject(error) : resolve(result);
          }
        });
        callback = promiseResolver;
      }
      // This assumes that we've already loaded the meteor package, so meteor
      // itself can't call Assets.get*. (We could change this function so that
      // it doesn't call bindEnvironment if you don't pass a callback if we need
      // to.)
      var _callback = Package.meteor.Meteor.bindEnvironment(function (err, result) {
        if (result && ! encoding)
          // Sadly, this copies in Node 0.10.
          result = new Uint8Array(result);
        callback(err, result);
      }, function (e) {
        console.log("Exception in callback of getAsset", e.stack);
      });

      // Convert a DOS-style path to Unix-style in case the application code was
      // written on Windows.
      assetPath = files.convertToStandardPath(assetPath);

      // Unicode normalize the asset path to prevent string mismatches when
      // using this string elsewhere.
      assetPath = files.unicodeNormalizePath(assetPath);

      if (! fileInfo.assets || ! hasOwn.call(fileInfo.assets, assetPath)) {
        _callback(new Error("Unknown asset: " + assetPath));
      } else {
        var filePath = path.join(serverDir, fileInfo.assets[assetPath]);
        fs.readFile(files.convertToOSPath(filePath), encoding, _callback);
      }

      if (promise)
        return promise;
    };

    var Assets = {
      getText: function (assetPath, callback) {
        const result = getAsset(assetPath, "utf8", callback);
        if (!callback) {
          return Future.fromPromise(result).wait();
        }
      },
      getTextAsync: function (assetPath) {
        return getAsset(assetPath, "utf8");
      },
      getBinary: function (assetPath, callback) {
        const result = getAsset(assetPath, undefined, callback);
        if (!callback) {
          return Future.fromPromise(result).wait();
        }
      },
      getBinaryAsync: function (assetPath) {
        return getAsset(assetPath, undefined);
      },
      /**
       * @summary Get the absolute path to the static server asset. Note that assets are read-only.
       * @locus Server [Not in build plugins]
       * @memberOf Assets
       * @param {String} assetPath The path of the asset, relative to the application's `private` subdirectory.
       */
      absoluteFilePath: function (assetPath) {
        // Unicode normalize the asset path to prevent string mismatches when
        // using this string elsewhere.
        assetPath = files.unicodeNormalizePath(assetPath);
        assetPath = files.convertToStandardPath(assetPath);

        if (! fileInfo.assets || ! hasOwn.call(fileInfo.assets, assetPath)) {
          throw new Error("Unknown asset: " + assetPath);
        }

        var filePath = path.join(serverDir, fileInfo.assets[assetPath]);
        return files.convertToOSPath(filePath);
      },
      getServerDir: function() {
        return serverDir;
      }
    };

    var wrapParts = ["(function(Npm,Assets"];

    var specialArgs =
      hasOwn.call(specialArgPaths, fileInfo.path) &&
      specialArgPaths[fileInfo.path](fileInfo);

    var specialKeys = Object.keys(specialArgs || {});
    specialKeys.forEach(function (key) {
      wrapParts.push("," + key);
    });

    // \n is necessary in case final line is a //-comment
    wrapParts.push("){", code, "\n})");
    var wrapped = wrapParts.join("");

    // It is safer to use the absolute path when source map is present as
    // different tooling, such as node-inspector, can get confused on relative
    // urls.

    // fileInfo.path is a standard path, convert it to OS path to join with
    // __dirname
    var fileInfoOSPath = files.convertToOSPath(fileInfo.path);
    var absoluteFilePath = path.resolve(__dirname, fileInfoOSPath);

    var scriptPath =
      parsedSourceMaps[absoluteFilePath] ? absoluteFilePath : fileInfoOSPath;

    var func = require('vm').runInThisContext(wrapped, {
      filename: scriptPath,
      displayErrors: true
    });

    var args = [Npm, Assets];

    specialKeys.forEach(function (key) {
      args.push(specialArgs[key]);
    });

    if (meteorDebugFuture) {
      infos.push({
        fn: Profile(fileInfo.path, func),
        args
      });
    } else {
      // Allows us to use code-coverage if the debugger is not enabled
      Profile(fileInfo.path, func).apply(global, args);
    }
  });

  maybeWaitForDebuggerToAttach();

  infos.forEach(info => {
    info.fn.apply(global, info.args);
  });
});

var callStartupHooks = Profile("Call Meteor.startup hooks", function () {
  // run the user startup hooks.  other calls to startup() during this can still
  // add hooks to the end.
  while (__meteor_bootstrap__.startupHooks.length) {
    var hook = __meteor_bootstrap__.startupHooks.shift();
    Profile.time(hook.stack || "(unknown)", hook);
  }
  // Setting this to null tells Meteor.startup to call hooks immediately.
  __meteor_bootstrap__.startupHooks = null;
});

var runMain = Profile("Run main()", function () {
  // find and run main()
  // XXX hack. we should know the package that contains main.
  var mains = [];
  var globalMain;
  if ('main' in global) {
    mains.push(main);
    globalMain = main;
  }
  if (typeof Package !== "undefined") {
    Object.keys(Package).forEach(function (name) {
      const { main } = Package[name];
      if (typeof main === "function" &&
          main !== globalMain) {
        mains.push(main);
      }
    });
  }
  if (! mains.length) {
    process.stderr.write("Program has no main() function.\n");
    process.exit(1);
  }
  if (mains.length > 1) {
    process.stderr.write("Program has more than one main() function?\n");
    process.exit(1);
  }
  var exitCode = mains[0].call({}, process.argv.slice(3));
  // XXX hack, needs a better way to keep alive
  if (exitCode !== 'DAEMON')
    process.exit(exitCode);

  if (process.env.METEOR_PARENT_PID) {
    startCheckForLiveParent(process.env.METEOR_PARENT_PID);
  }
});

Fiber(function () {
  Profile.run("Server startup", function () {
    loadServerBundles();
    callStartupHooks();
    runMain();
  });
}).run();