Про OCIBreak и принудительное прерывание обращения к БД

Я как-то уже не раз писал о том, что мы не используем стандартные компоненты доступа к БД. Почти всё самописное. И работаем мы с Oracle.

Недавно я, наконец-таки, сделал “фишку”, без которой вполне можно жить, но с ней приятнее.

Представьте, что у вас есть запрос к БД, который выполняется длительное время. Ну, например, пользователь указал слишком мягкие критерии для фильтрации данных. Или индекса в БД нет. Или запрос изначально “кривой”. Или всё вместе взятое… Для прерывания выполнения текущего обращения к серверу в OCI есть стандартная функция – OCIBreak.

У нас я реализовывал так: в отдельном потоке запускается запрос к серверу. Если запрос выполняется длительное время, то появляется модальное окошко с кнопкой [Прервать]:

image

По завершению запроса – окошко скрывается. Если пользователь успеет нажать кнопку – вызывается OCIBreak,  и запрос корректно прерывается.

Таймаут до появления такого окошка установлен в две секунды – большинство запросов отрабатывают гораздо быстрее. Но если вдруг пользователь видит это окно, то на интуитивном уровне он может догадаться, что надо как-то ограничивать параметры своего запроса.

Однако иногда бывает так, что OCIBreak не прерывает запрос. Точнее он его прерывает, но приходится долго ждать. Это встречается у нас всё реже, но встречается, обычно в старых запросах, когда клиент говорит серверу – мол сделай то-то, а я подожду. И пока сервер не закончит транзакцию – приложение как бы “висит”. А если пользователь испугался и нажал [Перервать] – начинается откат транзакции. И пользователь снова ждёт, пока сервер не отпустит транзакцию. А приложение – продолжает “висеть”.  И, по хорошему, дождаться бы. Но это раздражает, и есть “продвинутые” пользователи, которые тупо прекращают выполнение программы через диспетчер задач.

Вот для таких, довольно редких случаев, я реализовал дополнительную “фишку” – принудительное прерывание. Работает так: если в течении 5 секунд OCIBreak не отпустил обращение к БД, то кнопка [Перервать] превращается в [Принудительно] и её снова можно нажать.

Что же происходит в этом случае? (Сначала я пробовал убить поток, выполняющий обращение к серверу, но это, конечно же, ничем хорошим не кончилось.)

При нажатии на кнопку [Принудительно] я делаю две вещи:

  • запускаю отдельным потоком вторую сессию к БД и выполняю: alter system kill session »:sid, :serial» immediate;
  • разрываю текущее TCP-соединение на стороне приложения.

Первый пункт нужен, чтобы сервер понял о наших намерениях – мы не собираемся больше ждать. Второй пункт для меня был не тривиальным. Не вдаваясь в подробности поиска решения, привожу код модуля, который у меня получился:

unit MyMIBUtils;

interface

uses
  Windows;

type
  ULONG = Integer;
  PVOID = Pointer;

const
  ANY_SIZE = 1;
  AF_INET = 2;

type
  PMIB_TCPROW = ^MIB_TCPROW;
  _MIB_TCPROW_W2K = packed record
    dwState: DWORD;
    dwLocalAddr: DWORD;
    dwLocalPort: DWORD;
    dwRemoteAddr: DWORD;
    dwRemotePort: DWORD;
  end;
  MIB_TCPROW = _MIB_TCPROW_W2K;
  TMibTcpRow = MIB_TCPROW;
  PMibTcpRow = PMIB_TCPROW;

const
  MIB_TCP_STATE_CLOSED = 1;
  MIB_TCP_STATE_LISTEN = 2;
  MIB_TCP_STATE_SYN_SENT = 3;
  MIB_TCP_STATE_SYN_RCVD = 4;
  MIB_TCP_STATE_ESTAB = 5;
  MIB_TCP_STATE_FIN_WAIT1 = 6;
  MIB_TCP_STATE_FIN_WAIT2 = 7;
  MIB_TCP_STATE_CLOSE_WAIT = 8;
  MIB_TCP_STATE_CLOSING = 9;
  MIB_TCP_STATE_LAST_ACK = 10;
  MIB_TCP_STATE_TIME_WAIT = 11;
  MIB_TCP_STATE_DELETE_TCB = 12;

