Программное отключение сеансов 1С 8.2.




Принцип обмена данными из 1С с сайтом (на MySQL) и выдачи (публикации) этих данных по запросу.
PHP-Скрипт автоматической загрузки данных из файла данных в формате CSV в базу данных сайта работающего на WordPress.

В продолжение моей темы: 1С:Альфа-Авто Автосалон Автосервис: обмен с сайтом.
С помощью данного скрипта можно загружать в автоматическом режиме, по расписанию, данные сервисных книжек (ремонтов авто) из 1С:Альфа-Авто Автосалон Автосервис.
Также можно загружать данные в ручном режиме: для этого делается скрытая страница, где размещается специальная кнопка.
Комментарии размещенные внутри скрипта разъяснят логику и порядок действия.
Комментарии с "/////    echo" использовались для отладки.
Дополнительно создана таблица для журналирования результатов загрузки данных.
Скрипт включает в себя защиту от SQL инъекций (думаю безопасность соблюдена в полной мере).
В кратце:
1. Пишется скрипт, который запускает этот.
2. Создается регламентное задание в WordPress, по которому запускается скрипт из п.1. 
3. Этот скрипт осуществляет проверку на существование файла обмена в папке.
4. Если данные не новые, загрузка не производится.
5. Если данные новые, очищается таблица сервисных книжек.
6. Загружаются новые данные.

Собственно сам скрипт:

<?php // Полная загрузка сервисных книжек, создан 2026-01-05 12:44:55

global $wpdb2;
global $failure;
global $file_hist;

/////  echo '<H2><b>Старт загрузки</b></H2><br>';

$failure=FALSE;
//подключаемся к базе
$wpdb2 = include_once 'connection.php'; ; // подключаемся к MySQL
// если не удалось подключиться, и нужно оборвать PHP с сообщением об этой ошибке
if (!empty($wpdb2->error))
{
/////   echo '<H2><b>Ошибка подключения к БД, завершение.</b></H2><br>';
$failure=TRUE;
wp_die( $wpdb2->error );
}

$m_size_file=0;
$m_mtime_file=0;
$m_comment='';
/////проверка существования файлов выгрузки из 1С
////файл выгрузки сервисных книжек
$file_hist = ABSPATH.'/_1c_alfa_exchange/AA_hist.csv';
if (!file_exists($file_hist))
{
/////   echo '<H2><b>Файл обмена с сервисными книжками не существует.</b></H2><br>';
$m_comment='Файл обмена с сервисными книжками не существует';
$failure=TRUE;
}

/////инициируем таблицу лога
/////если не существует файла то возврат и ничего не делаем
if ($failure){
///включает защиту от SQL инъекций и данные можно передавать как есть, например: $_GET['foo']
/////   echo '<H2><b>Попытка вставить запись в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>$m_comment));
wp_die();
/////    echo '<H2><b>Возврат в начало.</b></H2><br>';
return $failure;
}
/////проверка лога загрузки, что бы не загружать тоже самое
$masiv_data_file=stat($file_hist);   ////передаем в массив свойство файла
$m_size_file=$masiv_data_file[7];    ////получаем размер файла
$m_mtime_file=$masiv_data_file[9];   ////получаем дату модификации файла
////создаем запрос на получение последней удачной загрузки
////выбираем по штампу времени создания (редактирования) файла загрузки AA_hist.csv, $m_mtime_file

/////   echo '<H2><b>Размер файла: '.$m_size_file.'</b></H2><br>';
/////   echo '<H2><b>Штамп времени файла: '.$m_mtime_file.'</b></H2><br>';
/////   echo '<H2><b>Формирование запроса на выборку из лога</b></H2><br>';
////препарируем запрос
$text_zaprosa=$wpdb2->prepare("SELECT * FROM `vin_logs` WHERE `last_mtime_upload` = %s", $m_mtime_file);
$results=$wpdb2->get_results($text_zaprosa);

