Новая концепция завершения работы приложения 1С 8.3.8

Все ниже описанное касается управляемого приложения.

Начиная с версии 8.3.8 фирма 1С изменила подход к завершению работы и  закрытию приложения. Новомодные веяния – ли, ошибки при работе с WEB интерфейсом – ли, но теперь со всех обработчиков событий, используемых нами при закрытии – «ПередЗавершениемРаботыСистемы» и «ПриЗавершенииРаботыСистемы», сервер не доступен. Рассмотрим, как теперь работают данные обработчики.

Обработчик: ПередЗавершениемРаботыСистемы(Отказ, ТекстПредупреждения)

Как мы помним возникает перед завершением программы, до закрытия главного окна. Если «Отказ» равен «Ложь», то программа просто закрывается, не выдавая никаких вопросов. В нем можно сделать всяческие проверки, все функции должны быть только клиентские (В процессе завершения работы приложения запрещены серверные вызовы и открытие окон.) и может быть установлен отказ от выхода из программы. И вот тут появляется первый подвох. Установление «Отказ» в Истина не отменяет выход из системы, а только указывает, что нужно выдать платформенный диалог, в котором будет предложено выйти или остаться с текстом из параметра «ТекстПредупреждения»:

Диалог завершения программы

И если пользователь нажмет «Завершить работу», все ваши проверки идут лесом. Платформа просто закрывает приложение не зависимо ни от чего. Отловить этот момент в коде нельзя, это все происходит на уровне платформы.

Если же пользователь нажмет на «Продолжить работу», то возможны два варианта событий:

  1. Если вы внутри вызова функций из данной процедуры подключали обработчик ожидания
    ПодключитьОбработчикОжидания("ИмяФункцииОбработчикаОжидания", 0.1, Истина);

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

  2. Если же вы этого не делали, программа просто продолжит работу.

Ответ на первый глобальный русский вопрос «кто виноват?» понятен. Я постараюсь ответить на второй «что делать?». Если нам надо что то зафиксировать в базе, например информацию о работе пользователей в системе. Кто когда заходил, кто когда вышел.

Решение однако лежит вообще в другой плоскости, а именно, в регламентном задании которое периодически будет опрашивать систему на активных пользователей, и фиксировать что такой то пользователь уже отсутствует в списке активных.

Для опроса системы у нас есть функция ПолучитьСеансыИнформационнойБазы(), которая возвращает массив сеансов. А для фиксации, что пользователя в сеансах уже нет нужно создавать новый регистр. Я предлагаю периодический (в пределах секунды) РС с двумя измерениями «Пользователь» и «НомерСеанса», в ресурсах же должно быть как минимум поле «ЗавершениеСеанса» по которому мы будем понимать что пользователь нами не обработан или обработан но активен.

Структура регистра
Процедура ОбработатьСеансыАктивныхПользователей() Экспорт 
	
	ТекущаяДатаСеанса = ТекущаяДатаСеанса();
	
	ТабАктивныхПользователей  = Новый ТаблицаЗначений;
	ТабАктивныхПользователей.Колонки.Добавить("Пользователь", Новый ОписаниеТипов("СправочникСсылка.Пользователи"));
	ТабАктивныхПользователей.Колонки.Добавить("НомерСеанса", Новый ОписаниеТипов("Число"));
	
	СеансыИнформационнойБазы = ПолучитьСеансыИнформационнойБазы();
	Для Каждого СеансИБ Из СеансыИнформационнойБазы Цикл
		//Фоновые задания или неавторизованных пользователей пропускаем
		Если СеансИБ.ПолучитьФоновоеЗадание() <> Неопределено ИЛИ (ТипЗнч(СеансИБ.Пользователь) = Тип("ПользовательИнформационнойБазы") И НЕ ЗначениеЗаполнено(СеансИБ.Пользователь.Имя)) Тогда
			Продолжить;
		КонецЕсли; 
		
		АктивныйПользователь = ДобавитьЗаписьАвторизацииПользователей(СеансИБ, ТекущаяДатаСеанса);
		Если ЗначениеЗаполнено(АктивныйПользователь) Тогда
			НоваяСтрока = ТабАктивныхПользователей.Добавить();
			НоваяСтрока.Пользователь = АктивныйПользователь;
			НоваяСтрока.НомерСеанса = СеансИБ.НомерСеанса;
		КонецЕсли;
	КонецЦикла;
	
	Запрос = Новый Запрос;
	Запрос.Текст = 
	"ВЫБРАТЬ
	|	ккПериодическиеСведенияОПользователяхСрезПоследних.Период КАК Период,
	|	ккПериодическиеСведенияОПользователяхСрезПоследних.Пользователь КАК Пользователь,
	|	ккПериодическиеСведенияОПользователяхСрезПоследних.НомерСеанса КАК НомерСеанса
	|ИЗ
	|	РегистрСведений.ккПериодическиеСведенияОПользователях.СрезПоследних(
	|			&ДатаРегистрации,
	|			НЕ (Пользователь, НомерСеанса) В (&ТабРаботающих)
	|				И ЗавершенияСеанса = ДатаВремя(1,1,1)) КАК ккПериодическиеСведенияОПользователяхСрезПоследних";
	
	Запрос.УстановитьПараметр("ДатаРегистрации", ТекущаяДатаСеанса);
	Запрос.УстановитьПараметр("ТабРаботающих", ТабАктивныхПользователей);
	
	Результат = Запрос.Выполнить();
	Выборка = Результат.Выбрать();
	
	Пока Выборка.Следующий() Цикл
		
		НаборЗаписей = РегистрыСведений.ккПериодическиеСведенияОПользователях.СоздатьНаборЗаписей();
		НаборЗаписей.Отбор.Период.Установить(Выборка.Период);
		НаборЗаписей.Отбор.Пользователь.Установить(Выборка.Пользователь);
		НаборЗаписей.Отбор.НомерСеанса.Установить(Выборка.НомерСеанса);
		НаборЗаписей.Прочитать();
		
		Для каждого СведенияОПользователе Из НаборЗаписей Цикл
			СведенияОПользователе.ЗавершенияСеанса = ТекущаяДатаСеанса;
			СведенияОПользователе.ВремяВСеансе = СведенияОПользователе.ЗавершенияСеанса - СведенияОПользователе.Период;
		КонецЦикла; 
		
		Попытка
		Если НаборЗаписей.Количество() Тогда
			НаборЗаписей.Записать();
		КонецЕсли; 
		Исключение
		КонецПопытки;
		
	КонецЦикла;
	
