Имя: Пароль:
1C
 
Блокировка объекта при работе с ним (2)
0 ТДК
 
27.08.26
11:44
первоначальная тема сдана в архив Блокировка объекта при работе с ним, оживлю проблему платформенная возможность  `ЗаблокироватьДанныеДляРедактирования()` не переживает аварийное завершение сеанса (выключили свет, завис клиент, killtask), и заблокированный документ остаётся недоступен для редактирования, пока кластер сам не «дожмёт» мёртвый сеанс — а если пользователь вообще ушёл в отпуск, то и того дольше.

Сценарий работы
1. Пользователь А открывает документ — объект захвачен, в регистре запись с его именем и номером текущего сеанса.
2. У пользователя А гаснет свет, сеанс на сервере рано или поздно обрывается (или зависает, но по факту не отвечает) — запись в регистре остаётся.
3. Любой другой обычный пользователь, пытаясь открыть документ, получает сообщение «документ редактируется пользователем А с ...» и открыть его не может — форма регистра блокировок ему не показывается, разбираться с этим не его задача.
4. Уполномоченный пользователь (роль `Документы_СнятиеБлокировкиЗахвата`) открывает тот же документ. Система проверяет `ПолучитьСеансыИнформационнойБазы()` — сеанса А там больше нет, значит `МожноВмешаться = Истина`. Показывается вопрос с тремя кнопками.
5. Он выбирает «Перевести блокировку на себя» — вызывается `ПерехватитьОбъект`, регистр перезаписывается на текущего пользователя и текущий сеанс, действие логируется. Либо «Снять блокировку» — вызывается `ПринудительноСнятьБлокировку`, запись в регистре просто очищается, документ становится свободным для всех, тоже с записью в лог.

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

1. При открытии документа (не нового) он «захватывается» текущим пользователем — в регистр пишется запись с его именем, временем и номером сеанса.
2. Пока документ открыт, запись обновляется при каждой записи документа (чтобы время захвата не «протухало» у тех, кто реально работает).
3. При закрытии формы документа блокировка снимается.
4. Если документ пытается открыть другой пользователь, пока он захвачен — система проверяет, **жив ли ещё сеанс**, в котором произошёл захват (через `ПолучитьСеансыИнформационнойБазы()`):
   - если сеанс жив — просто сообщение «документ редактируется пользователем NN», открытие отменяется;
   - если сеанса уже нет (тот самый случай с погасшим светом) — пользователю со специальной ролью показывается диалог с тремя кнопками: «Перевести блокировку на себя», «Снять блокировку», «Отмена». Обычный пользователь без этой роли просто видит предупреждение и продолжить не может — экран с формой регистра ему не нужен и не показывается вовсе.
5. Отдельным «рубежом» на сервере висит подписка на событие `ПередЗаписью`, которая не даёт записать заблокированный кем-то другим объект в обход интерфейса (фоновые задания, обмены и т.п.).


Подробности
2. Технические детали

2.1. Хранилище — независимый регистр сведений

`РегистрСведений.БлокировкиРедактированияОбъектов` — независимый, непериодический.

- Измерение **Объект** — тип `ОпределяемыйТип.СсылкаНаОбъектСБлокировкойРедактирования`. Определяемый тип используется специально для того, чтобы регистр не был жёстко привязан к одному документу: чтобы подключить блокировку к новому документу, достаточно расширить состав этого определяемого типа его ссылкой (сейчас там `ДокументСсылка.АмбулаторныйПрием`).
- Ресурсы:
  - **Пользователь** (`СправочникСсылка.Пользователи`) — кто захватил;
  - **ВремяЗахвата** (Дата) — когда;
  - **Компьютер** (Строка, 50) — с какого компьютера (`ИмяКомпьютера()`);
  - **Сеанс** (Число, 10) — номер сеанса ИБ (`НомерСеансаИнформационнойБазы()`), именно он используется для проверки «жив ли ещё захвативший».

2.2. Второй рубеж защиты — подписка на событие