if ($results)
{   foreach ( $results as $r)
{
////если штамп времени и размер файла совпадают, возврат
if (($r->last_mtime_upload==$m_mtime_file) && ($r->last_size_upload==$m_size_file))
{////echo '<H2><b>Возврат в начало, т.к. найдена запись в логе.</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>'Загрузка отменена, новых данных нет, т.к. найдена запись в логе.'));
wp_die();
return $failure;
}
}
}
////если данные новые, пишем в лог запись о начале загрузки
/////echo '<H2><b>Попытка вставить запись о начале загрузки в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>0, 'last_size_upload'=>$m_size_file, 'comment'=>'Начало загрузки'));

////очищаем таблицу
$clear_tbl_zap=$wpdb2->prepare("TRUNCATE TABLE %s", 'vin_history');
$clear_tbl_zap_repl=str_replace("'","`",$clear_tbl_zap);
$results=$wpdb2->query($clear_tbl_zap_repl);
/////   echo '<H2><b>Очистка таблицы сервисных книжек</b></H2><br>';
if (empty($results))
{
/////   echo '<H2><b>Ошибка очистки таблицы книжек, завершение.</b></H2><br>';
//// если очистка не удалась, возврат
$failure=TRUE;
wp_die();
return $failure;
}

////загружаем данные
$table='vin_history';         // Имя таблицы для импорта
//$file_hist Имя CSV файла, откуда берется информация     // (путь от корня web-сервера)
$delim=';';          // Разделитель полей в CSV файле
$enclosed='"';      // Кавычки для содержимого полей
$escaped='\

