流星方法调用循环

时间:2015-01-08 11:18:23

标签: javascript android meteor

短篇小说:

如果方法执行超过120秒,Meteor方法会循环。

这是我测试的方式

服务器端有一个名为'build'的Meteor方法,如下所示:

'build': function(buildRequest) {

  log.info('method build get called: buildRequest=%s', JSON.stringify(buildRequest));

  shell.exec('echo "simulate long Android build task."');
  shell.exec('sleep ' + buildRequest.sec);

  var result = {'successful': true, 'output': "OK", 'fileName': 'test.apk', 'genFile': '/tmp/test.apk'}

  log.info("method return result=%s", JSON.stringify(result));
  return result;
}

我设置路由来调用此方法如下:

this.route('buildApp', {
  where: 'server'
  , action: function() {
    var buildRequest = this.request.query;
    log.info('buildApp: http parameters: ', buildRequest);

    var result = methods.build(buildRequest);
    var response = this.response;

    if (result.successful) {
      methods.download(response, result.genFile, result.fileName);
    }
    else {
      response.writeHead(500, {});
      response.end("server has error: " + result.output);
    }
  }
})

然后,我打电话给网址

http://localhost:3000/buildApp?app=test&server=dev&db=DB&appId=test&sec=120

然后,构建方法循环

=> App running at: http://localhost:3000/
I20150109-14:55:45.285(9)? info: buildApp: http parameters:  app=test, server=dev, db=DB, appId=test, sec=120
I20150109-14:55:45.358(9)? info: method build get called: buildRequest={"app":"test","server":"dev","db":"DB","appId":"test","sec":"120"}
I20150109-14:55:45.358(9)? simulate long Android build task.
I20150109-14:57:45.359(9)? info: method return result={"successful":true,"output":"OK","fileName":"test.apk","genFile":"/tmp/test.apk"}
I20150109-14:57:45.387(9)? info: buildApp: http parameters:  app=test, server=dev, db=DB, appId=test, sec=120
I20150109-14:57:45.387(9)? info: method build get called: buildRequest={"app":"test","server":"dev","db":"DB","appId":"test","sec":"120"}
I20150109-14:57:45.446(9)? simulate long Android build task.

我认为它与此代码相关:

https://github.com/meteor/meteor/blob/096df9d62dc9c3d560d31b546365f6bdab5a87dc/packages/webapp/webapp_server.js#L18

长篇故事:

我使用Meteor创建了一个简单的Android应用构建屏幕。 一切都运作良好但是如果我提交表单来构建应用程序,它会一遍又一遍地构建。即使我停止服务器并重新启动,只要服务器重新启动,它就会再次调用。

如果填写并提交了表单,我会调用Meteor'build'方法。 该方法将克隆git存储库并通过调用下面的shell脚本来构建应用程序。

var exec = shell.exec('./genApp.sh ' + buildRequest.appId + " " + buildRequest.server + " " + buildRequest.db);
//var exec = shell.exec('echo "simple task will not loop"');

如果我打电话给./genApp.sh(这将需要几分钟。),那么Meteor'build'方法会循环播放。但如果我做了简单的任务,它就不会循环而是执行一次。

我在构建Meteor方法的开头添加了下面的代码来阻止它进行调试。但我不知道是什么导致了这一点。

      if (a == 1) {
        a = 0;
        throw new Error("Why call again?!");
      }
      ++ a;

服务器日志:

I20150108-19:48:08.220(9)? info: success
I20150108-19:48:08.221(9)? info: return result=[object Object]
I20150108-19:48:09.034(9)? Exception while invoking method 'build' Error: Why call again?!
I20150108-19:48:09.035(9)?     at [object Object].methods.build (app/javis.js:92:25)
I20150108-19:48:09.035(9)?     at maybeAuditArgumentChecks (packages/ddp/livedata_server.js:1599:1)
I20150108-19:48:09.035(9)?     at packages/ddp/livedata_server.js:648:1
I20150108-19:48:09.035(9)?     at [object Object]._.extend.withValue (packages/meteor/dynamics_nodejs.js:56:1)
I20150108-19:48:09.035(9)?     at packages/ddp/livedata_server.js:647:1
I20150108-19:48:09.035(9)?     at [object Object]._.extend.withValue (packages/meteor/dynamics_nodejs.js:56:1)
I20150108-19:48:09.036(9)?     at [object Object]._.extend.protocol_handlers.method (packages/ddp/livedata_server.js:646:1)
I20150108-19:48:09.036(9)?     at packages/ddp/livedata_server.js:546:1

主要源代码