Есть ещё один определяемый тип — `ОпределяемыйТип.ОбъектСБлокировкойРедактирования` (сейчас в составе — `ДокументОбъект.АмбулаторныйПрием`). На нём висит подписка на событие `ПередЗаписью`:

Процедура ПроверитьБлокировкуПередЗаписьюОбъекта(Источник, Отказ) Экспорт
	Если Источник.ОбменДанными.Загрузка Тогда
		Возврат;
	КонецЕсли;
	ИнформацияОЗахвате = ПолучитьИнформациюОЗахвате(Источник.Ссылка);
	Если ИнформацияОЗахвате.Захвачен Тогда
		Отказ = Истина;
	КонецЕсли;
КонецПроцедуры



Это не основной механизм (основной — на форме, см. ниже), а именно подстраховка: если кто-то попробует записать заблокированный документ мимо стандартной формы (программно, из обработки и т.п.), запись всё равно будет отклонена. Загрузка через обмен данными сознательно исключена из проверки.

2.3. Серверный модуль — `БлокировкаОбъектовСервер`

Процедура ЗахватитьОбъект(знач Объект) Экспорт
	Если НЕ ПравоДоступа("Изменение",Объект.Ссылка.Метаданные()) Тогда 
		ВызватьИсключение СтрШаблон("Документ доступен только для просмотра");
	КонецЕсли;
	
	УстановитьПривилегированныйРежим(Истина);
	НачатьТранзакцию();
	Попытка
		Блокировка = Новый БлокировкаДанных;
		ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.БлокировкиРедактированияОбъектов");
		ЭлементБлокировки.УстановитьЗначение("Объект", Объект);
		Блокировка.Заблокировать();
		НаборЗаписей = РегистрыСведений.БлокировкиРедактированияОбъектов.СоздатьНаборЗаписей();
		НаборЗаписей.Отбор.Объект.Установить(Объект);
		НаборЗаписей.Прочитать();
		Если НаборЗаписей.Количество() = 0 Тогда
			Запись = НаборЗаписей.Добавить();
			Запись.Объект = Объект;
		Иначе
			Запись = НаборЗаписей.Получить(0);
		КонецЕсли;
		Запись.Пользователь = Пользователи.ТекущийПользователь();
		Запись.ВремяЗахвата = ТекущаяДатаСеанса();
		Запись.Компьютер = ИмяКомпьютера();
		Запись.Сеанс = НомерСеансаИнформационнойБазы();
		НаборЗаписей.Записать();
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
	КонецПопытки;
	УстановитьПривилегированныйРежим(Ложь);
	
КонецПроцедуры



Функция ОсвободитьОбъект(знач Объект) Экспорт
	Результат=0;
	УстановитьПривилегированныйРежим(Истина);
	НачатьТранзакцию();
	Попытка
		Блокировка = Новый БлокировкаДанных;
		ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.БлокировкиРедактированияОбъектов");
		ЭлементБлокировки.УстановитьЗначение("Объект", Объект);
		Блокировка.Заблокировать();
		НаборЗаписей = РегистрыСведений.БлокировкиРедактированияОбъектов.СоздатьНаборЗаписей();
		НаборЗаписей.Отбор.Объект.Установить(Объект);
		НаборЗаписей.Записать();
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		Результат=1;
	КонецПопытки;
	УстановитьПривилегированныйРежим(Ложь);
	Возврат  Результат;
КонецФункции



Получение информации о захвате (используется практически везде — и в проверке возможности открытия, и в подписке на событие). Важный момент: если объект захвачен текущим же пользователем — это не считается «занято»:

Функция ПолучитьИнформациюОЗахвате(знач Объект) Экспорт
	Результат = Новый Структура("Захвачен, Пользователь, ВремяЗахвата", Ложь, Неопределено, Неопределено);
	УстановитьПривилегированныйРежим(Истина);
	ДанныеЗахвата = РегистрыСведений.БлокировкиРедактированияОбъектов.Получить(Новый Структура("Объект", Объект));
	УстановитьПривилегированныйРежим(Ложь);
	Если НЕ ЗначениеЗаполнено(ДанныеЗахвата.Пользователь) Тогда
		Возврат Результат;
	КонецЕсли;
	Если ДанныеЗахвата.Пользователь = Пользователи.ТекущийПользователь() Тогда
		Возврат Результат;
	КонецЕсли;
	Результат.Захвачен = Истина;
	Результат.Пользователь = ДанныеЗахвата.Пользователь;
	Результат.ВремяЗахвата = ДанныеЗахвата.ВремяЗахвата;
	Возврат Результат;