type
  TCP_TABLE_CLASS = Integer;

const
  TCP_TABLE_BASIC_LISTENER = 0;
  TCP_TABLE_BASIC_CONNECTIONS = 1;
  TCP_TABLE_BASIC_ALL = 2;
  TCP_TABLE_OWNER_PID_LISTENER = 3;
  TCP_TABLE_OWNER_PID_CONNECTIONS = 4;
  TCP_TABLE_OWNER_PID_ALL = 5;
  TCP_TABLE_OWNER_MODULE_LISTENER = 6;
  TCP_TABLE_OWNER_MODULE_CONNECTIONS = 7;
  TCP_TABLE_OWNER_MODULE_ALL = 8;

type
  PMIB_TCPROW_OWNER_PID = ^MIB_TCPROW_OWNER_PID;
  MIB_TCPROW_OWNER_PID = packed record
    dwState: DWORD;
    dwLocalAddr: DWORD;
    dwLocalPort: DWORD;
    dwRemoteAddr: DWORD;
    dwRemotePort: DWORD;
    dwOwningPid: DWORD;
  end;
  TMibTcpRowOwnerPid = MIB_TCPROW_OWNER_PID;
  PMibTcpRowOwnerPid = PMIB_TCPROW_OWNER_PID;

  PMIB_TCPTABLE_OWNER_PID = ^MIB_TCPTABLE_OWNER_PID;
  MIB_TCPTABLE_OWNER_PID = packed record
    dwNumEntries: DWord;
    Table: array [0..ANY_SIZE - 1] of MIB_TCPROW_OWNER_PID ;
  end;
  TMibTcpTableOwnerPid = MIB_TCPTABLE_OWNER_PID;
  PMibTcpTableOwnerPid = PMIB_TCPTABLE_OWNER_PID;


function SetTcpEntry(const pTcpRow: MIB_TCPROW): DWORD; stdcall;
function GetExtendedTcpTable(pTcpTable: PVOID; var dwSize: DWORD; bOrder: BOOL;
  ulAf: ULONG; TableClass: TCP_TABLE_CLASS; Reserved: ULONG): DWORD; stdcall;

function KillProcessAllTCPConnections(AProcessId: DWORD): DWORD;

implementation

const
  iphlpapilib = 'iphlpapi.dll';

function SetTcpEntry; external iphlpapilib name 'SetTcpEntry';
function GetExtendedTcpTable; external iphlpapilib name 'GetExtendedTcpTable';

function KillProcessAllTCPConnections(AProcessId: DWORD): DWORD;
var
  TCPTable: PMibTcpTableOwnerPid;
  Size: DWORD;
  Res: DWORD;
  I: DWORD;
  TCPRow: TMibTcpRow;
begin
  Result := 0;
  TcpTable := nil;
  Size := 0;
  Res := GetExtendedTcpTable(TCPTable, Size, False, AF_INET, TCP_TABLE_OWNER_PID_CONNECTIONS, 0);
  if Res <> ERROR_INSUFFICIENT_BUFFER then
    Exit;
  GetMem(TCPTable, Size);
  try
    Res := GetExtendedTcpTable(TCPTable, Size, False, AF_INET, TCP_TABLE_OWNER_PID_CONNECTIONS, 0);
    if Res <> NO_ERROR then
      Exit;

    for I := 0 to TCPTable^.dwNumEntries - 1 do
      if TCPTable^.Table[I].dwOwningPID = AProcessId then
        with TCPTable^.Table[I] do
        begin
          TCPRow.dwState := MIB_TCP_STATE_DELETE_TCB;
          TCPRow.dwLocalAddr := dwLocalAddr;
          TCPRow.dwLocalPort := dwLocalPort;
          TCPRow.dwRemoteAddr := dwRemoteAddr;
          TCPRow.dwRemotePort := dwRemotePort;
          Res := SetTCPEntry(TCPRow);
          if Res = NO_ERROR then
            Inc(Result);
        end;
  finally
    FreeMem(TCPTable);
  end;