var APP_01 = 'app01-andy';
var APP_02 = 'app02-andy';

if (Meteor.isClient) {

  Router.configure({
    layoutTemplate: 'layout'
  })
  Router.map(function() {
    this.route('/', 'home');
  });

  buildRequest = {
    appId: 0
    , server: 'dev'
    , db: 'DEFAULT'
    , app: APP_01
  };

  Session.set('successful',  false);
  Session.set('output', '');
  Session.set('downloadLink', null);

  Template.home.helpers({
    successful: function() {
      return Session.get('successful');
    }
    , output: function() {
      return Session.get('output');
    }
    , downloadLink: function() {
      var successful = Session.get('successful');
      var downloadLink = Session.get('downloadLink');
      console.log(downloadLink);
      if (successful) {
        $('#downloadLink').show();
      } else {
        $('#downloadLink').hide();
      }
      return downloadLink;
    }
  });

  Template.home.events({
    'submit .app-build-form': function() {
      event.preventDefault();

      buildRequest.appId = event.target.appId.value;
      buildRequest.server = event.target.server.value;
      buildRequest.db = event.target.db.value;
      buildRequest.app = event.target.app.value;

      $("#submit").prop('disabled', true);

      Meteor.call('build', buildRequest, function(error, result) {
        console.log(JSON.stringify(result));
        Session.set('successful', result.successful);
        Session.set('output', result.output);
        Session.set('downloadLink', '/downloadApp?fullPathFile='+result.genFile+'&fileName='+result.fileName);
        $("#submit").prop('disabled', false);
        console.log("meteor call end");
      });

      console.log("submit finished");
      // prevent default form submit.
      return false;
    },
    'click #sel_app': function() {
      console.log(event.target.value);
      var app = event.target.value;
      var selDb = $("#sel_db");
      if (app === APP_02) {
        selDb.val('APP_02_DB'); selDb.prop('disabled', true);
      }
      else {
        selDb.prop('disabled', false);
      }
    }
  });
}

if (Meteor.isServer) {

  var shell = Meteor.npmRequire('shelljs');
  var log = Meteor.npmRequire('winston');

  var a = 0;

  var methods = {
    'build': function(buildRequest) {
      if (a == 1) {
        a = 0;
        throw new Error("Why call again?!");
      }
      ++ a;
      log.info(JSON.stringify(buildRequest));
      var dir = shell.pwd();
      log.info("work dir: %s", dir);

      shell.cd('/project/build/');

      var branch = null;
      var app = buildRequest.app;
      if (app === APP_01) {
        branch = '2.0';
      }
      else if (app === APP_02) {
        branch = '1.0';
      }
      else {
        branch = 'master';
      }
      shell.exec('rm -rf ' + buildRequest.app);
      shell.exec('git clone -b '+branch+' ssh://git@company-build.com/'+buildRequest.app+'.git');
      shell.cd(buildRequest.app + "/app");

      var exec = shell.exec('./genApp.sh ' + buildRequest.appId + " " + buildRequest.server + " " + buildRequest.db);
      //var exec = shell.exec('echo "simple task will not loop"');

      var code = exec.code;
      var output = exec.output;

      log.info(code);
      log.info(output);

      var fileName = null;
      var matches = output.match(/The package copied to (.+apk)/);
      var genFile = null;
      if (matches != null && matches.length > 1) {
        genFile = matches[1];
        log.info(genFile);
        // TODO : do not write file in public, http://stackoverflow.com/questions/13201723/generating-and-serving-static-files-with-meteor
        /*
        shell.mkdir(process.env.PWD + '/tmp');
        shell.cp('-f', genFile, process.env.PWD + '/tmp');
        */
        fileName = genFile.substring(genFile.lastIndexOf('/') + 1);
      }
      matches = output.match(/BUILD SUCCESSFUL/);
      var successful = false;
      if (matches != null && matches.length > 0) {
        log.info("success");
        successful = true;
      }

      var result = {'successful': successful, 'output': output, 'fileName': fileName, 'genFile': genFile};

      log.info("return result="+result);
      return result;
    }
    , 'download' : function(response, fullPathFile, fileName) {

      var stat = fs.statSync(fullPathFile);
      response.writeHead(200, {
        'Content-Type': 'application/vnd.android.package-archive'
        , 'Content-Length': stat.size
        , 'Content-Disposition': 'attachment; filename=' + fileName
      });
      fs.createReadStream(result.genFile).pipe(response);
    }
  };

  Meteor.methods(methods);

  fs = Npm.require('fs');

  Router.map(function() {
    this.route('buildApp', {
      where: 'server'
      , action: function() {
        var buildRequest = this.request.query;
        log.info(this.request.query);
        log.info(buildRequest);
        var result = methods.build(buildRequest);
        var response = this.response;

        if (result.successful) {
          methods.download(response, result.genFile, result.fileName);
        }
        else {
          response.writeHead(500, {});
          response.end("server has error: " + result.output);
        }
      }
    }),
    this.route('downloadApp', {
      where: 'server'
      , action: function() {
        var params = this.request.query;
        var fullPathFile = params.fullPathFile;
        var fileName = params.fileName;
        methods.download(this.response, fullPathFile, fileName);
      }
    })
  });

  Meteor.startup(function () {
    // code to run on server at startup
  });
}

