При интеграции 1С с внешним программным обеспечением разработчики часто сталкиваются с необходимостью запуска консольных утилит (CLI). В простых сценариях достаточно запустить приложение, передать ему аргументы командной строки и дождаться завершения работы. Однако существуют более сложные задачи, когда консольное приложение работает в интерактивном режиме: оно запускается, ожидает ввода пользователя, обрабатывает команду, возвращает результат в консоль и снова переходит в режим ожидания ввода. Яркими примерами таких утилит являются средства проброса портов (например, ngrok), консольные клиенты баз данных (mysql, sqlite3) или специализированные утилиты криптозащиты.
Попытки реализовать такое двустороннее общение «на лету» средствами стандартного COM-объекта WScript.Shell часто приводят к непредсказуемому поведению: интерфейс 1С намертво зависает, команды не отправляются, а консольный вывод возвращается пустым. В этой статье мы подробно разберем физику этого процесса, выясним причины блокировок и построим надежный алгоритм интерактивного взаимодействия с консолью непосредственно из кода 1С.
Давайте разберем эту тему по шагам: от теории межпроцессного взаимодействия до практических примеров кода и альтернативных современных технологий.
Для начала проанализируем, как операционная система организует работу консоли. Любой консольный процесс по умолчанию имеет три стандартных потока данных (так называемые файловые дескрипторы):
Когда мы запускаем консольное приложение из 1С через объект WScript.Shell методом Exec(), операционная система перенаправляет эти потоки в специальные программные каналы (Pipes). Обмен данными начинает происходить в оперативной памяти между процессом 1С и процессом запущенной утилиты. Объект WshScriptExec, который возвращает метод Exec(), предоставляет программисту доступ к трем свойствам: StdIn, StdOut и StdErr. Все они представляют собой объекты типа TextStream.
Казалось бы, схема проста: пишем данные в StdIn, читаем ответ из StdOut. Но именно здесь начинаются технические проблемы, которые мы рассмотрим далее.
Главная причина зависания 1С при работе с потоками — это использование синхронных (блокирующих) методов чтения объекта TextStream. К ним относятся методы ReadAll(), ReadLine() и Read(КоличествоСимволов). Выясним причину их опасного поведения:
StdOut остается открытым до тех пор, пока само приложение не завершит свою работу. Вызов ReadAll() заставляет 1С бесконечно ожидать закрытия программы, из-за чего поток выполнения платформы полностью блокируется.\n или \r\n). Если консольная утилита вывела приглашение к вводу вида Введите число: или Password: без последующего перевода строки, метод ReadLine() заблокирует 1С навсегда в ожидании несуществующего символа новой строки.StdOut больше данных, чем вмещает буфер, и эти данные не будут своевременно прочитаны со стороны 1С, операционная система приостановит выполнение консольной утилиты. Приложение будет ждать освобождения буфера вывода, а 1С в этот момент может ожидать ответа в StdIn. Возникает взаимная блокировка процессов (Deadlock).Чтобы организовать стабильный обмен данными и не заблокировать платформу, мы должны соблюдать два золотых правила:
ReadAll() для интерактивных процессов.AtEndOfStream и посимвольного буфера.Рассмотрим рабочий пример кода, демонстрирующий запуск консольной утилиты (в данном случае гипотетического интерактивного приложения 55.exe, которое принимает текст и выводит результат) и пошаговый обмен командами:
// Шаг 1. Инициализируем системную оболочку Windows
WshShell = Новый COMОбъект("WScript.Shell");
// Шаг 2. Запускаем консольное приложение через командный интерпретатор CMD
// Ключ /C указывает закрыть окно CMD после завершения утилиты.
// Обратите внимание на экранирование кавычек для путей с пробелами.
ПутьКПриложению = "D:\55.exe";
WshExec = WshShell.Exec("cmd.exe /C """ + ПутьКПриложению + """");
// Даем процессу долю секунды на инициализацию
Предупреждение("Инициализация процесса...", 1);
// Шаг 3. Получаем доступ к потокам ввода-вывода
InStream = WshExec.StdIn;
OutStream = WshExec.StdOut;
ErrStream = WshExec.StdErr;
// Шаг 4. Отправляем первую команду в поток ввода приложения
КомандаДляОтправки = "same text zzzz";
InStream.WriteLine(КомандаДляОтправки);
// Шаг 5. Неблокирующее чтение ответа из потока StdOut
// Мы будем читать поток посимвольно только тогда, когда там физически есть данные.
РезультатВывода = "";
// Организуем цикл чтения, пока не достигнут конец потока или пока процесс активен
Пока Не OutStream.AtEndOfStream Цикл
// Считываем ровно один символ, чтобы избежать блокировки строки
Символ = OutStream.Read(1);
РезультатВывода = РезультатВывода + Символ;
// В реальном сценарии здесь должен быть анализ полученных данных.
// Например, если мы встретили ожидаемый маркер конца вывода (символ ">" или ":"),
// мы можем прервать чтение и подготовить следующую команду.
Если Символ = ":" Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Сообщить("Консоль вернула: " + РезультатВывода);
В приведенном кодовой схеме мы уходим от зависания благодаря посимвольному чтению Read(1). Если нам нужно прочитать конкретную строку, мы также можем использовать свойство AtEndOfLine для построчного разбора, что исключает блокировку выполнения при отсутствии символа конца файла.
Часто при чтении потока StdOut в 1С вместо русских букв отображаются непонятные символы («кракозябры»). Выясним причину этой проблемы: консоль Windows по умолчанию работает в кодировке DOS (для русской локализации это кодовая страница CP866), а платформа 1С ожидает строки в кодировке Unicode (UTF-16). Чтобы исправить кодировку, у нас есть два пути:
Мы можем принудительно перевести сессию командной строки в режим UTF-8 (кодовая страница 65001) или Windows-1251 (кодовая страница 1251). Для этого запуск утилиты нужно объединить со сменой кодовой страницы с помощью оператора && в команде вызова:
// Переводим консоль в UTF-8 перед запуском нашего приложения
КомандаЗапуска = "cmd.exe /C ""chcp 65001 && D:\55.exe""";
WshExec = WshShell.Exec(КомандаЗапуска);
Если изменить кодировку самой консоли не удается, мы можем прочитать данные как бинарный поток и перекодировать их с помощью стандартного COM-объекта ADODB.Stream. Посмотрим на пример функции для преобразования кодировки из CP866 в UTF-8:
Функция ДекодироватьСтрокуИзCP866(ИсходнаяСтрока)
// Создаем объект потока ADO
AdodbStream = Новый COMОбъект("ADODB.Stream");
AdodbStream.Type = 2; // Текстовый тип данных
AdodbStream.Mode = 3; // Доступ на чтение и запись
AdodbStream.Open();
AdodbStream.Charset = "cp866"; // Указываем исходную кодировку консоли
AdodbStream.WriteText(ИсходнаяСтрока);
// Перематываем поток в начало
AdodbStream.Position = 0;
// Меняем целевую кодировку для чтения платформой 1С
AdodbStream.Charset = "utf-8";
Результат = AdodbStream.ReadText();
AdodbStream.Close();
Возврат Результат;
КонецФункции
Использование WScript.Shell через механизм COM имеет один огромный недостаток — привязку исключительно к операционной системе Windows. Если ваша база 1С работает на сервере под управлением Linux, данный код вызовет ошибку инициализации COM-объекта. Рассмотрим современные архитектурные решения, позволяющие обойти эти ограничения.
В современной экосистеме 1С для автоматизации администрирования и написания скриптов поставляется специализированный инструмент — 1С:Исполнитель. В нем работа с внешними процессами реализована на кроссплатформенном уровне с использованием асинхронных неблокирующих потоков.
Посмотрим, как выглядит работа с интерактивным процессом в синтаксисе 1С:Исполнителя:
// Создаем описание запускаемого процесса
Перем ПараметрыЗапуска = Новый ОписаниеЗапускаПроцесса("D:\55.exe");
ПараметрыЗапуска.ПеренаправитьСтандартныйВвод = Истина;
ПараметрыЗапуска.ПеренаправитьСтандартныйВывод = Истина;
// Запускаем процесс ОС
Перем Процесс = ПроцессыОс.Запустить(ПараметрыЗапуска);
// Получаем потоки для интерактивной работы
Перем ПотокЗаписи = Процесс.СтандартныйВвод;
Перем ПотокЧтения = Процесс.СтандартныйВывод;
// Записываем данные в поток ввода приложения
ПотокЗаписи.ЗаписатьСтроку("Новая команда");
ПотокЗаписи.СброситьБуфер(); // Выталкиваем данные из буфера в pipe
// Читаем ответ построчно без зависания основного интерфейса
Перем Ответ = ПотокЧтения.ПрочитатьСтроку();
Для создания консольных сценариев на языке 1С активно применяется открытый проект OneScript. В его стандартной библиотеке реализован класс Процесс, который лишен недостатков буферизации COM-объектов Windows и работает как на Windows, так и на Linux.
Процесс = Новый Процесс("D:\55.exe");
Процесс.Запустить();
// Пишем в стандартный ввод процесса
Процесс.ЗаписатьСтроку("Привет, консоль!");
// Считываем результат
Ответ = Процесс.ПрочитатьСтроку();
Для масштабных и отказоустойчивых решений в высоконагруженных базах 1С лучшим подходом является разработка небольшой внешней компоненты Native API (например, на языках C++, C# или Go). Компонента запускает консольную утилиту в параллельном системном потоке ОС и по мере появления новых данных в StdOut генерирует в 1С Внешнее событие.
В этом случае разработчику 1С не нужно заботиться о циклах чтения и проверках AtEndOfStream. Вся обработка сводится к стандартной предопределенной процедуре модуля формы 1С:
&НаКлиенте
Процедура ОбработкаВнешнегоСобытия(Источник, Событие, Данные)
Если Источник = "КонсольнаяКомпонента" И Событие = "НовыеДанныеStdOut" Тогда
// Обрабатываем полученный из консоли текст, пришедший асинхронно
Сообщить("Получено из консоли: " + Данные);
КонецЕсли;
КонецПроцедуры
Интеграция 1С с интерактивными консольными утилитами — это решаемая задача, требующая аккуратного отношения к потокам данных ОС. При реализации такого функционала придерживайтесь следующих правил:
ReadAll().StdOut, иначе ОС приостановит его работу.WScript.Shell и неблокирующим чтением.