end;

end.

Этот код работает на Windows XP with SP2 и выше.

Соответственно я вызываю:

  KillProcessAllTCPConnections(GetCurrentProcessId);

и все текущие TCP-соединения моего процесса прерываются (а у меня оно всего одно). Получить информацию о TCP-соединении, которое используется именно текущим OCI-обращением к серверу мне не удалось, да я особо и не пытался. Если вдруг понадобится – это можно сделать, просмотрев активные соединения непосредственно до и после коннекта к БД.

 

P.S.: Пару слов про NonBlocking-mode, который есть в OCI. В современном мире многопоточных операционных систем его не рекомендуется использовать вовсе.

Читать на сайте автора.

Delphi Event bus

Решились на работе поддержать Delphi сообщество и начать выкладывать в open source собственные наработки. 

Первой ласточкой суждено было стать пакету DelphiEventBus — реализация паттерна проектирования Event Bus.  
В Jаva мире есть такие пакеты как guava-libraries, но в Delphi ничего похожего найти не удалось. Потому решено было запилить нечто подобное. 

Из статьи Java event bus library comparison можно выцепить характеристики библиотек реализующих шину сообщений. 

Для DelphiEventBus получается следующие:

  • Объявление слушателя — аннотация

  • Синхронность отправки в шину — по умолчанию отправка синхронна. 

  • Асинхронность — в планах на будующее

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

  • Иерархия событий — да. Событие это объект. Есть базовый класс всех событий. Обработчик может ждать события определенного класса и всех его наследников

  • Строгость ссылочности листенера — строгая. Обязательная дерегистрация. Регистрируются и дерегистрируются  сразу все обработчики в листенере

  • Приоритет обработчиков — отсутствует. Но дополнительно реализован механизм хуков, похожий на виндовый механизм.

  • Реализованы хуки — возможность игнорировать фильтры и выстраивать цепочки обработки. Вызов хуков строго обратен порядку регистрации.

Должно работать на XE3 и выше. Еще бы readme перевести на английский…


Читать на сайте автора.

Коррекция шрифта в Object Inspector’е

Пробую Delphi XE7. Пока нравится. Поставил GExperts – очень уж я привык к сочетаниям Ctrl+Alt+Down и Ctrl+Alt+Up – переход к следующему/предыдущему объявлению идентификатора. CnPackWizards пока не вышел, но и без него вполне можно жить.
И тут вдруг бросилось в глаза, что у Object Inspector’а шрифт не такой, как везде… (везде – Tahoma 8, а тут – Segoe UI 9)
В GExperts вроде и была возможность менять шрифт инспектора, но почему-то пропала. Поэтому “накидал” на скорую руку пакет, исправляющий данную особенность, результат на картинке (было – стало):
image
Исходник пакета доступен на GitHub, или можно скачать zip-архив отсюда – в  папке ObjInspFntChngr. В модуле uObjInspFntChngr.pas через константы:

const
  PreferParentFont = True;
  PreferFontName = 'Tahoma';
  PreferFontSize = 8;

можно настроить шрифт по своему вкусу.

HINT: Возможно пакет поможет тем, у кого масштабирование текста в Windows больше 100%.

Для установки – открыть dproj-файл, правой кнопкой мыши в Project Manager’е – Install. Пакет можно использовать и в предыдущих версиях Delphi, для этого достаточно удалить dproj-файл и открыть пакет через dpk-файл.

Читать на сайте автора.

Helper для TMessage

В предыдущей заметке я упомянул о хелпере для отладки сообщений Windows. Эта заметка будет предельно краткой: ссылки на исходники и скриншот демки.

Исходник: модуль MsgHlpr.pas. Использовать можно так:

procedure TfrmMain.ApplicationEvents1Message(var Msg: TMsg; var Handled: Boolean);
begin
  Memo1.Lines.Add(Msg.ToString);
..

Демо-приложение:

image

Скачать: MsgHlpr.pas + демо — zip-архив, MsgHlpr.pas на GitHub, исходник демо на GitHub, исполняемый exe-файл демки (zip-архив, 447 КБ).

Работает в Delphi 2010 и выше, при желании легко адаптировать и к предыдущим версиям Delphi.

Читать на сайте автора.

Используем макросы в IDE-редакторе Delphi

Занимаясь отладкой процедур, связанных с обработкой сообщений, постоянно приходится делать сопоставление между кодом сообщения и его строковым наименованием. Ну, к примеру, «прилетает» код 6 — это WM_ACTIVATE. Или сложнее: код 274 (0x0112) = WM_SYSCOMMAND.

Мне это порядком надоело — решил сделать Helper для TMessage (попутно и для TMsg). Ну и в планах попробовать сделать Debugger Visualizer.
Однако о самом хелпере я постараюсь написать в следующий раз. В этой заметке хочу описать, как можно использовать кнопочки Record Macro и Playback Macro, которые находятся в левом нижнем углу строки состояния редактора кода.

(Кстати, мне впервые в жизни пришло в голову попробовать их использовать.)

Итак, я хочу получить код вида:
  case Msg of
    WM_NULL: Result := 'WM_NULL';
    WM_CREATE: Result := 'WM_CREATE';
    ...
    WM_APP: Result := 'WM_APP';
  end;

Идём в модуль Messages и копируем оттуда код:
  {$EXTERNALSYM WM_NULL}
  WM_NULL             = $0000;
  {$EXTERNALSYM WM_CREATE}
  WM_CREATE           = $0001;
  ...
  {$EXTERNALSYM WM_APP}
  WM_APP = $8000;

Вставляем в новый модуль и начинаем макрос. По шагам:
  1. Нажимаю Ctrl+F (Панель поиска), указываю пробел, снимаю все флажки. Enter — чтобы запомнилось.
  2. Устанавливаю курсор на первой строке, нажимаю «Record Macro»:.
  3. Ctrl+Y — удаляем строку
  4. Ctrl+Вправо — курсор к началу идентификатора
  5. F3 — поиск до пробела
  6. Влево — курсор к концу идентификатора
  7. Ctrl+Shift+Влево — выделили идентификатор
  8. Ctrl+C — скопировали выделенное в буфер обмена
  9. Повторяем 5. и 6. — курсор к концу идентификатора
  10. Shift+End — выделение до конца строки
  11. Delete — удаляем выделенное
  12. Набираем на клавиатуре
    : Result := ‘
  13. Ctrl+V — вставили скопированное
  14. Набираем на клавиатуре
    ‘;
  15. Home — переход к началу строки
  16. Вниз — переход к следующей строке
  17. Нажимаю «Stop Recording Macro»:.
Макрос готов, теперь просто жамкаем в «Playback Macro»
пока не достигнем нужного результата.

Читать на сайте автора.

Для расовых ненавистников "with"

Коим я являюсь (ненавистником with)

Добрые люди предложили замену:

(procedure (A: TObject)

  begin
    A.Free;
  end)(TObject.Create);

Читать на сайте автора.

Простой текстовый итератор. Пример использования record в Delphi

Задача: осуществить перебор строковых значений в исходной строке, разделённых символом-разделителем, с учётом символа-кавычки.
Решение:

unit dnStringIterator;

interface