导致循环的原因是什么?任何帮助将不胜感激。

即使我打电话给http://localhost:3000/buildApp?app=xxx&server=live&db=DB&appId=12423,构建循环。

如果我将./genApp.sh更改为简单条件以缩小条件,则可以。

#!/bin/bash

echo "test"
echo "The package copied to /service/release/20150108/existing-file.apk"
echo "BUILD SUCCESSFUL"

sleep 180

睡眠180秒时再次调用。什么使这个电话再次?因为我直接打电话给网址。我认为没有客户端代码重审。

2 个答案:

答案 0 :(得分:0)

嗯...当shell.exec同步运行时,它可能会使事件循环变为“#34;”,从而导致一些问题。

解决方案是使用Future允许Meteor在等待Fiber完成时继续在另一个shell.exec中运行。

var Future = Npm.require('fibers/future');

Meteor.methods({
'build': function(buildRequest) {
  var fut = new Future(), result;
  log.info('method build get called: buildRequest=%s', JSON.stringify(buildRequest));

  shell.exec('echo "simulate long Android build task."');
  shell.exec('sleep ' + buildRequest.sec, function(code, output){
    fut.return([code, output]);
  });
  result = fut.wait();
  log.info(result[0], result[1])

  var result = {'successful': true, 'output': "OK", 'fileName': 'test.apk', 'genFile': '/tmp/test.apk'}

  log.info("method return result=%s", JSON.stringify(result));
  return result;
}
});

答案 1 :(得分:0)

