Tag: Delphi

VirtualTree updated to 4.8.7 … 그리고..

오늘 보니 VirtualTree가 4.8.7로 10월 말에 업데이트 되었네요. 그래서 가차없이 업데이트 했는데  TVirtualStringTree를 를 그대로 쓰지않고 몇 가지 수정을 가해서 사용하고 있던 부분에서 오류가 발생합니다.

DoHeaderClick의 파라미터가 달라지고 더이상 virtual이 아니다.

procedure DoHeaderClick(HitInfo: TVTHeaderHitInfo); virtual;
procedure DoHeaderClick(Column: TColumnIndex; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);

파라미터 달라진 것은 이해하겠습니다. 그런데 virtual 이었던 것이 virtual이 아닌것으로 돌아가다니.. 다른 DoXXX 함수들은 virtual인것을 보니 아마도 실수를 한 듯 합니다.

결국은 DoHeaderMouseUp을 override 하는 것으로 처리했지요

procedure TVirtualStringTreeEx.DoHeaderMouseUp(Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Column: Integer;
begin
  inherited;

  // Convert vertical position to local coordinates.
  Inc(Y, Header.Height);
  Column := Header.Columns.ColumnFromPosition(Point(X, Y));

  DoSometing;
end;

TimeSync를 업데이트 하다가…. Windows Service/ NamedPipe

TimeSync가 Windows 7에서 동작하지 않았던 문제를 최근에서야 알고, 그 문제를 수정하면서 했던 일 몇가지 기록해봅니다.

SetLocalTime과 권한

윈도우즈 시간 설정은 SetLocalTime API를 사용합니다. 이게 Windows Vista/7 에 오면서 보호된 시스템 리소스로 변경되면서 SE_SYSTEMTIME_NAME 권한을 필요로합니다. 관리자 권한으로 실행하도록 하면 되겠지만, TimeSync 원래 목적이 자동으로 시간을 맞추는 것였기 때문에 사용자에게 관리자 권한 물어보는 다이얼로그가 나오는 상황을 원치 않았습니다.

그래서 결국은 시간 동기화 서비스를 만들고, 클라이언트 프로그램으로 서비스를 호출하는 것으로 변경했습니다. 그렇습니다. 배보다 배꼽이 더 커졌습니다.

Windows Service

서비스 등록 및 해지

서비스의 등록과 삭제는 관리자 권한이 필요합니다. 관리자 권한으로 서비스 프로그램에 옵션을 주어서 실행합니다.

ServiceApplication.exe /INSTALL [/SILENT]
ServiceApplication.exe /UNINSTALL [/SILENT]

서비스 시작 및 종료

net.exe start ServiceName
net.exe stop ServiceName

Delphi로 Windows Service 작성하기

서비스 작성

Delphi로 서비스를 작성하는 것은 아래 참조 링크에 아주 자세히 설명이 되어있습니다. 서비스지만 서버라고 보시면 됩니다. 그래서 일반적인 서버를 작성하는 개념으로 작성해야합니다.

서비스는 서비스 Thread를 하나 만들고, 서비스의 각각의 이벤트에 해당 Thread를 컨트럴 해줍니다.

  1. OnServiceStart에 서비스 Thread 시작
  2. OnServiceStop에 서비스 Thread 종료
  3. OnServiceContinue, OnServicePause에 서비스 Thread Start/ Suspend
  4. OnServiceExecute에서 해당 요청을 처리하면 됩니다.

이렇게 해서 서비스의 대략적인 소스는 다음과 같습니다.

procedure TTimeSyncService.ServiceContinue(Sender: TService;
  var Continued: Boolean);
begin
  ServiceThread.Start;
  Continued := True;
end;

procedure TTimeSyncService.ServiceExecute(Sender: TService);
begin
  while not Terminated do
    ServiceThread.ProcessRequests(False);
end;

procedure TTimeSyncService.ServicePause(Sender: TService; var Paused: Boolean);
begin
  ServiceThread.Suspended := True;
  Paused := True;
end;

procedure TTimeSyncService.ServiceStart(Sender: TService; var Started: Boolean);
begin
  TimeSyncServiceThread := TTimeSyncServiceThread.Create(False);
  Started := True;
end;

procedure TTimeSyncService.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
  TimeSyncServiceThread.Terminate;
  Stopped := True;
end;

이 서비스 Thread에서 클라이언트와 통신을 대기하면 됩니다. 처음 서비스를 작성하려고 생각했을때 서비스니깐 뭔가 클라이언트와 통신하는 방법을 기본으로 제공해줄까? 하고 생각을 했었지만 아무것도 없습니다. 그 방법은 작성하는 사람이 TCP/IP, PIPE, MemoryMap 등등 제공하는 IPC 중에서 편한것 선택해서 하면 됩니다.

여기서는 그냥 가장 간단할 것 같은(하지만 또 다른 복병때문에 삽질 했지만요)  NamedPipe를 이용했습니다.

Service Thread

TimeSync는 여러 크라이언트의 동시 연결 또는 여러 복잡 다단한 명령들이 필요하지 않기에 요청이 있으면 바로 수행하고 연결을 끊어버리는 구조로 구성했습니다. HTTP와 비슷하게요.

대락적인 코드는 다음과 같습니다.

procedure TTimeSyncServiceThread.Execute;
begin
  // 통신할 파이프 생성
  PipeHandle := CreateNamedPipe(PIPE_NAME,
      PIPE_ACCESS_DUPLEX,
      PIPE_TYPE_MESSAGE or PIPE_READMODE_MESSAGE or PIPE_WAIT,
      PIPE_UNLIMITED_INSTANCES,
      BUFSIZE,
      BUFSIZE,
      NMPWAIT_USE_DEFAULT_WAIT,
      @sa);

  try
    while not Terminated do
    begin
      // 서비스 클라이언트의 연결 대기
      ConnectNamedPipe(PipeHandle, nil);

      // 클라이언트 패킷 수신
      FileRead(PipeHandle, Buffer, ....);

      // 클라이언트의 요청에 따라서 뭔가 수행
      DoSometing(Buffer);

      // 처리 결과를 클라이언트에 보내기
      FileWrite(PipeHandle, Buffer, ....);

      // 연결 해제하고 다른 클라이언트의 연결을 기다림...
      DisconnectNamedPipe(PipeHandle);
    end;
  finally
    CloseHandle(PipeHandle)
  end;
end;

클라이언트에서 마찬가지로 파이프에 연결해서 처리합니다.

procedure Sync;
var
  PipeHandle: THandle;
  PipeStream: THandleStream;
  Command, Result: DWORD;
begin
  PipeHandle := CreateFile(PIPE_NAME,
        GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0);

  // 연결할 파이프가 부족하면 잠시 대기합니다. 하지만 TimeSync에서는 가능성이 극히 적음
  if GetLastError = ERROR_PIPE_BUSY then
  begin
    if not WaitNamedPipe(PIPE_NAME, 2000) then
      raise Exception.Create(StrServiceConnectFailed);
  end;
  try
    FileWrite(PipeHandle, Buffer, ....);
    FileRead(PipeHandle, Buffer, ....);
  finally
    DisconnectNamedPipe(PipeHandle);
    CloseHandle(PipeHandle);
  end;
end;

아주 아주 간단한 구조로 구성이 되었고, 잘 될거라고 믿고 있었습니다.

Named Pipe로 서비스와 통신하기

하지만 항상 복병이 있지요. 우성 서비스로 작성하기 전에 일반 어플리케이션으로 파이프 서버 작성하고 했더니 잘 되어서 서비스로 적용하는데, 파이프로 연결하는 부분부터 안됩니다. 연결 자체가 안됩니다.

원인은 파이브를 기본으로 만들면 그 프로세스를 생성한 사용자만 그 파이브에 접근할 수 있습니다. Service는 관리자 권한으로 실행되는 것이니, 당연히 일반 사용자로 실행되는 클라이언트의 접근을 거부해버립니다.

그래서 서버의 파이프를 생성할때 CreateNamedPipe의 마지막 인자로 SecurityAttribute를 설정해야합니다. 아래는 누구나 다 그 만들어진 파이프에 연결이 가능하게 하는 SecurityAttribite를 만드는 코드입니다.

var
  PipeHandle: THandle;
  sa: TSecurityAttributes;
  sd: TSecurityDescriptor;
begin
  InitializeSecurityDescriptor(@sd, SECURITY_DESCRIPTOR_REVISION);
  SetSecurityDescriptorDacl(@sd, True, nil, False);

  FillChar(sa, SizeOf(sa), 0);
  sa.nLength := SizeOf(sa);
  sa.lpSecurityDescriptor := @sd;
  sa.bInheritHandle := False;

  PipeHandle := CreateNamedPipe(PIPE_NAME,
      PIPE_ACCESS_DUPLEX,
      PIPE_TYPE_MESSAGE or PIPE_READMODE_MESSAGE or PIPE_WAIT,
      PIPE_UNLIMITED_INSTANCES,
      BUFSIZE,
      BUFSIZE,
      NMPWAIT_USE_DEFAULT_WAIT,
      @sa);

이렇게 했더니만 연결이 잘 됩니다. 이제 프로그램은 완성했습니다. ^^;

InnoSetup으로 설치 스크립트 만들기

이렇게 만들고 보니 프로그램을 실행하기 위해서 먼저 해결해야할 것이 있네요.

  1. 서비스 등록
  2. 서비스 실행
  3. 클라이언트 시작프로그램에 등록

이걸 개인이 하라고 하는건 무린것 같고(특히 서비스 등록이나 실행은 관리자 권한이 필요하죠). 그래서 InnoSetup으로 설치 스크립트를 만들었습니다.

[Run]
Filename: net.exe; Parameters: stop TimeSyncService
Filename: {app}\TimeSyncSvc.exe; Parameters: /INSTALL /SILENT
Filename: net.exe; Parameters: start TimeSyncService

[UninstallRun]
Filename: net.exe; Parameters: stop TimeSyncService
Filename: {app}\TimeSyncSvc.exe; Parameters: /UNINSTALL /SILENT

[Icons]
Name: {commonstartup}\시간 동기화; Filename: {app}\TimeSync.exe; IconFilename: {app}\TimeSync.exe; Languages:

포인트는 설치중에 서비스 설치/ 실행하고, 설치 제거하면 서비스 정지/ 해제하는 것, 그리고 시작프로그램에 등록하는 것입니다.

기타

오류: "지정된 서비스가 지워진 것으로 표시되었습니다"

서비스를 잘못 시작/ 종료/ 삭제하다보면 위의 메시지가 나올때가 있습니다(지금 떠올려보면 서비스가 실행된 상태에서 서비스를 uninstall 했던 것 같습니다). 이것은 서비스가 말그대로 <a href="http://www.pyrasis.com/blog/entry/ServiceDeleteFlag">삭제 플레그가 설정된 것으로 시간이 지나면 자동으로 삭제된다</a>고 합니다.

하염없이 기다리시거나 아니면 재부팅하면 됩니다. 한없이 기다리기 뭐해서 재부팅하고 커피한잔 마셨습니다.

서비스를 실행했는데, 파이프가 만들어 졌을까?

이런 고민이 있을 수 있습니다. 서비스 디버깅 하면 되긴하지만, 간단한 것에 귀찮아서 안했고, 그냥 파이프가 만들어 졌는지만 확인하려고 했습니다.

파이프가 만들어진 것을 확인하는 툴은 SysInternalsSuite에 있는 pipelist.exe 툴을 이용하면 확인 가능합니다.

TimeSync_Pipe가 여기서 사용하는 파이프입니다.

참고 자료


pgdbx4 0.4 Release: dbExpress driver for PostgreSQL

그간 방치해뒀던 PostgreSQL용 dbExpress 드라이버를 오늘 좀 수정했습니다.

  • Boolean 형태가 잘 안가져오던 문제
  • BYTEA/TEXT 타입이 Next를 해도 다음 row를 가져오지 못하던 문제
  • 기타 기억안나는 자잘한 것...

자세한 내용은 pgexp4 페이지를 참고하세요.


TSQLConnection.SQLHourGlass란 놈은 참…

TSQLConnection.SQLHourGlass 가 있다. 그래 이걸 보고 상상을 하면 TSQLConnection을 이용해서 뭔가 서버에 쿼리를 주고 받고 하는 동안에  커서를 HourGlass로 표시해주는 기능일 것 같다.

그런데 이런 예상과는 완벽하게 다르게 서버에 연결할 때만 HourGlass를 표시해준다. 쩝... 아 이 사람들아!!! 연결할 때 뿐만 아니라 쿼리할때도 더 시간이 걸린다고!!!! 글쎄 기억이 가물가물해서 그런지 모르지만, TDatabase 같은 것에선 지원했던것 같다.

그래서 쿼리 던질때 마다 SQLHourGlass를 표시하려면 어떻게 하나고?

글쎄다.. 아직은 모르겠다. 나중에 해결하면 알려주마.^^;


Indy에 SSL 사용하기

Indy 라이브러리는 자체적으로 SSL을 지원하지 않습니다. Third party로 SSL IO Handler를 지정해줘야하는데 OpenSSL을 이용하는 방법은 다음과 같습니다.

  1. Indy SSL에서 미리 컴파일된 OpenSSL DLL을 다운로드 한다. 여기서는 fulgan의 DLL을 다운 받았다.
  2. 파일중에서 필요한 2개의 파일(libeay32.dll, ssleay32.dll)을 실행파일이 위치할 곳에 푼다.
  3. TIdHttp로 이용하는 코드는 다음과 같다.
FHTTP := TIdHTTP.Create(nil);
  FOpenSSLHandler := TIdSSLIOHandlerSocketOpenSSL.Create(FHTTP);

  FHTTP.IOHandler := FOpenSSLHandler;
  FHTTP.Get('https://mail.google.com');</pre>

http://www.woosum.net/wiki/Indy


TLinkLabel에 WordWrap 적용하기

TLinkLabel은 사용하기 따라서 상당히 유용합니다. 근디 WordWrap이 없군요. 이게 없으니깐 AutoSize를 줘도 한줄로 길게 쭈욱 나와버립니다. TLabel의 WordWrap과 같이 동작하면 참 편하겠는데 말이지요. 어찌 되었건 아래처럼 MaxWidth 변수가 사용되는 부분만 몇줄 추가하면 원하는데로 됩니다.

procedure TnxLinkLabel.AdjustBounds;
var
  DC: HDC;
  SaveFont: HFont;
  TextSize: TSize;
  Parsed: string;
  MaxWidth: Integer;
begin
  if not (csLoading in ComponentState) and FAutoSize then
  begin
    if CheckWin32Version(6) and UseThemes then
    begin
      if HandleAllocated then
      begin
        if FWordWrap then
          MaxWidth := ClientWidth else
          MaxWidth := MaxInt;

        SendGetStructMessage(Handle, LM_GETIDEALSIZE, MaxWidth, TextSize);
        SetBounds(Left, Top, TextSize.cx + (GetSystemMetrics(SM_CXBORDER) * 4),
          TextSize.cy + (GetSystemMetrics(SM_CYBORDER) * 4));
      end;
    end
    else

TCustomLinkLabel.AdjustBounds를 단 몇 줄 수정하면 되는데, AdjustBounds가 virtual로만 되어있어도 편한데 그게 아니라서 이렇게 수정만 해서는 안됩니다. 그렇다고 vcl을 직접 다시 컴파일하기도 그렇고..

결국은 TCustomLinkLabel, TLinkLabel을 모두 몽땅 가져다가 새 이름으로 콤포넌트를 등록해버렸군요. 찝찝하지만 Go~


Delphi의 새로운 라이센스 정책

델파이의 라이센스 정책에 새로운 라이센스 타입이 추가되었다는군요. 이름 재밌네요 TagCloud 라이센스. 뭐. 대략 읽어본 봐로는 하나의 라이센스를 가지면 Delphi 2007, Delphi 2010을 모두 설치할 수 있다고 합니다. 물론 일반 라이센스보다 25% 비싸답니다.

이거 참 반가운 일입니다.

새로운 버전의 기능을 쓰고싶은데, 라이센스 부담으로 설치못하고, 지금 것도 잘 되잖아라는 것에 반박할 거리 찾아내고.

새로운 버전으로 마이그레이션 하고 싶은데, 트라이얼 버전에 제한된 시간으론 완벽한 테스트가 불가능하고,

이러한 문제점을 일거에 해결할 수 있겠군요. 그런데.. 다른 새로운 버전이 나왔을 경우도 적용되는지에 대해선 언급이 없어 아쉽네요.

덧) 제가 잘못 알고 있었던 것도 있었군요. 자세한 것은 임프님 댓글로 http://www.borlandforum.com/impboard/impboard.dll?action=read&db=free&no=18437


