将控制台应用程序的标准输出流式传输到 ASP.NET MVC 视图

Posted

技术标签:

【中文标题】将控制台应用程序的标准输出流式传输到 ASP.NET MVC 视图【英文标题】:Streaming Standard Output of a console application to an ASP.NET MVC View 【发布时间】: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 循环......但同样,它不是我的强项,所以很想看看它是如何完成的。

谢谢!

【问题讨论】:

【参考方案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("01", 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/…)。

以上是关于将控制台应用程序的标准输出流式传输到 ASP.NET MVC 视图的主要内容,如果未能解决你的问题,请参考以下文章

将 excel 文件从 MVC 5 控制器流式传输到浏览器

将 ffmpeg 输出直接流式传输到调度程序

在 Python 中流式传输标准输入/标准输出

将标准输入流式传输到套接字

是否可能:使用多个线程将输出流式传输到不同的文件/

如何将 WPF 表单的内容(作为帧)流式传输到 AVI 文件?