使这项工作成功的傻瓜方式:

  1. 创建一个集合来存储"构建请求"
  2. 设置计时器,以便在单独的光纤中偶尔处理构建请求。
  3. 通知用户"已完成"用另一个集合和出版物构建请求。
  4. 以下是一个如何构建结构的示例[未经过bug测试]:

    "use strict";
    
    var fs, shell, log, Future;
    // need a collection to store build requests & their status
    var Builds = new Mongo.Collection('builds');
    var APP_01 = 'app01-andy';
    var APP_02 = 'app02-andy';
    
    // expose routes to client & server
    Router.configure({
      layoutTemplate: 'layout'
    });
    Router.map(function() {
      this.route('/', 'home');
      this.route('downloadApp', {
        where: 'server'
        , action: function() {
          var params = this.request.query,
              buildId = params.buildId,
              build, stat;
          check(buildId, String); // or Mongo Identifier
          build = Builds.findOne({_id: buildId});
          check(build, Match.ObjectIncluding({
            file: String,
            fileName: String
          }));
          stat = fs.statSync(build.file);
          this.response.writeHead(200, {
            'Content-Type': 'application/vnd.android.package-archive'
            , 'Content-Length': stat.size
            , 'Content-Disposition': 'attachment; filename=' + build.fileName
          });
          fs.createReadStream(build.file).pipe(this.response);
        }
      });
    });
    if (Meteor.isClient) {
    
      Session.set('currentBuildId', null);
      Session.set('submittingBuildRequest', false);
    
      Tracker.autorun(function(){
        // Whenever there is a current build set, subscribe to it.
        var buildId = Session.get('currentBuildId');
        if (buildId != null){
          Meteor.subscribe('build', buildId);
        }
      });
    
      Template.home.helpers({
        // Use this helper in your template to expose the `state` property (queued, running, success, failed)
        currentBuild: function() {
          var buildId = Session.get('currentBuildId'), build;
          if (buildId == null) return null;
          build = Builds.findOne({_id: buildId});
          if (build == null) return null;
          return build;
        }
      });
    
      Template.home.events({
        'submit .app-build-form': function(e) {
          var target, buildRequest;
          e.preventDefault();
          target = e.target;
    
          buildRequest = {
            appId: target.appId.value
            , server: target.server.value
            , db: target.db.value
            , app: target.app.value
          };
          Session.set('submittingBuildRequest', true);
    
          Meteor.call('requestBuild', buildRequest, function(error, buildId) {
            Session.set('submittingBuildRequest', false);
            if (error != null){
               console.error(error);
            } else {
              console.log("buildId=", JSON.stringify(buildId));
              Session.set('currentBuildId', buildId);
            }
          });
        },
        'click #sel_app': function(e) {
          var app = e.target.value;
          var selDb = $("#sel_db");
          if (app === APP_02) {
            selDb.val('APP_02_DB'); selDb.prop('disabled', true);
          }
          else {
            selDb.prop('disabled', false);
          }
        },
        'click a.downloadCurrentBuild': function(){
          // Alternatively, could create a download url with the "pathFor" template helper
          Router.go('downloadApp', {buildId: Session.get('currentBuildId')})
        }
      });
    }
    
    if (Meteor.isServer) {
      fs = Npm.require('fs');
      shell = Meteor.npmRequire('shelljs');
      log = Meteor.npmRequire('winston');
      Future = Npm.require('fibers/future');
    
      Meteor.publish({
        'build': function(buildId){
          check(buildId, String); // or Mongo Identifier
          return Builds.find({_id: buildId}, {fields: {
            fileName: false, // don't expose real paths to client
            file: false
          }});
        }
      });
    
      Meteor.methods({
        'requestBuild': function(buildRequest){
          check(buildRequest, {
            appId: Match.Integer,
            server: String, // apply additional restrictions
            db: String, // apply additional restrictions
            app: Match.OneOf(APP_01, APP_02)
          });
    
          _.extend(buildRequest, {
            state: 'queued'
            // These properties will be set later, just keeping them here for reference
            //, output: null
            //, fileName: null
            //, file: null
          });
    
          return Builds.insert(buildRequest);
        }
      });
    
    
      var Builder = {
        // Alternative: Poll the database for new builds
        //run: function Builder_Run() {
        //  log.info('checking for "queued" builds');
        //  // may need to change this to `fetch` and then loop manually
        //  Builds.find({state: 'queued'}).forEach(Builder.processBuildRequest);
        //  Meteor.setTimeout(Builder.run, 1000); // tail call to run again (after 1 second)
        //},
        exec: function(cmd){
          // Wraps shell.exec so that they don't block the event loop
          var fut = new Future();
          shell.exec(cmd, function(code, output){
            fut.return({code: code, output: output});
          });
          return fut.wait();
        },
        processBuildRequest: function Builder_processBuildRequest(buildRequest) {
          console.log('running buildRequest=', JSON.stringify(buildRequest));
          Builds.update({_id: buildRequest._id}, {
            $set: {
              state: 'running'
            }
          });
    
          var branch = null;
          if (buildRequest.ap === APP_01) {
            branch = '2.0';
          }
          else if (buildRequest.ap === APP_02) {
            branch = '1.0';
          }
          else {
            branch = 'master';
          }
    
          shell.cd('/project/build/');
          Builder.exec('rm -rf ' + buildRequest.app);
          Builder.exec('git clone -b ' + branch + ' ssh://git@company-build.com/' + buildRequest.app + '.git');
          shell.cd(buildRequest.app + "/app");
          //var exec = Builder.exec('./genApp.sh ' + buildRequest.appId + " " + buildRequest.server + " " + buildRequest.db)
          var exec = Builder.exec('sleep 180');
          var output = exec.output;
    
          log.info("code=" + exec.code);
          log.info("output=" + output);
    
          var fileName = null;
          var matches = output.match(/The package copied to (.+apk)/);
          var file = null;
          if (matches != null && matches.length > 1) {
            file = matches[1];
            log.info("file=" + file);
            fileName = file.substring(file.lastIndexOf('/') + 1);
          }
          matches = output.match(/BUILD SUCCESSFUL/);
    
          if (matches != null && matches.length > 0) {
            log.info("success");
            Builds.update({_id: buildRequest._id}, {
              $set: {
                state: 'success',
                file: file,
                fileName: fileName,
                output: output
              }
            });
          } else {
            log.info("failed");
            Builds.update({_id: buildRequest._id}, {
              $set: {
                state: 'failed'
              }
            });
          }
        }
      };
    
    
      Meteor.startup(function () {
        // code to run on server at startup
    
        // if using polling method
        //Meteor.setTimeout(Builder.run, 1000); // will poll for new builds every second (unless already running a build)
    
        // set up an observe [to run forever] to automatically process new builds.
        Builds.find({state: 'queued'}).observe({
          added: Builder.processBuildRequest
        });
      });
    }