Потоковый стандартный вывод консольного приложения в ASP.NET MVC View

Что я хочу сделать, это:

1) В представлении MVC запустите длительный процесс. В моем случае этот процесс является отдельным выполняемым консольным приложением. Консольное приложение запускается потенциально 30 минут и регулярно Console.Write его текущие действия.

2) Вернитесь в MVC View, периодически опрашивайте сервер, чтобы получить последнюю версию Standard Out, которую я перенаправил в поток (или где-нибудь, где я могу получить к нему доступ). Я добавлю вновь выведенный стандартный вывод в текстовое поле журнала или что-то подобное.

Звучит относительно легко. Мои программы на стороне клиента немного ржавые, и у меня возникают проблемы с потоковым вещанием. Я предполагаю, что это не редкая задача. Кто-нибудь получил достойное решение для этого в 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, чтобы протестировать действия, но мне также интересно, как вы подходите к опросу для этого текста. Если бы вы могли предоставить реальный код для этого тоже, было бы здорово. Я бы предположил, что у вас будет запущен цикл js после запуска процесса, который будет использовать ajax-вызовы к серверу, который возвращает результаты JSON... но, опять же, это не моя сильная сторона, поэтому хотелось бы посмотреть, как это делается.

Спасибо!

3 ответа

Решение

Правильно, поэтому из пары полученных мной предложений и большого количества проб и ошибок я нашел решение в процессе разработки и подумал, что должен поделиться со всеми вами. В настоящий момент, безусловно, существуют потенциальные проблемы, так как он опирается на статические переменные, общие для всего сайта, но, по моему требованию, хорошо справляется со своей задачей. Вот оно!

Давайте начнем с моей точки зрения. Мы начнем с привязки события click моей кнопки к некоторому jquery, который отправляет сообщение в /Upload/ProcessImport (Upload - мой MVC-контроллер, а ProcessImport - мое MVC-действие). Процесс Import запускает мой процесс, который я подробно опишу ниже. Затем js ждет короткое время (используя setTimeout) перед вызовом функции js getMessages.

Таким образом, getMessages вызывается после нажатия кнопки, и он отправляет сообщение в /Upload/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");
    }
}

Ну вот и все. Оно работает. Это все еще нуждается в работе, так что, надеюсь, я обновлю это решение, когда буду совершенствовать свое. Что вы думаете о статическом подходе (если предположить, что бизнес-правило заключается в том, что одновременно может выполняться только один импорт)?

Посмотри на длинный опрос. По сути, вы можете открыть запрос ajax и затем удерживать его внутри контроллера.

Образец длительного опроса

Это то, что вы хотите сделать Async или у вас могут возникнуть проблемы с истощением потока.

Подумайте о том, чтобы написать службу, которая работает где-нибудь на сервере и передает ее вывод в файл /db, доступный вашему веб-серверу. Затем вы можете просто загрузить сгенерированные данные на свой веб-сайт и вернуть их своему абоненту.

Поймите, что связывание потоков вашего веб-сервера в течение длительных периодов времени может привести к истощению потоков и создать впечатление, что ваш сайт потерпел крах (даже если он просто занят ожиданием запуска вашего консольного приложения).

Другие вопросы по тегам