|
Блокировка объекта при работе с ним (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 с проверкой «живости» сеанса. Чтобы подключить блокировку к ещё одному документу, нужно: добавить его ссылку и объект в состав соответствующих определяемых типов (`СсылкаНаОбъектСБлокировкойРедактирования` и `ОбъектСБлокировкойРедактирования`) и повторить эти четыре вставки в его форме — никакой документ-специфичной логики в общих модулях нет. |
| Форум | Правила | Описание | Объявления | Секции | Поиск | Книга знаний | Вики-миста |