КонецПроцедуры
Функция ДобавитьЗаписьАвторизацииПользователей(СеансИБ, ТекущаяДатаСеанса) Экспорт
	
	ТекущийПользователь = НайтиСсылкуПоИдентификаторуПользователя(СеансИБ.Пользователь.УникальныйИдентификатор);
	Если НЕ ЗначениеЗаполнено(ТекущийПользователь) Тогда
		Возврат Неопределено;
	КонецЕсли; 
	
	Запрос = Новый Запрос;
	Запрос.Текст = 
	"ВЫБРАТЬ ПЕРВЫЕ 1
	|	1 КАК Поле
	|ИЗ
	|	РегистрСведений.ккПериодическиеСведенияОПользователях.СрезПоследних(
	|			&ДатаРегистрации,
	|			Пользователь = &Пользователь
	|				И НомерСеанса = &НомерСеанса
	|				И ЗавершенияСеанса = ДАТАВРЕМЯ(1, 1, 1)) КАК ккПериодическиеСведенияОПользователяхОстатки";
	
	Запрос.УстановитьПараметр("ДатаРегистрации", ТекущаяДатаСеанса);
	Запрос.УстановитьПараметр("Пользователь", ТекущийПользователь);
	Запрос.УстановитьПараметр("НомерСеанса", СеансИБ.НомерСеанса);
	
	Результат = Запрос.Выполнить();
	
	НаборЗаписей = РегистрыСведений.ккПериодическиеСведенияОПользователях.СоздатьНаборЗаписей();
	НаборЗаписей.Отбор.Период.Установить(СеансИБ.НачалоСеанса);
	НаборЗаписей.Отбор.Пользователь.Установить(ТекущийПользователь);
	НаборЗаписей.Отбор.НомерСеанса.Установить(СеансИБ.НомерСеанса);
	НаборЗаписей.Прочитать();
	
	Если НЕ Результат.Пустой() Тогда
		Если НаборЗаписей.Количество() Тогда
			СведенияОПользователе = НаборЗаписей[0];
			СведенияОПользователе.ВремяВСеансе = ТекущаяДатаСеанса - СведенияОПользователе.Период;
		КонецЕсли; 
	Иначе
		СведенияОПользователе = НаборЗаписей.Добавить();
		СведенияОПользователе.Период = СеансИБ.НачалоСеанса;
		СведенияОПользователе.Пользователь = ТекущийПользователь;
		СведенияОПользователе.НомерСеанса = СеансИБ.НомерСеанса;
		
		СведенияОПользователе.Подключение = 1;
		СведенияОПользователе.ВремяВСеансе = ТекущаяДатаСеанса - СведенияОПользователе.Период;
		
		СведенияОПользователе.ИмяПриложения = ПредставлениеПриложения(СеансИБ.ИмяПриложения);
		СведенияОПользователе.ИмяКомпьютера = СеансИБ.ИмяКомпьютера;
	КонецЕсли; 
	
	Попытка
		НаборЗаписей.Записать();
	Исключение
	КонецПопытки;
	
	Возврат ТекущийПользователь;
	
КонецФункции

Функция НайтиСсылкуПоИдентификаторуПользователя(Идентификатор)
	
	// Нет доступа к разделенному справочнику из неразделенного сеанса.
	Если ОбщегоНазначения.РазделениеВключено() 
		И Не ОбщегоНазначения.ДоступноИспользованиеРазделенныхДанных() Тогда
		Возврат Неопределено;
	КонецЕсли;
	
	Запрос = Новый Запрос;
	
	ШаблонТекстаЗапроса = 
	"ВЫБРАТЬ
	|	Ссылка КАК Ссылка
	|ИЗ
	|	%1
	|ГДЕ
	|	ИдентификаторПользователяИБ = &Идентификатор";
					
	ТекстЗапросаПоПользователям = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(ШаблонТекстаЗапроса, Метаданные.Справочники.Пользователи.ПолноеИмя());
	
	ТекстЗапросаПоВнешнимПользователям = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(ШаблонТекстаЗапроса, Метаданные.Справочники.ВнешниеПользователи.ПолноеИмя());
	
	Запрос.Текст = ТекстЗапросаПоПользователям;
	Запрос.Параметры.Вставить("Идентификатор", Идентификатор);
	Результат = Запрос.Выполнить();
	
	Если НЕ Результат.Пустой() Тогда
		Выборка = Результат.Выбрать();
		Выборка.Следующий();
		Возврат Выборка.Ссылка;
	КонецЕсли;
	
	Запрос.Текст = ТекстЗапросаПоВнешнимПользователям;
	Результат = Запрос.Выполнить();
	
	Если НЕ Результат.Пустой() Тогда
		Выборка = Результат.Выбрать();
		Выборка.Следующий();
		Возврат Выборка.Ссылка;
	КонецЕсли;
	
	Возврат Справочники.Пользователи.ПустаяСсылка();
	
КонецФункции

 

Обработчик: ПриЗавершенииРаботыСистемы()

Привожу полный текст справки:

«Описание:

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

Примечание:

В данной процедуре не допускаются:

  • открытие форм и других окон,
  • использование серверных вызовов,
  • использование асинхронных вызовов.

Не поддерживаются:

  • выдача сообщений,
  • установка текста в панели состояния,
  • другие действия, требующие наличия главного окна.»

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

Резюме

Резюмируя данную статью хочу поблагодарить фирму 1С, что не дает нам соскучиться и всегда на страже нашего свободного времени.

Comments

So empty here ... leave a comment!

Добавить комментарий

Sidebar