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