type
  TStringValuesIterator = record
  type
    TCallBack = reference to procedure (const AValue: string);
    TOptions = set of (ioTrimValues, ioDequoteValues);
  private
    FSourceString: string;
    FDelimiter, FQuoteChar: Char;
    FCheckQuotes: Boolean;
    FOptions: TOptions;
    FOffset, FLength: Integer;
    FCurrent: string;
  public
    constructor Init(const ASourceString: string; ADelimiter: Char; AQuoteChar: Char = #0; AOptions: TOptions = []);
    function GetEnumerator: TStringValuesIterator;
    function MoveNext: Boolean;
    procedure Run(ACallBack: TCallBack); inline;
    property Current: string read FCurrent;
  end;

implementation

uses
  SysUtils;

{ TStringValuesIterator }

constructor TStringValuesIterator.Init(const ASourceString: string; ADelimiter, AQuoteChar: Char; AOptions: TOptions);
begin
  FSourceString := ASourceString;
  FDelimiter := ADelimiter;
  FQuoteChar := AQuoteChar;
  FOffset := 1;
  FLength := Length(FSourceString);
  FOptions := AOptions;
  FCurrent := '';

  FCheckQuotes := FQuoteChar <> #0;

  // нельзя, чтобы символ разделитель совпадал с символом-кавычкой:
  Assert(not FCheckQuotes or (FDelimiter <> FQuoteChar));
  // нельзя использовать ioDeqouteValues, если не указан QuoteChar
  Assert(FCheckQuotes or not (ioDequoteValues in FOptions));
end;

function TStringValuesIterator.GetEnumerator: TStringValuesIterator;
begin
  Result := Self;
end;

function TStringValuesIterator.MoveNext: Boolean;
var
  IsInQuote: Boolean;
  CurPos: Integer;
  Ch: Char;
begin
  Result := (FLength > 0) and (FOffset 

Примеры использования

С явным объявлением дополнительной переменной:
var
  svi: TStringValuesIterator;
begin
  svi.Init(TestString, ',');
  while svi.MoveNext do
    Memo1.Lines.Add(svi.Current);
end;

Без явного объявления дополнительной переменной, используя with:

begin
  with TStringValuesIterator.Init(TestString, ',') do
    while MoveNext do
      Memo1.Lines.Add(Current);
end;

Используя анонимную процедуру:

begin
  TStringValuesIterator.Init(TestString, ',').Run(
    procedure (const AValue: string)
    begin
      Memo1.Lines.Add(AValue);
    end
  );
end;

Используя for-in синтаксис:

var
  Tmp: string;
begin
  for Tmp in TStringValuesIterator.Init(TestString, ',') do
    Memo1.Lines.Add(Tmp);
end;

Читать на сайте автора.

Калькулятор восхода солнца, заката и истинного полудня

Читать на сайте автора.

Отправка сообщений в Microsoft Outlook. Скрытие окон безопасности

Всем доброго времени суток дорогие читатели блога. Давно я ничего не писал в блог, на этот раз решил исправиться. Передо мной стала задача разработать систему заявок на одном предприятии. Так вот, в одном приложении мне необходимо было сделать, чтобы копия заявки приходила на электронный адрес исполнителя. Все система моя работает на MS SQL Server, электронные адреса исполнителей хранятся там же.

На предприятии у нас работает Microsoft Outlook (установлен сервер) и соответственно на каждом рабочем месте свой клиент с учетной записью. Программа должна записывать заявку в базу данных, а копию отправлять по электронной почте, подключаясь к почтовому ящику пользователя (под его учетной записью, то есть, от его имени).

В принципе тут ничего сложного нет, можно для этих целей использовать MAPI, но минус в этом заключается в том, что когда используется MAPI, то пользователю показывается сообщение о том, что к Вашему ящику пытаются получить доступ, это может быть вредоносное ПО и так далее. Эта лишняя информация для пользователя (в моем случае), потому что они начнут паниковать, когда будут составлять заявку, будут звонить, что вредоносное ПО и так далее. Мне пришлось решать проблему по поводу скрытия данного уведомления. На предприятии у нас используется Microsoft Outlook 2003, поэтому в настройках безопасности там нельзя было выключить данный пункт, по крайней мере, я не знал, как это сделать.

Программа установлена более чем на 100 компьютерах. На некоторых компьютерах мне удалось решить данную проблему при помощи установки Microsoft Outlook Express, так как там имеется пункт по поводу отключения безопасности. В этом случае, отправка сообщений производится в фоном режиме, то есть, пользователю не придется нажимать на кнопку Да, что якобы какое-то программное обеспечение пытается получить доступ к учетной записи в электронной почте. На остальные компьютерах по прежнему был установлен Microsoft Outlook 2003 и там никак нельзя было менять его на более низкую версию. Полазив немного в Интернете, я нашел довольно интересную программу, которая называется ClickYes.

Программа очень полезная и интересная, по крайней мере, в моих целях она мне очень пригодилась. Но один ее очень большой минус – это то, что она является платной. Как Вам сказать платной, есть более ранние версии программы – они бесплатны, а уже новые версии, начиная с 2010 года – уже платные. На нее имеется тестовый период в 30 дней. Программа ClickYes предназначена для того, чтобы скрывать это самое окно безопасности в Microsoft Outlook.

Бесплатные версии его не скрывают, а просто нажимают на кнопку Да, в течение 10 секунд, то есть, пользователь видит это окно все равно и может сам нажать на него в течение 10 секунд, что касается версии с 2010 года (платная версия), то она полностью скрывает это окно безопасности и пользователь его вообще не видит. Конечно, можно было что-то подобное реализовать у себя, но я стал искать простой способ.

Этот способ заключался в том, что я возвращаю на все компьютеры версию Outlook до 2003 (Microsoft Outlook Express), а на сервере (где установлена база данных и серверная часть программы) устанавливаю Microsoft Outlook Express. С сервера и будет отправляться копия заявки на нужные адресаты (за это будет отвечать серверная часть программы). Так как в Outlook Express можно в настройках отключить уведомления безопасности, то я решил таким способом. Насколько я знаю, что в более старших версиях OutLook (после 2003) тоже можно отключать данные уведомления.

Самый простой способ – это установить Outlook Express на сервер и с него отправлять электронную почту на любые адреса (без разницы какой там будет установлен Outlook).

На сервере можно использовать следующий способ отправки сообщений при помощи MAPI:

procedure TForm3.Button2Click(Sender: TObject);
var
   lMail: TMapiControl;
begin
   try
      RichEdit2.Lines.Add('текст письма');
      lMail := TMapiControl.Create(Self);
       lMail.Reset;
       lMail.Recipients.Add(PChar('адресаты');
       lMail.Subject:='Заявка от: '+Ini.ReadString('Authorization','login','');
       lMail.Body:=RichEdit2.Text;
       lMail.ShowDialog:=False; //Показывать перед отправкой диалог мейл-клиента по умолчанию или нет.
     lMail.Sendmail;
    lMail.Free;
   except
    on e:Exception do
   end;
end;

Не забудьте в usesподключить модуль MAPI. Что касается адресатов, то можно отправлять сразу нескольким, тогда Вам придется каждого указывать через точку запятой. Думаю, что тут все понятно, особо ничего сложного нет.

Подведем итоги: если Вам требуется отправить письмо из своей программы при помощи клиента Microsoft Outlook и чтобы не появлялось окно безопасности, то можно воспользоваться версией Outlook Express (необходимо в настройках безопасности будет выставить), либо же другие версии, где эти уведомления отключаются. Можете разработать функционал, который сам будет скрывать данные окна (нажимать на нужные кнопки). Можете использовать сервер отправки сообщений, как я привел выше, а можете использовать программу ClickYes, которая очень мне понравилась, причем она не слишком дорогая. В конце я прикладываю скриншот одного модуля своей системы заявок, из которого осуществляется отправка электронной почты:

modul_zayv

Кстати, еще очень удобно отправлять сообщения из Outlook Express, так как она понимает HTML-разметку, и Вы с легкостью можете формировать внешний вид своего сообщения как Вам захочется. Это еще один плюс в пользу того, что лучше использовать Outlook Expres sна том же самом сервере. Не забудьте посмотреть статью про MAPI, там тоже имеется полезная информация.

Читать на сайте автора.