52 Comments

  1. V_V_V

    Спасибо, то что нужно.

    Но есть пару непоняток (для меня):

    я заменил «Сервер» на «ИмяСервера» в строке

    РабПроц = Коннектор.ConnectWorkingProcess(Сервер + «:» + СтрЗаменить(Порт, Символы.НПП, «»));

    и в COMОбъекте Соединение нет значения UserName, т.е. не могу проверить на ИмяПользователя(). Но и в Сеанс, и в Соединение есть SessionID, пришлось выкручиваться через него (получать в Сеанс и проверять в Соединение). UserName только у меня отсутствует? Платформа 8.2.11.236.

    Reply ↓
  2. V_V_V

    По синтакс-помощнику GetInfoBaseConnections получает массив описаний соединений информационной базы. А в описании соединения нет свойства UserName. Оно есть в свойствах соединения.

    Осталось только разобраться как получить не описание соединений, а сами соединения… 😀

    Reply ↓
  3. V_V_V

    Хм… Соединения заработали только когда прописал (обгуглившись по самое ну погоди):

    ИнформационнаяБаза2 = РабПроц.CreateInfoBaseInfo();

    ИнформационнаяБаза2.Name = ИмяБазы;

    Соединения = РабПроц.GetInfoBaseConnections(ИнформационнаяБаза2);

    При этом проверять запущенные приложения нужно не по Соединение.Application, а по Соединение.AppID. Плюс не мешало бы добавить проверку не только backgroundjob, designer, а и comconsole — а то выкинет и только что аутентифицированное COM-соединение…

    Все вышесказанное абсолютно не претендует на истину — пробуйте на свой страх и риск… 😀

    Reply ↓
  4. Asdam

    Rabajaba, поделитесь, плиз, модулем регламентного задания, отвечающего за резервное копирование.

    Reply ↓
  5. Rabajaba

    (3) Спасибо за развитие, честно дальше не копал. По «Сервер» на «ИмяСервера» — это я код не подправил 🙂

    «Плюс не мешало бы добавить проверку не только backgroundjob, designer, а и comconsole — а то выкинет и только что аутентифицированное COM-соединение… » я показал лишь пример, дальше можно расширять как угодно 🙂 моей целью было НЕ убивать текущее фоновое задание и НЕ закрывать конфигуратор, в котором я могу что-то не сохранить 🙂 да и вообще сама идея мочить конфигуратор — ужас.

    (4) Модуль довольно специфичен, но суть проста. Объясню на примере:

    01:00 запуск регламетного задания, которое генерирует *.bat файл пакетного запуска конфигуратора в режиме выгрузки ИБ, после чего мочит все соединения

    01:20 регламентное задание винды стартует *.bat файл.

    Поделиться можно, но там код весь на константах и не очень красив, чтобы на люди показывать 🙂

    Reply ↓
  6. sound

    Материал нужный однозначно, особенно для конкретных заточек (мало ли кому что надо). А вот про скрипты для бекапов уже можно оды слагать, причем хоть на 1C хоть на VBS, вот например есть http://infostart.ru/public/19363/, там кстати любезно расписаны все обработки аналогичной направленности — бери нихачу 🙂

    Reply ↓
  7. artur_antipin

    Платформа 8.2.14.519, режим серверный.

    При подключении к Агенту выдает такую ошибку:

    {Форма.Форма.Форма(108)}: Ошибка при вызове метода контекста (ConnectAgent)

    СлужбаСервера = Соединитель.ConnectAgent(СерверБД);

    по причине:

    Произошла исключительная ситуация (V82.COMConnector.1): descr=Сервер недоступен (Не отвечает, завершается аварийно или порт занят другим приложением) line=556 file=SrcRemoteCreatorImpl.cpp

    Кто нибудь с такой сталкивался?

    Reply ↓
  8. VchikA

    (7) столкнулся с такой ошибкой. Дело в том, что строка подключения к базе данных содержало порт. Таким образом при получении имени сервера, возвращается не имя сервера(«127.0.0.1»), а имя сервера + порт («127.0.0.1:1689»).

    Заменил строку — все заработало:

    //ИмяСервера = Лев(ПодстрокаПоиска, Найти(ПодстрокаПоиска, «»»») — 1); //для стандартного порта (1541)

    ИмяСервера = Лев(ПодстрокаПоиска, Найти(ПодстрокаПоиска, «»»») — 6); //для не стандартного порта (7) artur.antipin,

    Reply ↓
  9. alexbur

    (3) V_V_V, Всё верно. Тоже дошёл до этого обчитавшись хелпа. Дело в том, что если делать GetInfoBaseConnections через агента сервера (а не через рабочий процесс), то мы получаем на выходе тип «Описание соединения», а не «Соединение». У него действительно нет UserName и вместо AppID есть Applications.


    Если Соединение.UserName = ИмяПользователя() Тогда

    // это текущий пользователь

    Продолжить;

    КонецЕсли;

    ИМХО, чтобы не убить своё собственное соединение лучше использовать проверку на SessionID (ConnID), который есть и в соединении (сеансе). Получить SessionID (ConnID) текущего (своего) подключения можно через НомерСеансаИнформационнойБазы() (НомерСоединенияИнформационнойБазы()).

    Reply ↓
  10. V_V_V

    (9) Если ничего не путаю, то именно через SessionID я и выкрутился. Кажется об этом речь в моем первом посте шла (1).

    Как давно это было… Год прошел… 🙂

    Reply ↓
  11. alexbur

    (10) V_V_V, Ну, некоторые темы не стареют. Для меня как раз проблемой было как раз узнать ID текущего сеанса, чтобы сравнить с SesionID. Может кому нибудь тоже поможет.

    Reply ↓
  12. ekean

    Вместо —

    РабПроц = Коннектор.ConnectWorkingProcess(Имяервера + «:» + СтрЗаменить(Порт, Символы.НПП, «»));

    надо —

    РабПроц = Коннектор.ConnectWorkingProcess(«tcp://» + Процесс.HostName + «:» + Формат(Процесс.MainPort, «ЧГ=0»));

    Reply ↓
  13. mike24
    Reply ↓
  14. Dvornik

    В платформе 8.2.14 работает? Никто не подскажет?

    Reply ↓
  15. edyardg

    Спасибо очень нужная информация

    Reply ↓
  16. Marichka2

    Спасибо очень нужная информация, попробую у себя

    Reply ↓
  17. quares

    Интересно и полезно — да. Но схема алгоритма (если читать код описанный выше) не совсем соответствует коду.

    Автору 5 баллов.

    Reply ↓
  18. Leoway

    Огромное спасибо, очень помогло.

    Reply ↓
  19. deshi2

    спасибо, безгранично полезный код!

    Reply ↓
  20. deshi2

    кстати, рекомендую на совпадение с текущей базой проверять с учетом регистра, уже нарвался. Вот так надо:

    Если ВРЕГ(База.Name) = ВРЕГ(ИмяБазы) Тогда

    ИнформационнаяБаза = База;

    Прервать;

    КонецЕсли;

    Reply ↓
  21. sad12345

    8.2.15.301

    Соединение.UserName = ИмяПользователя()

    ERROR:

    Поле объекта не обнаружено (UserName)

    FAQ:

    Соединение (IInfoBaseConnectionInfo)

    UserName (UserName)

    Использование:

    Только чтение.

    Описание:

    Тип: Строка.

    Содержит имя пользователя 1С:Предприятия, подсоединенного к информационной базе.

    Доступность:

    Интеграция.

    Что не так???

    Reply ↓
  22. virus-1c

    (21) sad12345,

    У меня такая же проблема

    сделал так

    ТекПользовательApplication = «»;

    ТекПользовательConnId = «»;

    ТекПользовательHost = «»;

    Сеансы = Агент.GetInfoBaseSessions(Кластер, ИнформационнаяБаза);

    Для каждого Сеанс из Сеансы Цикл

    Если нРег(Сеанс.AppID) = «backgroundjob» ИЛИ нРег(Сеанс.AppID) = «designer» Тогда

    // если это сеансы конфигуратора или фонового задания, то не отключаем

    Продолжить;

    КонецЕсли;

    Если Сеанс.UserName = ИмяПользователя() Тогда

    // это текущий пользователь

    ТекПользовательApplication = Сеанс.Connection.Application;

    ТекПользовательConnId = Сеанс.Connection.ConnId;

    ТекПользовательHost = Сеанс.Connection.Host;

    Продолжить;

    КонецЕсли;

    Агент.TerminateSession(Кластер, Сеанс);

    Сообщить(строка(Сеанс.UserName) + » — » + нРег(Сеанс.AppID));

    КонецЦикла;

    СоединенияБазы = Агент.GetInfoBaseConnections(Кластер, ИнформационнаяБаза);

    // Разорвать соединения клиентских приложений.

    Для Каждого Соединение Из СоединенияБазы Цикл

    Если нРег(Соединение.Application) = «backgroundjob» ИЛИ нРег(Соединение.Application) = «designer» Тогда

    // если это соединение конфигуратора или фонового задания, то не отключаем

    Продолжить;

    КонецЕсли;

    //Если Соединение.UserName = ИмяПользователя() Тогда

    // // это текущий пользователь

    // Продолжить;

    //КонецЕсли;

    Если (Соединение.Application = ТекПользовательApplication) и (Соединение.ConnId = ТекПользовательConnId) и (Соединение.Host = ТекПользовательHost) Тогда

    // это текущий пользователь

    Продолжить;

    КонецЕсли;

    РабПроц.Disconnect(Соединение);

    КонецЦикла;

    Reply ↓
  23. m_aster

    Работает, спасибо!

    Reply ↓
  24. Stamper

    шикарно! благодарю

    был у меня еще «отключатель» от файловой базы «ProcessKiller» на vbs, но потерялся 🙁

    никто не встречал?

    Reply ↓
  25. Stepan_1c

    Спасибо. раньше почему то не нашел и написал свой вариант через батник. сейчас попробую через 1с 🙂

    Reply ↓
  26. dyak84

    Автору огромное спасибо. Написано просто и доступно. Надоело бесконечно тестировать чужую работу пора и самому разбиратся как и что работает в 1С. Побольше бы таких статей. Протестирую и отпишусь

    Reply ↓
  27. dmbal

    Платформа 1С:Предприятие 8.2 (8.2.17.153).

    РабПроц.Disconnect(Соединение);

    по причине:

    Типы не совпадают (1)

    Как не пробовал, одно и тоже 🙁

    Вариант кода следующий:

    Для Каждого Соединение Из СоединенияБазы Цикл

    Если нРег(Соединение.Application) = «backgroundjob» ИЛИ нРег(Соединение.Application) = «designer» Тогда

    // если это соединение конфигуратора или фонового задания, то не отключаем

    Продолжить;

    КонецЕсли;

    //Если Соединение.UserName = ИмяПользователя() Тогда

    // // это текущий пользователь

    // Продолжить;

    //КонецЕсли;

    Если (Соединение.Application = ТекПользовательApplication) и (Соединение.ConnId = ТекПользовательConnId) и (Соединение.Host = ТекПользовательHost) Тогда

    // это текущий пользователь

    Продолжить;

    КонецЕсли;

    РабПроц.Disconnect(Соединение);

    КонецЦикла;

    Показать

    Reply ↓
  28. aprol

    (27) dmbal, Та же ситуация на платформе 15.319, решения не нашел.

    Reply ↓
  29. griffer

    Спасибо автору!

    Внес небольшие изменения и использовал для закрытия сеансов и соединений оставшихся после аварийного завершения работы.

    Reply ↓
  30. Stamper

    (29) griffer, выкладывайте, чего уж там 🙂

    Reply ↓
  31. griffer
    Reply ↓
  32. vers139

    Интересно, что если пытаться разорвать соединения перед завершением сеансов, то 1С выдаёт ошибку при попытке получить сеансов. Ругается на несоответствие типов в методе GetInfoBaseSessions. Поменял местами куски кода (сначала сеансы, потом соединения) и всё заработало.

    Reply ↓
  33. poyson

    Спасибо! Спас!

    Reply ↓
  34. miller-adm

    Реализация данного механизма на практике… Обработка «Консоль Администратора»

    Reply ↓
  35. gerandy

    Спасибо, очень полезный код!

    Строчку в коде griffer

    Если UserName = ИмяПользователя() И ConnID <> НомерСоединенияИнформационнойБазы() Тогда

    исправил

    UserName <> ИмяПользователя()
    Reply ↓
  36. aspiid

    (8) VchikA,

    Тоже столкнулся с этой проблемой.

    Заменил строку — все заработало:

    //ИмяСервера = Лев(ПодстрокаПоиска, Найти(ПодстрокаПоиска, «»»») — 1); //для стандартного порта (1541)

    ИмяСервера = Лев(ПодстрокаПоиска, Найти(ПодстрокаПоиска, «»»») — 6); //для не стандартного порта (7) artur.antipin,

    Вот не уверен, что это правильно.

    Вообще говоря, то ИмяСервера (включая порт), которое мы получаем таким образом, это адрес кластера 1с, на котором крутится эта база. Если порт стандартный, то он совпадает с именем сервера (читай агента сервера). Если порты не стандартные — то они разные.

    Так вот. Если вы просто обрежете порт из адреса кластера, то получите обращение по стандартному порту, т.е. 1541, и скорее всего, попадете на другой сервер, не тот, на котором крутится данная база.

    Вот в связи с этим вопрос: можно ли как то узнать полный адрес агента сервера, на котором крутится эта база?

    Reply ↓
  37. tolstoy

    Спасибо автору!

    От себя добавлю: если администратор кластера не указан, то аутентификацию к агенту сервера нужно осуществлять кодом:

    АгентСервера.Authenticate(Кластер,»»,»»);

    Если подключаться так, как указано в статье

    Агент.Authenticate(Кластер,,);

    то будет ошибка: «Указанное число параметров не соответствует ожидаемому числу».

    Reply ↓
  38. DrBerg

    А если мне нужен номер SPID, в СОМ объекте его нет. Каким образом его выудить? Консоль получает этот столбец. Есть идеи?

    Reply ↓
  39. DrBerg

    (37) tolstoy, Вместо параметров можно передать значения реквизитов формы. Если они пустые и админы не заявлены, то все проходит.

    Reply ↓
  40. besometr

    Еще порт может быть не 1540:

    мМассивСтрокиСоединения = ОбщегоНазначения.РазложитьСтрокуВМассивПодстрок(СтрокаСоединенияИнформационнойБазы(),»;»);

    мСтрокаИБ = мМассивСтрокиСоединения[0];

    мСтрокаИБ = СтрЗаменить(мСтрокаИБ,»Srvr=»,»»);

    СерверПорт = СтрЗаменить(мСтрокаИБ,»»»»,»»);

    мМассивСерверПорт = ОбщегоНазначения.РазложитьСтрокуВМассивПодстрок(СерверПорт,»:»);

    Если мМассивСерверПорт.Количество() > 1 Тогда

    Сервер = мМассивСерверПорт[0];

    Порт = СтрЗаменить(Строка(Число(мМассивСерверПорт[1])-1), Символы.НПП, «»);

    ИначеЕсли мМассивСерверПорт.Количество() = 1 Тогда

    Сервер = мМассивСтрокиСоединения[0];

    Порт = 1540;

    Иначе

    Сервер = «localhost»;

    Порт = 1540;

    КонецЕсли;

    мСтрокаИБ = мМассивСтрокиСоединения[1];

    мСтрокаИБ = СтрЗаменить(мСтрокаИБ,»Ref=»,»»);

    База = СтрЗаменить(мСтрокаИБ,»»»»,»»);

    Reply ↓
  41. isn

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

    Reply ↓
  42. yuraer

    А кто-нибудь пробовал отключать зависшие регламентные задания таким способом?

    Чем чревато такое отключение, если оно возможно?

    Reply ↓
  43. etarabarova

    Спасибо за статью и комментарии. Очень помогло.

    Reply ↓
  44. ShonLe

    Спасибо за статью помогла, но если более новые возможности?

    Reply ↓
  45. progr-2008

    В 8.3 можно настроить время отключения спящих сеансов в конфигураторе.

    Reply ↓
  46. Manticor

    Друзья, подскажите пожалуйста какими средствами программно создать новую Иб в реестре кластеров? Очень нужно

    Reply ↓
  47. seregasame

    Соединение с рабочим процессом.CreateInfoBase (IWorkingProcessConnection.CreateInfoBase)

    Соединение с рабочим процессом (IWorkingProcessConnection)

    CreateInfoBase (CreateInfoBase)

    Синтаксис:

    CreateInfoBase(<ИнформационнаяБаза>, <Режим>)

    Параметры:

    <ИнформационнаяБаза> (обязательный)

    Тип: Информационная база.

    Информационная база. Все свойства, необходимые для создания информационной базы (Name, dbServerName, dbName, dbUser, dbPassword, Locale, [DateOffset]) должны быть заполнены.

    <Режим> (обязательный)

    Тип: Число.

    Режим создания информационной базы:

    0 — при создании информационной базы базу данных не создавать;

    1 — при создании информационной базы создавать базу данных.

    Возвращаемое значение:

    Тип: Информационная база.

    Описание:

    Создает информационную базу с заданными параметрами. Требуется аутентификация администратора кластера.

    Доступность:

    Интеграция.

    Reply ↓
  48. blgdk86

    GetInfoBaseSessions() — получаем сеансы

    TerminateSession() — убиваем сеансы

    GetInfoBaseConnections() — получаем соединения

    Disconnect() — убиваем соединения

    А как быть с блокировками?

    GetInfoBaseLocks() — получаем блокировки

    А как их убрать??

    Получилось такая ситуация, что при обновлении конфигурации возникает ошибка исключительной блокировки ИБ. Т.к. есть некие зависшие соединения. Эти соединения видны через консоль кластера только в Блокировках. В сеансах и в соединениях — пусто. Как программно решить эту проблему?

    Reply ↓
  49. jONES1979

    «учавствует» -> «участвует»

    А так, вообще, спасибо! И интересно, и в работе пригодится!

    Reply ↓
  50. isn

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

    Reply ↓
  51. ROM_1C

    Почему может не завершаться сеансы пользователей?

    До

    РабочийПроцес.AddAuthentication(«test»,»pass»);
    СписокСоединенийСбазой  = РабочийПроцес.GetInfoBaseConnections(ИнфоБаза);
    
    для Каждого стр из СписокСоединенийСбазой Цикл
    Если  нРег(стр.AppID) = «backgroundjob» ИЛИ нРег(стр.AppID) = «comconsole» Тогда
    Продолжить;
    КонецЕсли;
    
    РабочийПроцес.Disconnect(стр);
    //Сообщить(стр.userName);
    КонецЦикла;

    Показать

    Reply ↓
  52. ROM_1C

    (51) один кластер, один рабочий сеанс, AddAuthentication — проходить удачно, список соединений получаю со всеми их свойствами

    Reply ↓

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *