Вызов консольного приложения из программы на Delphi

Для вызова внешнего приложения из программы на Delphi обычно используется функция WinAPI CreateProcess. Но как получить то, что это приложение выводит в консоль, и, например, отобразить в логе программы?  Варианта два. Первый: пере-направить вывод консольного приложения в файл и потом прочитать этот файл. Однако, создавать временный файл не охота. А кроме того, такой способ не всегда хорошо работает. Есть другой способ - использовать пайпы (pipes). Ниже приведен код процедуры RunDosCommand на Delphi, который использует этот способ. Весь вывод на консоль записывается в AMemo: TMemo.

function RunDosCommand(ACmdString: String; AMemo: TMemo;
AReadPipe, AWritePipe: THandle): Boolean;
const
ReadBuffer = 4200;
var
Security : TSecurityAttributes;
ReadPipe, WritePipe: THandle;
start : TStartUpInfo;
ProcessInfo : TProcessInformation;
Buffer : Pchar;
BytesRead : DWord;
begin
Result := False;
if AMemo <> nil then
begin
AMemo.SelStart := Length(AMemo.Text);
AMemo.SelText := ACmdString + #13#10;
AMemo.SelStart := Length(AMemo.Text);
Application.ProcessMessages;
end;
ReadPipe := AReadPipe;
WritePipe := AWritePipe;
with Security do
begin
nLength := SizeOf(TSecurityAttributes);
bInheritHandle := true;
lpSecurityDescriptor := nil;
end;
if (ReadPipe = 0) and (WritePipe = 0) then
begin
if not CreatePipe(ReadPipe, WritePipe, @Security, 0) then
begin
ReadPipe := 0;
WritePipe := 0;
end;
end;
if (ReadPipe <> 0) or (WritePipe <> 0) then
begin
if AMemo <> nil then
begin
Buffer := AllocMem(ReadBuffer + 1);
end;
FillChar(Start, Sizeof(Start), #0);
start.cb := SizeOf(start) ;
start.hStdOutput := WritePipe;
start.hStdError := WritePipe;
start.hStdInput := ReadPipe;
start.dwFlags := STARTF_USESTDHANDLES +
STARTF_USESHOWWINDOW;
start.wShowWindow := SW_HIDE;
if CreateProcess(nil,
PChar(ACmdString),
@Security,
@Security,
true,
NORMAL_PRIORITY_CLASS,
nil,
nil,
start,
ProcessInfo) then
begin
WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
if AMemo <> nil then
begin
BytesRead := 0;
ReadFile(ReadPipe, Buffer[0], ReadBuffer, BytesRead, nil);
Buffer[BytesRead] := #0;
AMemo.SelStart := Length(AMemo.Text);
AMemo.SelText := String(Buffer);
AMemo.SelStart := Length(AMemo.Text);
Application.ProcessMessages;
end;
Result := True;
end else
if AMemo <> nil then
begin
AMemo.SelStart := Length(AMemo.Text);
AMemo.SelText :='Command failed.'#13#10;
AMemo.SelStart := Length(AMemo.Text);
Application.ProcessMessages;
end;
if AMemo <> nil then
begin
FreeMem(Buffer);
end;
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
if (AReadPipe = 0) and (AWritePipe = 0) then
begin
CloseHandle(ReadPipe);
CloseHandle(WritePipe);
end;
end;
end;

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Преобразование _TCHAR в QString

Для преобразования _TCHAR в QString и обратно (QString в _TCHAR) я объявил в tqchar.h пару простых макросов: _TQSTRING_TQCHAR.

#ifndef TQCHAR_H
#define TQCHAR_H

#include <tchar.h>

#ifdef _UNICODE

#define _TQCHAR(x) (wchar_t*) x.utf16()
#define _TQSTRING(x) QString::fromUtf16((x))

#else

#define _TQCHAR(x) x.toLocal8Bit().constData()
#define _TQSTRING(x) QString::fromLocal8Bit((x))

#endif

#endif //TQCHAR_H

Кстати, нашел хороший плагин для Visual Studio чтобы копировать код в HTML с подсветкой синтаксиса: Copy As HTML.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

ASProtect 1.65 - Delphi XE2 Compatible

Наконец-то появилась версия ASProtect 1.65 в которой заявлена совместимость с Delphi XE и XE2. Я попробовал - созданный в Delphi XE2 и запакованный новым “аспром” exe действительно запускается и работает. Ошибок пока не замечено.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

VerQueryValue Access Violation

Столкнулся с интересным багом при получении информации о версии из ресурсов (Visual Studio 10, C++).

Access Violation возникает при выполнении VerQueryValue (см. картинку).
Причем, ошибка возникает только если используется Multi-Byte Character Set. А если выставить в свойствах проекта Use Unicode Character Set, то ошибка пропадает. Из этого можно сделать вывод, что баг именно в VerQueryValueA, а в VerQueryValueW его нет.
Что ж, решение проблемы заключается в переписывании функции чтения информации о версии таким образом, чтобы всегда использовалась функция VerQueryValueW. Ниже приведен листинг готовой функции.

UPD 30.01.2014. Спустя полтора года выяснилось, что и с использованием VerQueryValueW возможен Access Violation. Что же, лучше поздно чем никогда. Дело в том, что эта функция рассчитана на то, чтобы быть использованной совместно GetFileVersionInfo. Возможно, VerQueryValue может в каких-то случаях модифицировать ресурс. В общем, фикс заключается в том, что нужно выделить участок памяти (например, с помощью LocalAlloc), скопировать в него информацию о версии из ресурса (CopyMemory), и использовать его при вызове VerQueryValue. Реализация приведена ниже.

void ReadVersion(_TCHAR * szVer, int cbVer, const _TCHAR * szEntry)
{
_tcscpy(szVer, _T("\n"));
HRSRC hVersion = FindResource(
NULL,
MAKEINTRESOURCE(VS_VERSION_INFO),
RT_VERSION);
if (hVersion)
{
DWORD dwSize = SizeofResource(NULL, hVersion);
HGLOBAL hGlobal = LoadResource(NULL, hVersion);
if (hGlobal != NULL)
{
LPVOID versionInfoGlobal = LockResource(hGlobal);
if (versionInfoGlobal != NULL)
{
LPVOID versionInfoLocal = LocalAlloc(LMEM_FIXED, dwSize);
CopyMemory(versionInfoLocal, versionInfoGlobal, dwSize);

DWORD vLen, langD;
BOOL retVal;

LPVOID retbuf = NULL;

#ifdef _UNICODE
const _TCHAR * entry = szEntry;
#else
WCHAR entry[64]; //I assume here that length of szEntry string is less than 64
MultiByteToWideChar(CP_ACP, 0, szEntry, -1, entry, 64);
#endif

WCHAR fileEntry[1024];

_swprintf(fileEntry, L"\\VarFileInfo\\Translation");
retVal = VerQueryValueW(versionInfoLocal, fileEntry, &retbuf, (UINT*)&vLen);
if (retVal && vLen == 4)
{
memcpy(&langD, retbuf, 4);
_swprintf(fileEntry, L"\\StringFileInfo\\%02X%02X%02X%02X\\%s",
(langD & 0xff00)>>8,langD & 0xff,(langD & 0xff000000)>>24,
(langD & 0xff0000)>>16, entry);
}
else
_swprintf(fileEntry, L"\\StringFileInfo\\%04X04B0\\%s",
GetUserDefaultLangID(), entry);

retVal = VerQueryValueW(versionInfoLocal, fileEntry, &retbuf, (UINT*)&vLen);
if (retVal)
{
#ifdef _UNICODE
_sntprintf(szVer, cbVer-1, _T(" %s\n"), (_TCHAR*)retbuf);
szVer[cbVer-1] = '\0';
#else
szVer[0] = '\0';
int n = WideCharToMultiByte(CP_ACP, 0, (LPWSTR)retbuf, -1, szVer+1, cbVer-2, NULL, NULL);
if (n > 0)
{
szVer[0] = ' ';
szVer[n] = '\n';
szVer[n+1] = '\0';
}
#endif
}

LocalFree(versionInfoLocal);
}
FreeResource(hGlobal);
}
}
}

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Qt и Visual Studio 2008

Чтобы использовать Qt совместно со средой разработки Visual Studio нужно выполнить следующие шаги:

1. Скачать и установить Qt for Visual Studio 2008: http://qt.nokia.com/downloads/windows-cpp-vs2008 (каталог установки не должен содержать пробелов, например это может быть C:\qt\4.8.2)

2. Скачать и установить Visual Studio Add-in: http://qt.nokia.com/downloads/visual-studio-add-in

3. Добавить в Переменную среды Path (Пуск->Панель инструментов->Система->Дополнительно->Переменные среды) путь к bin каталогу Qt (например, C:\\qt\\4.8.2\\bin)
Вот, в принципе, все. В Microsoft Visual Studio IDE должен появиться пункт меню Qt. А в диалоге New Project - типы проектов Qt4 Projects.

Есть только два нюанса:
1. Если хочется избавиться от Qt Runtime (статически прилинковать к exe файлу все необходимые библиотеки), то нужно пересобрать Qt с ключом -static:
  • Запустить Visual Studio 2008 Command Prompt
  • Выполнить команду: cd  C:\qt\4.8.2
  • Выполнить команду: configure -platform win32-msvc2008 -static
  • Выполнить команду: nmake

Сборка Qt у меня заняла несколько часов.

2. По-умолчанию библиотеки Qt компилируются в режиме Multi-threaded DLL - /MD (для Debug -  Multi-threaded Debug DLL - /MDd). Что означает наличие зависимостей от msvcr90.dll и msvcp90.dll. Т.е. при установке программы возможно потребуется устанавливать Microsoft Visual C++ 2008 Redistributable Package. Чтобы избавиться от этой зависимости нужно перед сборкой Qt поправить qmake.conf ( C:\qt\4.8.2 \mkspecs\win32-msvc2008\qmake.conf) следующим образом:

заменить строки

  QMAKE_CFLAGS_RELEASE    = -O2 -MD

  QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO += -O2 -MD -Zi

  QMAKE_CFLAGS_DEBUG      = -Zi -MDd

на

  QMAKE_CFLAGS_RELEASE    = -O2 -MT

  QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO += -O2 -MT -Zi

  QMAKE_CFLAGS_DEBUG      = -Zi -MTd

===

Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Зависимость от Microsoft Visual C++ Runtime

Чтобы убрать зависимость от Microsoft Visual C++ Runtime нужно в свойствах проекта выбрать Runtime Library: Multi-threaded (/MT). Получающийся при компиляции exe/dll будет несколько больше, зато не нужно будет устанавливать Visual C++ Redistributable Package.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Проверка серверного сертификата и Enhanced Security

В моем посте про проверку серверного сертификата SSL в Delphi 7 проверка этого самого сертификата выполнялась вызовом функции CryptoAPI CertGetIssuerCertificateFromStore. Я столкнулся на практике со следующим: оказывается, что на эту функцию оказывает влияние Internet Explorer Enhanced Security (по умолчанию оно включено на всех серверах Windows 2003). Т.е. до тех пор, пока хост не будет включен в список доверенных, его сертификат тоже может не проходить проверку. Как-то так (слишком глубоко эту тему не копал). В общем, если на одном компьютере проверка сертификата работает, а на другом - нет (ошибка “Error connecting with SSL”), то следует проверить - включен ли IE Enhanced Security, и если да, то либо отключить его, либо добавить хост в список доверенных.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Delphi XE 2 & ASProtect

ASProtect, к сожалению, не поддерживает приложения, сделанные в Delphi XE 2. Упакованные им exe-ки не запускаются. В Windows Application Log-е появляется соответствующая запись об ошибке в приложении.

Сообщение о несовместимости Delphi XE 2 и ASProtect на официальном форуме датировано 18 ноября 2011, но до сих пор решения нет. Последний ответ от 26 марта 2012 гласит:

Your problem in the queue for the decision and will be solved in the next release of ASProtect (will be released in the nearest future).

Ждем новую версию… Если в ближайшее время не появится, придется переходить на какой-нибудь другой протектор.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Coolsoftware on github

Завел аккаунт в github: https://github.com/coolsoftware/

Это не первый мой аккаунт в github, поэтому потребовалось как-то организовывать одновременный доступ к нему из под разных учеток, с разными SSHKey. По этому поводу маленький “трик” - как я все это сделал:

1. При установке Git for Windows создает такой ярлык для Git Bash:

Как мы видим текущий каталог Git Bash - %HOMEDRIVE%%HOMEPATH%. В нем хранятся все настройки git, каталог .ssh с SSHKey и проекты обычно тоже там.

Я создал в %HOMEDRIVE%%HOMEPATH% каталог Coolsoftware и в нем git.cmd следующего содержания:

@set HOME=%HOMEDRIVE%%HOMEPATH%\Coolsoftware

C:\Windows\SysWOW64\cmd.exe /c “”C:\Program Files (x86)\Git\bin\sh.exe” –login -i”

Затем создал ярлык на этот git.cmd и вынес его на рабочий стол, обозвав “Git Bash - Coolsoftware”. Теперь при его запуске текущий каталог пользователя в “баше” %HOMEDRIVE%%HOMEPATH%\Coolsoftware, а при запуске старого ярлыка “Git Bash” текущий каталог %HOMEDRIVE%%HOMEPATH%. Таким образом получилось два “баша”, в каждом - свои настройки.

Далее запустил “Git Bash - Coolsoftware” произвел установку github в соответствии с инструкцией и выложил проект VHashedStringList - оптимизированный список строк, доступ к элементам которого организован с помощью хеш-индекса.

TVHashedStringList - аналог стандартного THashedStringList (фактически, он сделан на его основе, хотя наследуется не от него, а от TStringList).

Существенное отличие от THashedStringList заключается в том, что оптимизированы операции перестроения индекса при изменении списка.

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

Например, в следующем примере полное перестроение индекса будет произведено 3 раза:

procedure ChangeList(lst: THashedStringList);
begin
lst.Values['key1'] := 'Value1';
lst.Values['key2'] := 'Value2'; //неявный вызов IndexOfName и перестроение индекса
lst.Values['key3'] := 'Value3'; //неявный вызов IndexOfName и перестроение индекса
lst.Values['key4'] := 'Value4'; //неявный вызов IndexOfName и перестроение индекса
end;

В отличие от THashedStringList, TVHashedStringList не требует полного перестроения индекса после добавления нового элемента в конец списка или

при изменении значений элементов списка. Перестроение происходит только после вставки нового элемента в список или удаления из списка.

Поэтому в следующем примере полного перестроения индекса не произойдет ни разу.

procedure ChangeList(lst: TVHashedStringList);
begin
lst.Values['key1'] := 'Value1'; //индекс модифицирован и не требует перестроения
lst.Values['key2'] := 'Value2'; //индекс модифицирован и не требует перестроения
lst.Values['key3'] := 'Value3'; //индекс модифицирован и не требует перестроения
lst.Values['key4'] := 'Value4'; //индекс модифицирован и не требует перестроения
end;

===

Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Много читателей и один писатель

В много-поточном приложении довольно часто возникает необходимость синхронизировать обращение к разделяемому ресурсу, который могут одновременно читать несколько потоков “читателей”, а производить запись в этот ресурс может только один поток “писатель”. Когда читатели читают данные, то писатель не может писать и должен ждать, пока все читатели не закончат чтение. Когда писатель пишет, то все читатели должны ждать, пока он не запишет все данные и не освободит ресурс.

Ниже приведена моя реализация “читателя” и “писателя”. В этой реализации есть допущение о том, что нам известно максимальное количество читателей MAX_READERS.


LONG gCounter = 0;
//алгоритм читателя
for (;;) //бесконечный цикл ожидания освобождения ресурса
{
LONG n = InterlockedIncrement(&gCounter);
//в n - значение gCounter после инкремента
if (n <= MAX_READERS) break; //писатель ничего не пишет - можно читать
InterlockedDecrement(&gCounter);
}
// здесь читаем данные
...
//
InterlockedDecrement(&gCounter); //освобождаем блокировку читателем
// алгоритм писателя
for (;;) //бесконечный цикл освобождения ресурса читателями/писателями
{
LONG n = InterlockedCompareExchange(&gCounter, (MAX_READERS+1), 0);
//в n - предыдущее значение gCounter, которое было ДО попытки заменить его на MAX_READERS+1 в InterlockedCompareExchange;
//если там был 0, то никаких читателей/писателей не было, новое значение в gCounter будет MAX_READERS+1;
//если в gCounter был не 0, то это значение НЕ будет заменено на MAX_READERS+1, а останется прежним
if (n == 0) break;
}
// здесь пишем данные
...
//
InterlockedExchangeAdd(&gCounter, -(MAX_READERS+1)); //освобождаем блокировку писателем

Обращаю внимание на использование Interlocked-функций. Это такие функции, которые обеспечивают атомарность.
Например, InterlockedIncrement(&gCounter) - это атомарное увеличение на 1 (инкремент) значения gCounter.
Вообще, операция инкремента gCounter не атомарна, она состоит из следующих 3-х операций:

1. прочитать значение из памяти по адресу &gCounter
2. увеличить значение на 1
3. записать результат в память по адресу &gCounter

Если выполняется параллельно 2 потока, то возможна такая ситуация :

[поток 1] читает gCounter, прочитан 0
[поток 2] читает gCounter, прочитан 0
[поток 1] увеличивает значение на 1, получается 1
[поток 2] увеличивает значение на 1, получается 1
[поток 1] записывает 1 в gCounter
[поток 2] записывает 1 в gCounter

В итоге в gCounter будет 1, а не 2, как можно было бы предположить.

UPD. LockLib - классы C++ для блокировки разделяемых ресурсов: http://blog.coolsoftware.ru/2013/12/locklib.html

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru