При разработке и поддержке корпоративных информационных систем на платформе 1С:Предприятие программисты часто сталкиваются с жесткими политиками безопасности. Типичная ситуация: пользователям на удаленных рабочих столах (RDP) или локальных ПК запрещено вносить изменения в каталог Program Files, регистрировать библиотеки в системном реестре (для COM-объектов) и выполнять любые действия под учетной записью администратора. В таких условиях обновление самописной внешней компоненты (ВК), особенно если она взаимодействует со сторонними консольными утилитами (EXE-файлами), превращается в нетривиальную задачу.
В этой статье мы подробно разберем, как реализовать механизм бесшовного обновления и подключения внешних компонент типа NativeAPI без участия системных администраторов, проанализируем ошибки в коде инициализации, изучим особенности системных каталогов Windows и предложим надежные архитектурные решения.
Для начала проанализируем листинг кода, который часто используется для подключения компоненты из макета. В оригинальном примере автора темы присутствует критическая логическая ошибка, из-за которой внешняя компонента физически не может получить параметры (в частности, путь к исполняемому файлу):
Макет = ПолучитьМакет("ВК");
Адрес = ПоместитьВоВременноеХранилище(Макет);
ПодключениеВыполнено = ПодключитьВнешнююКомпоненту(Адрес, "VK", ТипВнешнейКомпоненты.Native );
Если ПодключениеВыполнено Тогда
мОбъектАндроид = Новый("AddIn.VK.VKExtention");
мОбъектВК.РабочаяПапка = ПутьКЕХЕ; // Ошибка: обращение к неопределенной переменной!
СообщениеИнф("Подключение библиотеки ВК v" + мОбъектВК.Версия);
Возврат мОбъектВК;
Иначе
Возврат Неопределено;
КонецЕсли;
Выясним причину сбоя: при создании объекта компоненты экземпляр присваивается переменной мОбъектАндроид, однако последующая настройка свойств (РабочаяПапка) и чтение версии происходят через переменную мОбъектВК. Если в модуле не объявлена глобальная переменная с таким именем, платформа вызовет ошибку времени выполнения (Runtime Error), либо свойства просто не будут применены к вновь созданному объекту. В результате компонента не сможет обнаружить внешний EXE-файл, так как путь к нему останется пустым.
Исправим этот код и приведем его к правильному, отказоустойчивому виду:
// Безопасное подключение и инициализация внешней компоненты
МакетКомпоненты = ПолучитьМакет("ВК_NativeAPI");
АдресВременногоХранилища = ПоместитьВоВременноеХранилище(МакетКомпоненты);
ИмяКомпонентыНаДиске = "VK";
ПодключениеВыполнено = ПодключитьВнешнююКомпоненту(АдресВременногоХранилища, ИмяКомпонентыНаДиске, ТипВнешнейКомпоненты.Native);
Если ПодключениеВыполнено Тогда
Попытка
// Создаем экземпляр объекта компоненты с корректным именем переменной
НовыйОбъектВК = Новый("AddIn." + ИмяКомпонентыНаДиске + ".VKExtention");
// Передаем рабочий каталог для запуска стороннего EXE
НовыйОбъектВК.РабочаяПапка = ПутьКЕХЕ;
ВызватьИсключениеОбновления = Ложь;
// Проверяем версию компоненты при необходимости
Если НовыйОбъектВК.Версия < МинимальноНеобходимаяВерсия Тогда
ВызватьИсключениеОбновления = Истина;
КонецЕсли;
Если Не ВызватьИсключениеОбновления Тогда
Возврат НовыйОбъектВК;
КонецЕсли;
Исключение
ЗаписатьЛогСобытий("Ошибка инициализации объекта ВК: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
Иначе
ЗаписатьЛогСобытий("Не удалось подключить внешнюю компоненту из временного хранилища.");
Возврат Неопределено;
КонецЕсли;
Многие разработчики ошибочно полагают, что для работы внешних компонент обязательно требуется доступ к каталогу установки платформы в Program Files или регистрация библиотек в реестре. На самом деле платформа 1С:Предприятие имеет продуманный встроенный механизм для работы в условиях ограниченных прав пользователей.
Рассмотрим подробнее, как это функционирует изнутри:
УстановитьВнешнююКомпоненту() или ПодключитьВнешнююКомпоненту() платформа не пытается писать данные в системные защищенные папки. Вместо этого файлы распаковываются в подкаталоги личного профиля текущего пользователя операционной системы. Обычно это пути вида:
%APPDATA%\1C\1Cv8\ExtCompT\ (для современных версий платформы 8.3)%LOCALAPPDATA%\Temp\1Cv8\ (для временных файлов сессии)%USERPROFILE%), операционная система по умолчанию предоставляет текущему пользователю полные права на чтение, запись и изменение файлов в них. Никаких прав локального администратора не требуется.
MANIFEST.XML, платформа 1С берет на себя задачу контроля версий. При вызове метода УстановитьВнешнююКомпоненту() платформа сравнивает версию компоненты, хранящуюся в базе данных (в макете), с версией файла, находящегося на диске компьютера пользователя. Если версия в макете новее, 1С автоматически перезапишет DLL-файл в каталоге %APPDATA% «на лету», освобождая старую библиотеку из оперативной памяти процесса.Если синтаксические ошибки в коде 1С устранены, но компонента все равно выдает ошибку о невозможности найти или запустить внешний исполняемый файл, следует проанализировать инфраструктурные факторы:
.exe, .bat, .cmd) из папок профиля пользователя (включая %TEMP% и %APPDATA%). Это делается для защиты от шифровальщиков и вирусов. Если такая политика активна, операционная система заблокирует запуск сторонней утилиты вашей компонентой, вернув ошибку "Доступ запрещен" или "Файл не найден".C:\Windows\System32), ОС неявно перенаправит запрос в каталог C:\Windows\SysWOW64. Если исполняемый файл расположен в реальной системной папке, 32-битная DLL его просто не обнаружит.Чтобы полностью исключить человеческий фактор и необходимость предварительной подготовки рабочих мест (ручного копирования EXE-файлов на каждый компьютер), рекомендуется использовать концепцию "Единого архива". Разберем этот подход по шагам:
<?xml version="1.0" encoding="UTF-8"?>
<bundle xmlns="http://v8.1c.ru/8.2/addin/bundle">
<component id="MyCustomComponent" type="native" path="MyComponentDLL.dll" os="Windows" x86="true" x64="true" />
</bundle>
УстановитьВнешнююКомпоненту() платформа 1С автоматически распакует в подкаталог папки %APPDATA% абсолютно все файлы, содержащиеся в ZIP-архиве. Таким образом, и DLL, и нужный EXE-файл гарантированно окажутся в одной и той же папке на диске пользователя.GetModuleFileName, передав в качестве дескриптора модуля адрес загруженной библиотеки. Получив этот путь, компонента может запустить EXE-файл, лежащий в этой же папке, по абсолютному пути, полностью избавив код 1С от необходимости передавать параметр РабочаяПапка.Если единственная цель создания внешней компоненты — это запуск консольной утилиты и перехват текста, который она выводит в консоль (stdout/stderr), писать собственную C++ библиотеку вообще не требуется. Эту задачу можно решить стандартными средствами 1С:Предприятия, которые гарантированно работают без прав администратора.
Проанализируем пример реализации с использованием перенаправления потоков ввода-вывода операционной системы:
// Функция запускает консольное приложение и возвращает его вывод в виде строки
Функция ПолучитьКонсольныйВыводПриложения(ПутьКПрограмме, ПараметрыЗапуска) Экспорт
// Создаем уникальные имена файлов во временном каталоге пользователя
ИмяВременногоФайлаВывода = ПолучитьИмяВременногоФайла("txt");
ИмяФайлаСкрипта = ПолучитьИмяВременногоФайла("bat");
// Формируем команду с перенаправлением вывода в текстовый файл
// Символ ">" перенаправляет стандартный вывод (stdout)
// Символы "2>&1" объединяют вывод ошибок (stderr) с основным потоком
ТекстКоманды = """" + ПутьКПрограмме + """ " + ПараметрыЗапуска + " > """ + ИмяВременногоФайлаВывода + """ 2>&1";
// Записываем команду в bat-файл для корректной обработки кириллицы и кавычек в путях
ЗаписьТекста = Новый ЗаписьТекста(ИмяФайлаСкрипта, КодировкаТекста.OEM);
ЗаписьТекста.ЗаписатьСтроку(ТекстКоманды);
ЗаписьТекста.Закрыть();
// Запускаем cmd-процесс в скрытом режиме и ожидаем завершения его работы
ПараметрыЗапускаПриложения = Новый ОписаниеЗапускаПриложения;
ПараметрыЗапускаПриложения.ТекущийКаталог = КаталогВременныхФайлов();
ПараметрыЗапускаПриложения.ДождатьсяЗавершения = Истина;
ПараметрыЗапускаПриложения.РежимЗапускаОкна = РежимЗапускаОкнаПриложения.Скрытое;
ЗапуститьПриложение("cmd.exe /c """ + ИмяФайлаСкрипта + """", ПараметрыЗапускаПриложения);
// Считываем результат работы утилиты
ПотокЧтения = Новый ТекстовыйДокумент;
ПотокЧтения.Прочитать(ИмяВременногоФайлаВывода, КодировкаТекста.OEM);
РезультатВывода = ПотокЧтения.ПолучитьТекст();
// Производим очистку временных файлов на диске
Попытка
УдалитьФайлы(ИмяФайлаСкрипта);
УдалитьФайлы(ИмяВременногоФайлаВывода);
Исключение
// Игнорируем ошибки удаления, если файлы заблокированы ОС
КонецПопытки;
Возврат РезультатВывода;
КонецФункции
Данный подход является кроссплатформенным, не требует регистрации сторонних библиотек и полностью изолирован внутри временных папок пользователя, что позволяет обходить ограничения на запись в Program Files.