Потоковый стандартный вывод консольного приложения в ASP.NET MVC View - PullRequest
3 голосов
/ 12 декабря 2011

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

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

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

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

4 голосов
/ 14 декабря 2011

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

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

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

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

1 голос
/ 12 декабря 2011

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

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

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

0 голосов
/ 13 декабря 2011

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

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

...