Delphi IDE에서의 정규식…

hi=:param

hi = :param

으로 변경하고 싶습니다. 정규식을 써야겠지요? 그래서 Replace에서 정규식을 적었더니 이상하게 잘 못 찾습니다. 도움말을 봤죠. 그랬더니 약간 제가 알고 있던 정규식과는 이상한 부분이 있데요.

{ } Braces group characters or expressions. Groups can be nested, with a maximum number of 10 groups in a single pattern. For the Replace operation, groups are referred to by a backslash and a number, according to the position in the "Text to find" expression, beginning with 0. For example, given the text to find and replacement strings, Find: {[0-9]}{[a-c]*}, Replace: NUM\1, the string 3abcabc is changed to NUMabcabc.

그냥 괄호"()"를 쓰는게 일반적인 정규식으로 알고 있었는데, 여기선 중괄호"{}"를 사용하네요.

Text to find: {[a-zA-Z]}=\:{[a-zA-Z]}
Replace with : \0 = :\1

ps 1. 그리고 IDE 검색기능에 전체 프로젝트 파일에서 찾는것까진 정말 좋습니다. 그런데 여기서 추가로 찾은 것에 대해서 바꾸기나 프로젝트 전체에서 바꾸기 그런거 있으면 좋겠네요. 또한 dfm 파일에서도 찾을 수 있으면 좋겠네요.

