Я увидел интересный ответ в обсуждении на форуме, в котором я участвовал. Вопрос был не на тему этого поста, но ответ с кодом был достаточно интересным, чтобы написать эту статью. Код был написан на Delphi, но, очевидно, писался в стиле простого C (даже с использованием goto!).
Однако сейчас я хочу поговорить о том, как WinAPI обычно возвращает ошибки, и почему код в ответе - не самый удачный способ.
Тот код выглядел не в точности так. Я просто выделил из него существенные части для вашего удобства (исходный код напоминал C ещё больше, чем этот пример):
function IsUserMemberOf(const Group : PSID) : BOOL; var hToken : HANDLE; begin result := false; // получаем Token и сохраняем в hToken ... (короткий вариант) if not OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY or TOKEN_DUPLICATE, hToken) then goto Exit; // дополнительные действия для Vista и много-много gotos if not CheckTokenMembership(hToken, Group, @result) then begin goto Exit_1; end; :Exit_1 CloseHandle(hToken); goto Exit; :Exit end;В чём проблема с возвращаемым значением этой функции? Во-первых, возвращаемое значение каждой WinAPI функции проверяется и функция возвращает управление, если вызываемая WinAPI функция возвращает ошибку. Это - очень хорошо, потому что я видел кучу кода, который ничем таким не озаботились. Но здесь есть другая проблема. Существует два возможных набора результатов, которые мы бы хотели сообщить нашему вызывающему:
- Является ли пользователь членом группы. Это, очевидно, двоичное значение (скажем,
True/False
). - Произошла ли ошибка. Аналогично, это двоичное значение (
True/False
).
False(0)
до True(1)
. Функция проецирует два набора с разным смыслом в один набор - результат функции. Если один из вызовов WinAPI завершиться неудачей, то функция выйдет и вернёт False
. Очевидно, что значение False
используется для двух совершенно разных по смыслу результатов. Одно из которых говорит вызывающему, что пользователь не входит в группу, а другое - что функция почему-то завершилась с ошибкой. А мы соединили эти два смысла в одно значение. Т.е. мы потеряли информацию! Теперь мы не можем отличить один случай от другого без дополнительной (мета) информации. Всегда думайте об этом, когда вы проектируете заголовок функции. Но, конечно же, эта информация есть у нас в другом месте. В самом WinAPI. GetLastError
даст вам значение ошибки, которое (я надеюсь) позволит нам решить нашу проблему. Так что всё нормально? Хотелось бы верить! Посмотрите на это:
function GetMyUserName : String; var nSize : Cardinal; p : PChar; begin nSize := 0; if not GetUserName(nil, nSize) and (GetLastError() = ERROR_INSUFFICIENT_BUFFER) then begin GetMem(p, nSize * sizeof(CHAR)); try if not GetUserName(p, nSize) then RaiseLastOSError; result := p; finally FreeMem(p); end; end else RaiseLastOSError; end; begin ... xy := GetMyUserName(); bIsMember := IsUserMemberOf(aGroup); if not bIsMember and (GetLastError() <> 0) then ОШИБКА //...обработка; end;Функция
IsUserMemberOf
вызывается после успешного вызова GetMyUserName
, которая первым делом просит WinAPI-функцию GetUserName
вернуть размер строки, содержащей имя пользователя. В этом случае GetLastError
вернёт ERROR_INSUFFICIENT_BUFFER
(122dec), которая говорит нам, что необходимо использовать больший блок памяти.В итоге
GetMyUserName
вернёт нам имя пользователя (успешно). Предположим, что IsUserMemberOf
также будет успешна, но при этом она говорит нам, что пользователь не входит в группу. Тогда возвращаемое значение будет False
, а тот факт, что большинство WinAPI функций не изменяют значение LastError при успешном вызове, говорит нам, что GetLastError
всё ещё будет равно ERROR_INSUFFICIENT_BUFFER
(122dec): поэтому когда мы будем проверять bIsMember
- мы попадём на ветку кода с обработкой ошибки, хотя никакой ошибки не было.Как вы видите, даже хотя мы можем получить дополнительные сведения от
GetLastError
, но эта функция реализована так, что мы не можем воспользоваться этой информацией.Если вы планируете написать такую функцию, я призываю вас не использовать техники языка C. Почему? Просто посмотрите как выглядит этот стиль:
function GetUserNameW(lpBuffer: LPWSTR; nSize: PDWORD): BOOL; var pUserName: PWideChar; res: NTSTATUS; bRes: Boolean; begin bRes := FALSE; if (nSize = nil) then begin SetLastError(ERROR_INVALID_PARAMETER); goto Exit; end; if (lpBuffer = NIL) or (nSize^ <= InternalGetMinUserNameLength()) then begin nSize^ := InternalGetMinUserNameLength(); SetLastError(ERROR_INSUFFICIENT_BUFFER); goto Exit; end; // Это не совсем нужно, но просто добавить сложности в пример pUserName := HLOCAL(LocalAlloc(LPTR, InternalGetMinUserNameLength() * sizeof(WIDECHAR)); // TODO: проверить pUserName на nil: если nil - goto Exit res := NtGetUserName(pUserName, InternalGetMinUserNameLength() * sizeof(WIDECHAR)); if NT_FAILED(res) then begin SetLastError(NtStatusToDosError(res)); goto Exit_1; end; if not StrCopyMemory(lpBuffer, pUserName) then goto Exit_1; bRes := TRUE; :Exit_1 FreeMem(pUserName); goto Exit; :Exit result := bRes; end;Выглядит сложно, не так ли? Я не собирался писать точную реализацию
GetUserName
(положа руку на сердце - я её не знаю), а вместо этого показал возможную реализацию. Так что WinAPI использует результат функции только как указание на ошибку. False
означает провал операции, а GetLastError
сообщит вам, что именно пошло не так. True
означает, что всё в порядке.Нам действительно нужно всё это в Delphi? Вообще-то нет. Если вы хотите написать библиотеку (DLL), которую нужно вызывать из других языков, то вы можете использовать
SetLastError
. И вам придётся возвращать результат функции в var/out параметре (по ссылке). Конечно же, вы также можете использовать тип HRESULT
в качестве возвращаемого значения.
function GetUserNameW(lpBuffer: LPWSTR; nSize: PDWORD): HRESULT;Если же вы хотите написать просто функцию для Delphi, то вы можете использовать исключения.
procedure GetUserNameW(Name: PWideChar; var nSize: DWORD);Большой плюс - что вы можете использовать функцию как функцию, поскольку результат функции теперь может прямо указывать на результат операции, а не на признак ошибки.
function GetUserNameW(Name: PWideChar; var nSize: DWORD) : PWideChar; begin if (nSize = nil) then begin SetLastError(ERROR_INVALID_PARAMETER); RaiseLastOsError; end; ... // не буду копировать весь код - идею вы уловили end;Как вы можете видеть, мы вызываем
RaiseLastOsError
, которая возбуждает исключение EOsError
. Оно содержит код от GetLastError
в своём свойстве ErrorCode
. Мы даже можем создать свой собственный класс исключения (к примеру, JWSCL использует EJwsclWinCallFailedException
), и вообще убрать вызов SetLastError
! И, конечно же, вы можете использовать родной строковый тип Delphi, что сделает эту функцию очень простой в использовании.
function GetUserName() : String;Функция возвращает строку. Она не возвращает пустой строки при ошибке, как делали те реализации, что мы нашли в интернете. Иначе у нас была бы та же самая проблема с двумя наборами значений, спроецированными на одно.
function IsUserMemberOf(const Group : PSID) : Boolean; var hToken : HANDLE; begin result := false; // получаем Token и сохраняем в hToken ... (короткий вариант) if not OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY or TOKEN_DUPLICATE, hToken) then RaiseLastOsError; // дополнительные действия для Vista и много-много RaiseLastOsError; if not CheckTokenMembership(hToken, Group, @result) then RaiseLastOsError; end;Улучшенный вариант этой функции есть в JEDI Component Library (JCL) в файле
JclSecurity.pas
. Забудьте обо всех других реализациях!! Используйте эту:
function IsGroupMember(RelativeGroupID: DWORD): Boolean; var psidAdmin: Pointer; Token: THandle; Count: DWORD; TokenInfo: PTokenGroups; HaveToken: Boolean; I: Integer; const SE_GROUP_USE_FOR_DENY_ONLY = $00000010; begin Result := not IsWinNT; if Result then // на Win9x и ME нет безопасности Exit; psidAdmin := nil; TokenInfo := nil; HaveToken := False; try Token := 0; HaveToken := OpenThreadToken(GetCurrentThread, TOKEN_QUERY, True, Token); if (not HaveToken) and (GetLastError = ERROR_NO_TOKEN) then HaveToken := OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token); if HaveToken then begin {$IFDEF FPC} Win32Check(AllocateAndInitializeSid(SECURITY_NT_AUTHORITY, 2, SECURITY_BUILTIN_DOMAIN_RID, RelativeGroupID, 0, 0, 0, 0, 0, 0, psidAdmin)); if GetTokenInformation(Token, TokenGroups, nil, 0, @Count) or (GetLastError <> ERROR_INSUFFICIENT_BUFFER) then RaiseLastOSError; TokenInfo := PTokenGroups(AllocMem(Count)); Win32Check(GetTokenInformation(Token, TokenGroups, TokenInfo, Count, @Count)); {$ELSE FPC} Win32Check(AllocateAndInitializeSid(SECURITY_NT_AUTHORITY, 2, SECURITY_BUILTIN_DOMAIN_RID, RelativeGroupID, 0, 0, 0, 0, 0, 0, psidAdmin)); if GetTokenInformation(Token, TokenGroups, nil, 0, Count) or (GetLastError <> ERROR_INSUFFICIENT_BUFFER) then RaiseLastOSError; TokenInfo := PTokenGroups(AllocMem(Count)); Win32Check(GetTokenInformation(Token, TokenGroups, TokenInfo, Count, Count)); {$ENDIF FPC} for I := 0 to TokenInfo^.GroupCount - 1 do begin {$RANGECHECKS OFF} // Массив [0..0] - игнорируем ERangeError Result := EqualSid(psidAdmin, TokenInfo^.Groups[I].Sid); if Result then begin // Учесть denied ACE с SID администратора Result := TokenInfo^.Groups[I].Attributes and SE_GROUP_USE_FOR_DENY_ONLY <> SE_GROUP_USE_FOR_DENY_ONLY; Break; end; {$IFDEF RANGECHECKS_ON} {$RANGECHECKS ON} {$ENDIF RANGECHECKS_ON} end; end; finally if TokenInfo <> nil then FreeMem(TokenInfo); if HaveToken then CloseHandle(Token); if psidAdmin <> nil then FreeSid(psidAdmin); end; end; function IsAdministrator: Boolean; begin Result := IsGroupMember(DOMAIN_ALIAS_RID_ADMINS); end;Это же можно сделать и на JWSCL:
uses JwaWindows, JwsclToken, JwsclKnownSid, JwsclUtils; var Token : TJwSecurityToken; begin JwInitWellKnownSIDs; Token := TJwSecurityToken.CreateTokenEffective(TOKEN_READ or TOKEN_QUERY or TOKEN_DUPLICATE); try Token.ConvertToImpersonatedToken(DEFAULT_IMPERSONATION_LEVEL, MAXIMUM_ALLOWED); if Token.CheckTokenMembership(JwAdministratorsSID) then // Пользователь - член группы Администраторы finally Token.Free; end; end;
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.