КонецФункции



2.4. Проверка «живости» сессии владельца блокировки

Это ядро того, что отвечает на вопрос из темы — как отличить «документ реально редактируют» от «блокировка осиротела»:


Функция СессияЗахватаАктивна(Объект)
	ДанныеЗахвата = РегистрыСведений.БлокировкиРедактированияОбъектов.Получить(Новый Структура("Объект", Объект));
	Если НЕ ЗначениеЗаполнено(ДанныеЗахвата.Сеанс) Тогда
		Возврат Ложь;
	КонецЕсли;
	Сеансы = ПолучитьСеансыИнформационнойБазы();
	УстановитьПривилегированныйРежим(Истина);
	Для Каждого Сеанс Из Сеансы Цикл
		Если Сеанс.НомерСеанса = ДанныеЗахвата.Сеанс Тогда
			УстановитьПривилегированныйРежим(Ложь);
			Возврат Истина;
		КонецЕсли;
	КонецЦикла;
	УстановитьПривилегированныйРежим(Ложь);
	Возврат Ложь;
КонецФункции



И функция, которую дёргает форма при открытии документа — она же решает, можно ли вообще предложить пользователю «вмешаться»:


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



То есть кнопки «Перехватить» / «Снять» появятся только если одновременно выполнены два условия: у пользователя есть роль `Документы_СнятиеБлокировкиЗахвата`, и сеанс, в котором документ был захвачен, уже не существует. Если сеанс жив — хоть с той же ролью — вмешаться нельзя, будет только «занято».

2.5. Принудительные действия

Обе процедуры дополнительно проверяют роль на входе (не полагаясь только на то, что клиент до этого правильно посчитал `МожноВмешаться`), выполняются в привилегированном режиме


Процедура ПерехватитьОбъект(знач Объект) Экспорт
	Если НЕ (РольДоступна("Документы_СнятиеБлокировкиЗахвата") 
		ИЛИ ПривилегированныйРежим())Тогда
		ВызватьИсключение "Недостаточно прав";
	КонецЕсли;
	УстановитьПривилегированныйРежим(Истина);
	ЗахватитьОбъект(Объект);
	УстановитьПривилегированныйРежим(Ложь);
КонецПроцедуры


Процедура ПринудительноСнятьБлокировку(знач Объект) Экспорт
	Если НЕ (РольДоступна("Документы_СнятиеБлокировкиЗахвата") 
		ИЛИ ПривилегированныйРежим())Тогда
		ВызватьИсключение "Недостаточно прав";
	КонецЕсли;
	ДанныеЗахвата = РегистрыСведений.БлокировкиРедактированияОбъектов.Получить(Новый Структура("Объект", Объект));
	УстановитьПривилегированныйРежим(Истина);
	ОсвободитьОбъект(Объект);
	УстановитьПривилегированныйРежим(Ложь);
	
КонецПроцедуры



`ЗахватитьОбъект` внутри `ПерехватитьОбъект` дополнительно оборачивается в привилегированный режим — это нужно, чтобы уполномоченный пользователь мог захватить объект в обход своих собственных прав на регистр (которых у рядового пользователя обычно и не должно быть напрямую — запись в регистр всегда идёт через сервер).

2.6. Общий клиентский модуль — `БлокировкаОбъектовКлиент`

Процедура ОбработатьОткрытие(знач СсылкаНаОбъект,Отказ) Экспорт
	Если СсылкаНаОбъект.Пустая() Тогда
		Возврат;
	КонецЕсли;
	Проверка = БлокировкаОбъектовСервер.ПроверитьВозможностьОткрытияРасширенная(СсылкаНаОбъект);
	Если Проверка.Разрешено Тогда
		Возврат;
	КонецЕсли;
	Если Проверка.МожноВмешаться Тогда
		Кнопки = Новый СписокЗначений;
		Кнопки.Добавить("Перехватить", "Перевести блокировку на себя");
		Кнопки.Добавить("Снять", "Снять блокировку");
		Кнопки.Добавить("Отмена", "Отмена");
		ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыбораДействияСБлокировкой", ЭтотОбъект,СсылкаНаОбъект);
		ПоказатьВопрос(ОписаниеОповещения,
			Проверка.Сообщение + Символы.ПС + "Сессия этого пользователя сейчас неактивна.", Кнопки, , "Отмена");
	Иначе
		ПоказатьПредупреждение(, Проверка.Сообщение);
	КонецЕсли;
	Отказ=Истина;
КонецПроцедуры


Функция ОсвободитьОбъект(знач Объект) Экспорт
    Возврат БлокировкаОбъектовСервер.ОсвободитьОбъект(Объект);
КонецФункции

Процедура ПослеВыбораДействияСБлокировкой(РезультатВопроса, Ссылка) Экспорт
	Если РезультатВопроса = Неопределено 
		ИЛИ РезультатВопроса = "Отмена" Тогда
		Возврат;
	КонецЕсли;
	Если РезультатВопроса = "Перехватить" Тогда
		БлокировкаОбъектовСервер.ПерехватитьОбъект(Ссылка);
		ПоказатьЗначение(,Ссылка);
	ИначеЕсли РезультатВопроса = "Снять" Тогда
		БлокировкаОбъектовСервер.ПринудительноСнятьБлокировку(Ссылка);
	Иначе
		Возврат;
	КонецЕсли;
КонецПроцедуры



2.7. Подключение к форме документа

&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
	Если объект.Ссылка.Пустая() = Истина Тогда
		ЗаполнитьРеквизитыШапкиНовогоОбъекта();
	Иначе
		Если НЕ БлокировкаОбъектовСервер.ПолучитьИнформациюОЗахвате(Объект.Ссылка).Захвачен Тогда
			БлокировкаОбъектовСервер.ЗахватитьОбъект(Объект.Ссылка);
		КонецЕсли;
	КонецЕсли;
	ИнициализацияФормыОбъекта();
КонецПроцедуры


&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
    БлокировкаОбъектовСервер.ЗахватитьОбъект(Объект.Ссылка);
КонецПроцедуры

&НаКлиенте
Процедура ПриОткрытии(Отказ)
    БлокировкаОбъектовКлиент.ОбработатьОткрытие(Объект.Ссылка,отказ);
    Если Отказ Тогда
        Возврат;
    КонецЕсли;
    // ... остальная логика формы, которая не должна выполняться,
    // если открытие отменено из-за блокировки
КонецПроцедуры

&НаКлиенте
Процедура ПриЗакрытии(ЗавершениеРаботы)
    Если НЕ ЗавершениеРаботы Тогда
        БлокировкаОбъектовКлиент.ОсвободитьОбъект(Объект.Ссылка);
    КонецЕсли;
КонецПроцедуры

</PRE>

Таким образом жизненный цикл блокировки полностью привязан к жизненному циклу формы документа: открыли — захватили (или обновили захват, если уже держим сами), закрыли форму — освободили. `ПриЗакрытии` не освобождает объект только при полном завершении работы клиента (`ЗавершениеРаботы = Истина`) — блокировка останется висеть до штатного завершения сеанса платформой, и именно для этого случая нужен весь механизм из п. 2.4 с проверкой «живости» сеанса.

Чтобы подключить блокировку к ещё одному документу, нужно: добавить его ссылку и объект в состав соответствующих определяемых типов (`СсылкаНаОбъектСБлокировкойРедактирования` и `ОбъектСБлокировкойРедактирования`) и повторить эти четыре вставки в его форме — никакой документ-специфичной логики в общих модулях нет.
Чтобы обнаруживать ошибки, программист должен иметь ум, которому доставляет удовольствие находить изъяны там, где, казалось, царят красота и совершенство. Фредерик Брукс-младший