물론 다른 에디터를 실행해서 할 순 있지만, 귀찮을 때가 있어요.



AlphaBlend를 사용할 때 리소스 부족

Dimmer라는게 있습니다. 다이얼로그가 뜨면, 나머지를 흐리게 표시하는, 요즘 웹에서 많이 보이는 것이죠. 이것을 델파이로 구현한 것이 저 링크인데.. 오늘 사용하다보니 이상하게도 가려진 폼의 전체가 아닌 일부분만 조그만하게 표시되는 겁니다. 이상해서 Spy++로 봐도 윈도의 크기는 원하는데로 있는데, 표시만 저렇게 됩니다.

결국 AlphaBlend처리하는 소스를 보니 SetLayeredWindowAttributes를 사용하는데, 에게 실패하는 것 같아서 확인해보니 역시나입니다. 이게 리소스 부족이라는 메시지를 내면서 실패하더군요.

또한 이럴때 현상을 보면 DimmerForm이 작으면 제대로 되가다 일정 크기가 넘어가면 제대로된 동작을 하지 않던가, 아니면 폼에 일그러진 이미지들이 표시됩니다.

결국 SetLayeredWindowAttributes가 실패하면 Dimmer를 표시하지 않도록 변경했습니다.

procedure TDimmerForm.Display;
const
  cUseAlpha: array [Boolean] of Integer = (0, LWA_ALPHA);
  cUseColorKey: array [Boolean] of Integer = (0, LWA_COLORKEY);
begin
  BoundsRect := Application.MainForm.BoundsRect;

  // 여기는 TCustom.SetLayeredAttribs에서 가져옮
  if SetLayeredWindowAttributes(Handle, ColorToRGB(TransparentColorValue), AlphaBlendValue,
    cUseAlpha[AlphaBlend] or cUseColorKey[TransparentColor]) then
    Show;
end;

이젠 리소스가 부족하면 차라리 안보이지.. 엉뚱하게 보이지는 않습니다. 아.. 이 리소스 부족에러.. 어떤놈이 리소스를 많이 잡아먹는지 알 수 있거나, 아님 리소스 누스를 만들어 내는 프로그램들을 어떻게 찾을 수 있는 방법 없을까나...


  • Copyright © 1996-2010 Your wish is my command. All rights reserved.
    iDream theme by Templates Next | Powered by WordPress