【问题标题】:Streaming Standard Output of a console application to an ASP.NET MVC View将控制台应用程序的标准输出流式传输到 ASP.NET MVC 视图
【发布时间】:2012-01-18 16:41:51
【问题描述】:

我想做的是:

1) 从 MVC 视图中,启动一个长时间运行的进程。就我而言,这个过程是一个单独的控制台应用程序正在执行。控制台应用程序可能会运行 30 分钟,并定期控制台。写入其当前操作。

2) 返回 MVC 视图,定期轮询服务器以检索我已重定向到 Stream(或任何我可以访问它的地方)的最新标准输出。我会将新检索的标准输出附加到日志文本框或类似的东西。

听起来相对容易。虽然我的客户端编程有点生疏,但我在实际流式传输时遇到了问题。我认为这不是一个不寻常的任务。有人在 ASP.NET MVC 中找到了一个不错的解决方案吗?

最大的问题似乎是在执行结束之前我无法获得 StandardOutput,但我能够通过事件处理程序获得它。当然,使用事件处理程序似乎失去了我的输出焦点。

这就是我目前为止的工作......

    public ActionResult ProcessImport()
    {
        // Get the file path of your Application (exe)
        var importApplicationFilePath = ConfigurationManager.AppSettings["ImportApplicationFilePath"];

        var info = new ProcessStartInfo
        {
            FileName = importApplicationFilePath,
            RedirectStandardError = true,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            CreateNoWindow = true,
            WindowStyle =  ProcessWindowStyle.Hidden,
            UseShellExecute = false
        };

        _process = Process.Start(info);
        _process.BeginOutputReadLine();

        _process.OutputDataReceived += new DataReceivedEventHandler(_process_OutputDataReceived);

        _process.WaitForExit(1);

        Session["pid"] = _process.Id;

        return Json(new { success = true }, JsonRequestBehavior.AllowGet);
    }

    void _process_OutputDataReceived(object sender, DataReceivedEventArgs e)
    {
        _importStandardOutputBuilder.Insert(0, e.Data);
    }

    public ActionResult Update()
    {
        //var pid = (int)Session["pid"];
        //_process = Process.GetProcessById(pid);

        var newOutput = _importStandardOutputBuilder.ToString();
        _importStandardOutputBuilder.Clear();

        //return View("Index", new { Text = _process.StandardOutput.ReadToEnd() });
        return Json(new { output = newOutput }, "text/html");
    }

我还没有编写客户端代码,因为我只是点击 URL 来测试操作,但我也很感兴趣您将如何处理此文本的轮询。如果您也可以为此提供实际代码,那就太好了。我假设您在启动将使用 ajax 调用返回 JSON 结果的服务器的进程后运行一个 js 循环......但同样,它不是我的强项,所以很想看看它是如何完成的。

谢谢!

【问题讨论】:

    标签: asp.net-mvc ajax json stream redirectstandardoutput


    【解决方案1】:

    查看长民意调查。基本上你可以打开一个 ajax 请求,然后在控制器中保持它。

    Sample of long poll

    这是您想要执行异步的事情,否则您可能会遇到线程不足的问题。

    【讨论】:

    • 酷。我会检查一下。我还没有任何使用 AsyncControllers 的经验。他们的示例看起来就像我想要的一样......但我仍然需要看看我是否可以在控制台应用程序处理时获得 StandardOut。
    • 我会在网站上托管一个端点,控制台应用程序将其输出传送到该端点。然后可以将其推送到一个可观察的集合中,该集合可以将输出推送到长轮询客户端。
    【解决方案2】:

    考虑编写一个在某处服务器上运行的服务,并将其输出通过管道传输到您的 Web 服务器可访问的文件/数据库。然后,您可以将生成的数据加载到您的网站中并将它们返回给您的调用者。

    了解长时间占用您的网络服务器的线程可能会导致线程匮乏,并使您的网站看起来像是崩溃了(即使它实际上只是忙于等待您的控制台应用运行)。

    【讨论】:

      【解决方案3】:

      好的,所以根据我收到的一些建议和大量的反复试验,我想出了一个正在进行中的解决方案,并认为我应该与大家分享。目前它肯定存在潜在问题,因为它依赖于整个网站共享的静态变量,但根据我的要求,它做得很好。来了!

      让我们从我的观点开始。我们首先将我的按钮的单击事件与一些 jquery 绑定,该 jquery 向 /Upload/ProcessImport 发布信息(Upload 是我的 MVC 控制器,ProcessImport 是我的 MVC 操作)。流程导入启动我的流程,我将在下面详细说明。然后 js 在调用 js 函数 getMessages 之前等待一小段时间(使用 setTimeout)。

      所以 getMessages 在单击按钮后被调用,它会发布到 /Upload/Update (我的更新操作)。 Update 操作基本上检索 Process 的状态并返回它以及自上次调用 Update 以来的 StandardOutput。然后 getMessages 将解析 JSON 结果并将 StandardOutput 附加到我视图中的列表中。我也尝试滚动到列表的底部,但这并不完美。最后,getMessages 检查进程是否完成,如果没有完成,它将每秒递归调用自身,直到完成为止。

      <script type="text/javascript">
      
          function getMessages() {
      
              $.post("/Upload/Update", null, function (data, s) {
      
                  if (data) {
                      var obj = jQuery.parseJSON(data);
      
                      $("#processOutputList").append('<li>' + obj.message + '</li>');
                      $('#processOutputList').animate({
                          scrollTop: $('#processOutputList').get(0).scrollHeight
                      }, 500);
                  }
      
                  // Recurivly call itself until process finishes
                  if (!obj.processExited) {
                      setTimeout(function () {
                          getMessages();
                      }, 1000)
                  }
              });
          }
      
          $(document).ready(function () {
      
              // bind importButton click to run import and then poll for messages
              $('#importButton').bind('click', function () {
                  // Call ProcessImport
                  $.post("/Upload/ProcessImport", {}, function () { });
      
                  // TODO: disable inputs
      
                  // Run's getMessages after waiting the specified time
                  setTimeout(function () {
                      getMessages();
                  }, 500)
              });
      
          });
      
      </script>
      
      <h2>Upload</h2>
      
      <p style="padding: 20px;">
          Description of the upload process and any warnings or important information here.
      </p>
      
      <div style="padding: 20px;">
      
          <div id="importButton" class="qq-upload-button">Process files</div>
      
          <div id="processOutput">
              <ul id="processOutputList" 
                  style="list-style-type: none; margin: 20px 0px 10px 0px; max-height: 500px; min-height: 500px; overflow: auto;">
              </ul>
          </div>
      
      </div>
      

      控制器。我选择不使用 AsyncController,主要是因为我发现我不需要。我最初的问题是将我的控制台应用程序的 StdOut 管道传输到视图。我发现无法 ReadToEnd 的标准输出,所以改为挂钩事件处理程序 ProcessOutputDataReceived,当接收到标准输出数据时触发该事件处理程序,然后使用 StringBuilder,将输出附加到先前接收到的输出。这种方法的问题是控制器在每一篇文章中都会被重新实例化,为了克服这个问题,我决定让应用程序的 Process 和 StringBuilder 成为静态的。这允许我随后接收对更新操作的调用,获取静态 StringBuilder 并将其内容有效地刷新回我的视图。我还将一个布尔值发送回视图,指示进程是否已退出,以便视图在知道这一点时可以停止轮询。此外,由于是静态的,我试图确保如果正在进行导入,则不允许其他人开始。

      public class UploadController : Controller
      {
          private static Process _process;
          private static StringBuilder _importStandardOutputBuilder;
      
          public UploadController()
          {
              if(_importStandardOutputBuilder == null)
                  _importStandardOutputBuilder = new StringBuilder();
          }
      
          public ActionResult Index()
          {
              ViewData["Title"] = "Upload";
              return View("UploadView");
          }
      
          //[HttpPost]
          public ActionResult ProcessImport()
          {
              // Validate that process is not running
              if (_process != null && !_process.HasExited)
                  return Json(new { success = false, message = "An Import Process is already in progress. Only one Import can occur at any one time." }, "text/html");
      
              // Get the file path of your Application (exe)
              var importApplicationFilePath = ConfigurationManager.AppSettings["ImportApplicationFilePath"];
      
              var info = new ProcessStartInfo
              {
                  FileName = importApplicationFilePath,
                  RedirectStandardError = true,
                  RedirectStandardInput = true,
                  RedirectStandardOutput = true,
                  CreateNoWindow = true,
                  WindowStyle =  ProcessWindowStyle.Hidden,
                  UseShellExecute = false
              };
      
              _process = Process.Start(info);
              _process.BeginOutputReadLine();
              _process.OutputDataReceived += ProcessOutputDataReceived;
              _process.WaitForExit(1);
      
              return Json(new { success = true }, JsonRequestBehavior.AllowGet);
          }
      
          static void ProcessOutputDataReceived(object sender, DataReceivedEventArgs e)
          {
              _importStandardOutputBuilder.Append(String.Format("{0}{1}", e.Data, "</br>"));
          }
      
          public ActionResult Update()
          {
              var newOutput = _importStandardOutputBuilder.ToString();
              _importStandardOutputBuilder.Clear();
              return Json(new { message = newOutput, processExited = _process.HasExited }, "text/html");
          }
      }
      

      嗯,到此为止。有用。它仍然需要工作,所以希望我能在完善我的解决方案时更新这个解决方案。您对静态方法有什么看法(假设业务规则是任何时候只能进行一次导入)?

      【讨论】:

      • 这是一个很好的基础解决方案。我在自己的代码中将它用作跳板,在那里我必须解决你所说的“静态方法”,因为我不得不假设我的网站被多个用户同时使用,并且每个用户都可以运行一个进程时间。所以控制器中的静态进程对象不会为我做。因此,我围绕 Process 类编写了一个包装器,我以松散耦合的方式将其放入用户会话状态(参见josephwoodward.co.uk/2014/06/…)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-03
      • 1970-01-01
      • 1970-01-01
      • 2013-02-13
      相关资源
      最近更新 更多