삽질하는플머

윈도에서 Objective-C 개발환경 만들기

탐구생활/Objective-C
아직 뭐가뭔지 잘 모르겠지만, GNUStep 이라는 플랫폼을 이용해 윈도에서 Objective-C 개발환경을 구축할 수 있다. 


GNU Step 사이트 에서 GNUstep System 과 GNUStep Core 를 다운받아 설치한다. 중간에 rxvt 설정부분은 무시하고 그냥 깔기만 하면 된다. 

설치 후 뜨는 MSys의 셸이 복사/붙이기가 잘 안되는 문제가 있으니 DKW를 셸로 사용하자. GNUStep 의 디렉토리에 DKW 실행파일과 INI를 복사하고 이름을 각각 GNUStep.exe, GNUStep.ini 로 바꾼다. GNUStep.ini 의 exec 항목을 다음과 같이 바꿔준다.  

exec=C:\GNUstep\bin\sh.exe --login -i


한글 입출력을 원활히 하기 위해 .inputrc 수정

set output-meta on
set convert-meta off


환경설정, 프롬프트 및 ls 색 변경, 침침한 셸 색상 변경을 위해 .profile 파일 추가. 

PS1="[\u@\h \W]\\$ "
alias ls="ls --color=auto --show-control-chars"

BGCOLOR=black
FGCOLOR=#AFAFAF

/GNUStep/System/Library/Makefiles/GNUstep.sh


이제 익숙한 셸 환경이 보인다. 우헤~~




다음과 같이 source.m 파일을 만들고

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>

int
main (void)
{
  NSAutoreleasePool *pool;

  pool = [NSAutoreleasePool new];

  [NSApplication sharedApplication];

  NSRunAlertPanel (@"Test", @"Hello from the GNUstep AppKit",
                   nil, nil, nil);

  return 0;
}


GNUMakefile 을 다음과 같이 만들어준다. 

include $(GNUSTEP_MAKEFILES)/common.make

APP_NAME = PanelTest <---------- 실행파일 이름
PanelTest_OBJC_FILES = source.m <--- 소스파일 이름

include $(GNUSTEP_MAKEFILES)/application.make


콘솔창에서 make를 때리면 빌드. 햐~~ 맥에서의 어플과 마찬가지로 *.app를 가지는 디렉토리가 떨어진다. 
컴파일이 다 끝났으면 실행. 

$ openapp ./PanelTest.app


NS객체를 쓰지 않는 다음과 같은 hello.m 은 GCC명령 한방으로 컴파일되니 함 돌려보세. 

#import <stdio.h>

int main (int argc, const char *argv[]) {
  printf( "Hello world\n");
  return 0;
}

gcc -o hello.exe hello.m





이 위키의 다음 페이지에서는 Dev-C++ 을 사용한 통합환경 꾸미기에 대해서도 설명되어있다.  


Dev-C++ MinGW없는 버전을 받아 설치한다. 

메뉴의 Tools(도구) > Compiler Options(컴파일러 설정) > Compiler(컴파일러)  를 열고 컴파일러 추가명령에 다음을 등록한다. 

-lobjc -lgnustep-base -fconstant-string-class=NSConstantString -enable-auto-import

링커 추가명령도 동일하게 지정한다. 




디렉토리의 각 항목에 다음과 같이 입력해준다. 

실행파일들
C:\GNUstep\mingw\bin
C:\GNUstep\bin

라이브러리
C:\GNUstep\mingw\lib
C:\GNUstep\GNUstep\System\Library\Libraries

C Include
C:\GNUstep\mingw\include
C:\GNUstep\GNUstep\System\Library\Headers


파일 -> 새로 만들기 -> 프로젝트 메뉴를 클릭하고 Empty Project 를 골라준다. 언어설정은 C로 해준다. 




프로젝트에서 유닛추가를 하고 이름을 적당히 바꿔준다. 이 때 확장자는 *m으로 한다. 

적당한 프로젝트를 빌드해보면... 뭐 대충 잘 되는군. 


----
2010.11.23

컴파일러 설정을 바꿔봄. 
* 컴파일러 추가 명령
-fconstant-string-class=NSConstantString

*링커 추가 명령
-lobjc -lgnustep-base -enable-auto-import




이 상태에서 추가로 AppKit 을 사용하는 경우 프로젝트 옵션의 매개변수에서 링커에 다음 항목을 추가한다. 
-lgnustep-gui 





다음 코드를 테스트. 

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>

int main (void)
{
  NSAutoreleasePool *pool;

  pool = [NSAutoreleasePool new];

  [NSApplication sharedApplication];

  NSRunAlertPanel (@"Test", @"Hello from the GNUstep AppKit",
                   nil, nil, nil);

  return 0;
}


실행결과는 뭐 잘 돌아가는군. 











Objective-c Beginner's guide






윈도에서의 gcc

Delphi to Cocoa. 컨셉 비교. 




유니코드 커맨드 라인 실행하고 결과값 받아오기

탐구생활/Delphi
소위 이야기하는 도스커맨드 실행하고 결과 받아오기. 김성동님의 코드를 살짝 바꿔 사용. 
커맨드라인 파라미터를 유니코드로 넘길 수 있게 하고, 중간 중간에 이루어지는 출력을 받아서 처리할 수 있는 콜백버전을 새로 만듦. 

다국어 INI를 읽어 UTF-8 로 뿌려주는 간단한  유틸리티를 만들었는데, 이 유틸리티를 호출하려면 INI 파일의 이름을 유니코드 파라미터로 넘겨야 하기 때문에 필요해짐. 


// 유니코드 커맨드라인 실행하고 출력값 얻어오자. .
// 주의! 콘솔로 출력되는 문자열은 안시문자열이다. 
// GetConsoleOutputCP(), SetConsoleOutputCP() API 참조. 

type
  TRunCmdCallback = procedure (const aRetStr: AnsiString; aParam: Pointer);

// 실행결과 문자열을 RunCmdCallback으로 조금씩 받아 조립할 때 사용.
// 콜백의 호출 주기는 상수 BUFFER_SIZE 의 값과 WaitForSingleObject() 의 두번째 인자인 시간값으로 결정된다. 
function RunWideCmd(Command: WideString; RunCmdCallback: TRunCmdCallback; Param: Pointer = nil): DWORD; overload;
const
  BUFFER_SIZE = 512; // 버퍼 크기가 작으면 RunCmdCallback이 좀 더 자주 일어남
var
  hReadPipe : THandle;
  hWritePipe : THandle;
  SI : TStartUpInfo;
  PI : TProcessInformation;
  SA : TSecurityAttributes;
  SD : TSecurityDescriptor;
  BytesRead : DWORD;
  Avail, wrResult : DWORD;
  RetStr: AnsiString;
begin
  { Dos Application }
  if Win32Platform = VER_PLATFORM_WIN32_NT then
  begin
    InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorDacl(@SD, True, nil, False);
    SA.nLength := SizeOf(SA);
    SA.lpSecurityDescriptor := @SD;
    SA.bInheritHandle := True;
    CreatePipe(hReadPipe, hWritePipe, @SA, BUFFER_SIZE);
  end else
    CreatePipe(hReadPipe, hWritePipe, nil, BUFFER_SIZE);

  try
    { STARTUPINFO를 설정한다. }
    FillChar(SI, SizeOf(SI), 0);
    SI.cb := SizeOf(TStartUpInfo);
    SI.wShowWindow := SW_HIDE;
    SI.dwFlags := STARTF_USESHOWWINDOW;
    { STARTF_USESTDHANDLES 를 빼면 PIPE로 입출력이 Redirect 되지 않는다. }
    SI.dwFlags := SI.dwFlags or STARTF_USESTDHANDLES;
    SI.hStdOutput := hWritePipe;
    SI.hStdError  := hWritePipe;
    { Process를 생성한다. }
    if CreateProcessW(nil, PWChar(Command), nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, SI, PI) then
    begin
      Result := 0;
      while Result = 0 do
      begin
      // 대기시간이 길면 반응이 느리다. 짧으면 콜백이 그만큼 자주 일어남. 적당히 조절해 사용. 
        wrResult := WaitForSingleObject(PI.hProcess, 5); 
        { Pipe에 출력된 내용이 있는 지 조사한다. }
        if PeekNamedPipe(hReadPipe, nil, 0, nil, @Avail, nil) then
        begin
          if Avail > 0 then
          begin
            { Redirect된 화면 내용을 얻어낸다. }
            SetLength(RetStr, Integer(Avail));
            ReadFile(hReadPipe, PAnsiChar(RetStr)[0], Avail, BytesRead, nil);

            if Assigned(RunCmdCallback) then RunCmdCallback(RetStr, Param);
          end;
        end;
        { WAIT_TIMEOUT이 아니면 루프를 마친다. }
        if wrResult <> WAIT_TIMEOUT then Result := 1;
      end;
      { 실행이 끝났다.. }
      GetExitCodeProcess(PI.hProcess, Result);
      CloseHandle(PI.hProcess);
      CloseHandle(PI.hThread);
    end;
  finally
    CloseHandle(hReadPipe);
    CloseHandle(hWritePipe);
  end;
end;

// 실행결과 문자열 전체를 한꺼번에 리턴받을 때 사용.
function RunWideCmd(Command : WideString; var RetStr: AnsiString): DWORD; overload;
const
  BUFFER_SIZE = 512;
var
  hReadPipe : THandle;
  hWritePipe : THandle;
  SI : TStartUpInfo;
  PI : TProcessInformation;
  SA : TSecurityAttributes;
  SD : TSecurityDescriptor;
  BytesRead : DWORD;
  Avail, wrResult : DWORD;

  LastSize: Integer;
begin
  { Dos Application }
  if Win32Platform = VER_PLATFORM_WIN32_NT then
  begin
    InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorDacl(@SD, True, nil, False);
    SA.nLength := SizeOf(SA);
    SA.lpSecurityDescriptor := @SD;
    SA.bInheritHandle := True;
    CreatePipe(hReadPipe, hWritePipe, @SA, BUFFER_SIZE);
  end else
    CreatePipe(hReadPipe, hWritePipe, nil, BUFFER_SIZE);

  try
    { STARTUPINFO를 설정한다. }
    FillChar(SI, SizeOf(SI), 0);
    SI.cb := SizeOf(TStartUpInfo);
    SI.wShowWindow := SW_HIDE;
    SI.dwFlags := STARTF_USESHOWWINDOW;
    { STARTF_USESTDHANDLES 를 빼면 PIPE로 입출력이 Redirect 되지 않는다. }
    SI.dwFlags := SI.dwFlags or STARTF_USESTDHANDLES;
    SI.hStdOutput := hWritePipe;
    SI.hStdError  := hWritePipe;
    { Process를 생성한다. }
    if CreateProcessW(nil, PWideChar(Command), nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, SI, PI) then
    begin
      Result := 0;
      while Result = 0 do
      begin
        wrResult := WaitForSingleObject(PI.hProcess, 5);
        { Pipe에 출력된 내용이 있는 지 조사한다. }
        if PeekNamedPipe(hReadPipe, nil, 0, nil, @Avail, nil) then
        begin
          if Avail > 0 then
          begin
            { Redirect된 화면 내용을 얻어낸다. }
            LastSize := Length(RetStr);
            SetLength(RetStr, LastSize + Integer(Avail));
            ReadFile(hReadPipe, PChar(RetStr)[LastSize], Avail, BytesRead, nil);
          end;
        end;
        { WAIT_TIMEOUT이 아니면 루프를 마친다. }
        if wrResult <> WAIT_TIMEOUT then Result := 1;
      end;
      { 실행이 끝났다.. }
      GetExitCodeProcess(PI.hProcess, Result);
      CloseHandle(PI.hProcess);
      CloseHandle(PI.hThread);
    end;
  finally
    CloseHandle(hReadPipe);
    CloseHandle(hWritePipe);
  end;
end;


두번째 함수는 사용법 간단. 대충 다음과 같이. 

procedure TForm1.Button5Click(Sender: TObject);
var
  RetStr: AnsiString;
begin
  RunWideCmd('cmd /c dir c:\', RetStr);
  ListBox1.Items.Text := RetStr;
  ListBox1.ItemIndex := ListBox1.Count -1;
end;


첫번째 함수는 실제 콘솔창처럼 변화하는 상태를 표현할 때 써먹기 위해 만들었는데 사용은 약간 귀찮다. 
(콜백의 호출 주기는 RunWideCmd 함수 내의 상수 BUFFER_SIZE 의 값과 WaitForSingleObject() 로 전달되는 시간값으로 결정된다.) 

var
  ugRetStr: AnsiString;

procedure RunCmdCallback (const aRetStr: AnsiString; aParam: Pointer);
begin
  ugRetStr := ugRetStr + aRetStr;

  TListBox(aParam).Items.BeginUpdate;
  TListBox(aParam).Items.Text := ugRetStr;
  TListBox(aParam).ItemIndex := TListBox(aParam).Count -1;
  TListBox(aParam).Items.EndUpdate;
  Application.ProcessMessages; 
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  ugRetStr := '';
  RunWideCmd('cmd /c dir c:\', RunCmdCallback, ListBox1);
end;


덤으로... 이렇게 호출당한 입장에서 유니코드 파라미터를 참고하는 방법은 다음과 같다. 

function GetWideParamStr(P: PWideChar; var Param: WideString): PWideChar;
var
  i, Len: Integer;
  Start, S, Q: PWideChar;
begin
  while True do
  begin
    while (P[0] <> #0) and (P[0] <= ' ') do
      P := CharNextW(P);
    if (P[0] = '"') and (P[1] = '"') then Inc(P, 2) else Break;
  end;
  Len := 0;
  Start := P;
  while P[0] > ' ' do
  begin
    if P[0] = '"' then
    begin
      P := CharNextW(P);
      while (P[0] <> #0) and (P[0] <> '"') do
      begin
        Q := CharNextW(P);
        Inc(Len, Q - P);
        P := Q;
      end;
      if P[0] <> #0 then
        P := CharNextW(P);
    end else
    begin
      Q := CharNextW(P);
      Inc(Len, Q - P);
      P := Q;
    end;
  end;

  SetLength(Param, Len);

  P := Start;
  S := Pointer(Param);
  i := 0;
  while P[0] > ' ' do
  begin
    if P[0] = '"' then
    begin
      P := CharNextW(P);
      while (P[0] <> #0) and (P[0] <> '"') do
      begin
        Q := CharNextW(P);
        while P < Q do
        begin
          S[i] := P^;
          Inc(P);
          Inc(i);
        end;
      end;
      if P[0] <> #0 then P := CharNextW(P);
    end else
    begin
      Q := CharNextW(P);
      while P < Q do
      begin
        S[i] := P^;
        Inc(P);
        Inc(i);
      end;
    end;
  end;

  Result := P;
end;

function WideParamStr(Index: Integer): WideString;
var
  P: PWideChar;
begin
  Result := '';
  if Index = 0 then
  begin
    SetLength(Result, MAX_PATH);
    SetLength(Result, GetModuleFileNameW(0, PWideChar(Result), MAX_PATH));
  end else
  begin
    P := GetCommandLineW;
    while True do
    begin
      P := GetWideParamStr(P, Result);
      if (Index = 0) or (Result = '') then Break;
      Dec(Index);
    end;
  end;
end;


위에 정의된 WideParamStr() 함수를 ParamStr() 함수 대신에 사용하면 됨. 


원격 커멘드 - rcmd

탐구생활/Others
원격 컴퓨터의 콘솔 프로그램을 실행하고 그 결과를 내 컴퓨터에 뿌려줘야 한다면? 바로 떠오르는 해법은 SSHD 를 설치해 사용하는 것이다. 하지만 로컬 네트웍에서 간단한 작업을 위해 cygwin 깔고 SSHD 설정하고 키를 교환한다는 건... 왠지 소잡는 칼을 생선 다듬는 데 휘둘러대는 폼안나는 일이라 생각된다. 

해서 간단한 유틸리티를 만들어볼까 폼잡다가 윈도 2000 리소스킷에서 멋진 솔루션을 발견했다. 이름하여 rcmd / rcmdsvc !! 
오늘은 이 물건을 가지고 재미나게 놀아보자!! (검색 안해보고 만들었으면 속 좀 쓰릴 뻔...)

다운로드는 여기서 "Remote command service" 를 클릭하면 rcmd.zip 을 얻을 수 있다. 압축을 풀면 rcmd.exe 와 rcmdsvc.exe 가 나온다. 

(나중에 찾아가기 귀찮으니 걍 여기에 붙여두자)

1. rcmdsvc.exe 를 서비스로 등록. 

"Service Controller Query Tool" 이라 불리는 sc.exe 는 예전에는 리소스킷으로 제공되었었는데 XP에는 이미 포함되어있다. 
압축이 풀린 rcmd.exe 와 rcmdsvc.exe 를 윈도우의 System32 폴더로 옮기고 커맨드창에서 다음과 같이 입력하자. 

C:\>sc create rcmdsvc binPath= C:\WINDOWS\system32\Rcmdsvc.exe displayname= "Remote command service" start= auto

[SC] CreateService SUCCESS


제어판의 서비스 항목에 방금 설치한 Remote command service 가 제대로 들어갔는지 살펴보자. 




여기서 "시작" 버튼을 살포시 눌러주거나, net start rcmdsvc 또는 sc start rcmdsvc 로 서비스를 시작한다. 
(등록된 서비스를 삭제하는 명령은 sc delete rcmdsvc 이다.)


2. 로컬에서 테스트.

이제 준비된 서버에 접속해보자. rcmd.exe 를 과감하게 실행시킨다. 




서버의 이름을 입력하라는 안내가  나온다. 로컬 서버의 IP인 127.0.0.1 을 입력하면 서비스가 실행된 계정인 LocalService 의 홈디렉토리를 기준으로 셸이 열리게 된다. 

실행 후 서버의 CMD를 띄우는 방식 외에 다음과 같이 직접 명령어를 입력하고 결과를 받아올 수도 있다. 
(도메인 이름 앞에 역슬래시 두개가 붙는 것에 유의하자.)

C:\> rcmd \\127.0.0.1 dir c:\ /w


실행 결과는 다음과 같다. 




3. 원격지에서 접속.

로컬에서만 띄우면 이게 뭐지 싶을 것이다. 이제 원래 목적이었던 원격지에서의 접속을 테스트 해 보자. 
이 글에서 rcmdsvc 서비스를 동작시키는 컴퓨터의 내부 네트웍 IP 는 192.168.29.1 이다. 다른 컴퓨터에서 이 컴퓨터의 dir 명령을 내려보자. 

C:\> rcmd \\192.168.29.1 dir c:\ /w

Error - Failed to connect to <\\192.168.29.1>, Error = 1326


1326 에러는 대상 컴퓨터의 IPC 권한을 갖지 못했기 때문에 발생한다. 네트워크 공유 연결과 동일한 방식으로 권한을 획득해주자. 

C:\> set user=[사용자명]
C:\> set passwd=[비밀번호]]

C:\> net use \\192.168.29.1\IPC$ %PASSWD% /user:%USER%
명령을 잘 실행했습니다.


이제 앞에서 내렸던 rcmd 명령을 다시 실행하면 원격지의 C 드라이브 내용을 볼 수 있게 된다. 

확보한 IPC 권한은 다음과 같은 방법으로 반환할 수 있다. 

C:\>net use \\192.168.29.1\IPC$ /delete
\\192.168.29.1\IPC$이(가) 제거되었습니다.


콘솔창에서 입력이 아닌 델파이에서 대상지의 IPC 권한을 확보하는 방법은 예전에 델마당에 올렸던 강좌를 참고하자.


// 도메인에 연결을 만든다.
function UseConnection(const aDomain, aUserID, aPassWord: String; var ConnStr: String): Boolean;
var
  NetResource: TNetResource;
  ConnStrLen: DWORD;
  RetFlag: DWORD;
  RetValue: DWORD;
begin
  FillChar(NetResource, SizeOf(TNetResource), 0);

  with NetResource do
  begin
    dwType := RESOURCETYPE_ANY;
    lpLocalName := nil;   // 로컬 드라이브 지정하지 않음
    lpRemoteName := PChar('\\' + aDomain);
    lpProvider := nil;
  end;

  SetLength(ConnStr, MAX_PATH);
  ConnStrLen := MAX_PATH;
  RetValue:=
    WNetUseConnection(
      0, NetResource,
      PChar(aPassWord), PChar(aUserID), CONNECT_INTERACTIVE,
      PChar(ConnStr), ConnStrLen, RetFlag
    );
  SetLength(ConnStr, StrLen(PChar(ConnStr)));

  ConnStr := '\\'+aDomain;

  Result := RetValue = 0;
end;


var
  ConnStr: String;
...
  UseConnection('192.168.29.1\IPC$', 유저이름, 패스워드, ConnStr) 


이 호출이 True면 권한 확보가 된 상태이므로 rcmd 를 사용할 수 있게 된다. 다 사용한 권한은 WNetCancelConnection() API 로 반환한다. 

WNetCancelConnection(PChar(ConnStr), true)


응용범위는 사실 무궁무진한데... 예를 들어 여러대의 PC에서 메인 PC의 스케줄에 따라 rsync 로 동기화해야 할 경우도 이 방법을 사용하면 중앙의 한 대의 PC에서 간단히 해결할 수 있다. 또한 Screen 과 버무리면 지금 서비스중인 게임에서 SSH 를 걷어내고 설정을 간단히하는 용도로 사용할 수도 있을 것이다. 

쓰기 편한 방법은 항상 보안위험에 노출되어있다. 하지만 SSHD 를 띄우고 키를 교환해두는 것도 불편함을 해결하기 위해 보안은 나몰라라 하는 것은 마찬가지이고, 또한 IPC가 로컬네트웍에서만 유효하다는 것을 감안하면 생선 비늘 벗기는 용도로는 딱 알맞은 칼이라 생각된다. 




한가지 http://www.simpleisbest.net/archive/2005/10/18/260.aspx 이 사이트의 내용을 보면 "HKEY_CURRENT_USER 레지스트리에 접근하면 데스크탑에 로그온한 사용자의 레지스트리가 아닌 로컬 서비스 계정의 레지스트리가 액세스되어 버릴 것이다" 라는 무시무시한 구절이 있다. 흠.. 이건 좀 곤란한데... 진짜로 그런지 확인해보자. 

일단 로컬시스템에서 적당한 레지스트리를 익스포트 하자. (뭐하는데 쓰는지는 잘 모르겠지만 값이 달랑 하나라 선택)

regedit /E "C:\test.reg" HKEY_CURRENT_USER\Calendar


출력된 C:\test.reg 파일의 내용은 다음과 같다. 

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Calendar]
"Skin"="1"


이제 이 Skin 값을 뭐 한 100 으로 바꿔준 뒤, 원격지에서 임포트 할 때 제대로 들어가는지 확인해 보자.

rcmd \\192.168.29.1 regedit /S C:\test.reg


로컬로 돌아와 레지스트리 편집기를 열어서 확인해본다. 




흠... 제대로 잘 들어가는뎅... 





당연한 얘기지만 서비스를 Interactive Service 로 설정하지 않으면, 즉 데스크탑과 상호연동을 시키지 않으면 윈도를 띄우거나 하는 일은 불가능. 즉 rcmd 로 원격지의 "윈도 어플" 을 실행시키면 이 윈도는 LocalService 계정에서 실행되고 나타나지 않는다.  

심심해서 rcmdsvc 의 로그온 설정의 "서비스와 데스크톱 상호 작용 허용" 을 체크하고 재구동한 뒤 원격지에서 로컬컴퓨터의 맵서버를 구동시켜 봄. 원격지에서 조작윈도나 루아윈도를 띄우는 명령을 내리면 이 창은 모두 로컬에서 나타난다.




오래전 GPG 글에서 rcmd 관련된 언급을 발견하고 반가워서 트랙백을 걸어둠. 


rsync 가지고 놀기

탐구생활/CYGWIN
rsync 는 로컬, 또는 리모트의 파일들을 동기화하는 멋진 물건이다. 
미루어두었던 cygwin 상에서 rsync 삽질내용을 정리하자. 

한동안 cygwin 을 돌리지 않았더니 버전이 2.697 -> 2.721 로 올라가버림. 업데이트에 한세월이 걸리네...

rsync는 rsync --daemon 옵션을 통해 데몬으로 동작시킬 수 있다. 

rsync --daemon --configure=rsyncd.conf

rsyncd.conf 에 지정된 PID파일에는 rync 데몬의 PID가 기록된다. 종료시에는 이 값을 참고하면 된다.  

매번 이 작업이 싫다면 cygrunsvc 를 이용해 서비스로 등록하면 될 듯. 
고맙게도 rsync를 이용한 백업방법을 깔끔하게 정리해둔 곳이 있다. 소스포지에 적절한 구성예제도 있고 말이지...


이걸 함 따라해보자. 

먼저 소스포지에서 cygwin-rsyncd-2.6.8_0.zip 을 내려받아 압축을 푼다. 이 파일은 cygwin 에서 rsync 데몬구동에 필요한 요소만 추려낸 것이다. (나중에 sshd 구성요소를 독립시켜 쓰는데도 많은 참고가 될 듯 하다.) 
압축이 풀린 파일들 중 다음 파일들을 C:\cygwin\etc\rsyncd-conf 에 옮긴다. 

  • rsyncd.conf
    rsyncd.secrets
    Rreadme.txt - 별 필요없지만... 

rsyncd.secrets 파일을 열고 적절한 사용자명과 패스워드를 설정한다. 여기서는 oranke:1234 마지막 한 줄은 비워야 한다. 

# Also: make sure this file ends in a newline.  Otherwise the last
# username/password pair will be ignored.
#
UUU:PPP
oranke:1234


rsyncd.conf 파일을 열고 경로명을 제대로 맞춰준다. 

# Uncomment this line and change the path if
# you would like to log rsync messages.
#
# log file = c:/cygwin/etc/rsyncd-conf/rsyncd.log

#
# The location of the rsync process ID file
#
# 서비스 구동시 재부팅하면 pid 파일이 남아있게 된다. 
# pid 파일이 있으면 데몬이 실행되지 않으므로 이 부분도 주석처리 한다. 
# pid file = c:/cygwin/etc/rsyncd-conf/rsyncd.pid

#
# The locations of the rsync lock file
#
lock file = c:/cygwin/etc/rsyncd-conf/rsyncd.lock


auth users 섹션을 찾아  secrets 에 추가한 아이디를 지정해준다. 

auth users = UUU, oranke


secrets file 역시 수정된 경로로 맞추어준다. 

    secrets file = c:/cygwin/etc/rsyncd-conf/rsyncd.secrets


이 곳에 접속할 rsync 클라이언트의 아이피를 제한하려면  hosts allow 섹션의 주석을 털어주고 적절한 아이피를 설정한다.  

    # hosts allow = 172.16.0.17


기본적으로 이 설정파일은 'C:\Documents and Settings' 폴더를  백업하는 방식이다. 여기서는 테스트를 위해 이 설정을 'C:\SyncWork-Test' 로 변경한다. (여기까지 작업된 파일은 백업하고 수정하는 편이 좋겠군.) 
주의 ! 여기서 path 값은 cygpath 로 지정해야 한다. 

[SyncWork-Test]
    # path = c:/SyncWork-Test
    path = /cygdrive/c/SyncWork-Test
    comment = Sync work test
    strict modes = false
    auth users = UUU, oranke
    secrets file = c:/cygwin/etc/rsyncd-conf/rsyncd.secrets
    # hosts allow = 172.16.0.17
    # 서버로 파일전송이 가능하게 하려면 read only = false 로 할 것. 
    # 현재는 가져가는 용도로 설정하고 있으므로 true 로 되어있음. 
    read only = true
    list = false


rsyncd.secrets 파일의 속성을 '읽기 전용' 으로 변경한다. 우클릭 후 읽기전용 체크로 간단히 해결.
또는 이렇게 
chmod 600 rsyncd.secrets 


이제 대강 준비는 완료. 남은 것은 rsyncd 를 서비스로 동작시켜야 한다. cygrunsrv 를 쓰면 되는데... 아까 받아둔 zip 에 예제가 있다. 

cygrunsrv.exe -I rsyncd -e CYGWIN=nontsec -p c:/cygwin/bin/rsync.exe -a "--config=c:/cygwin/etc/rsyncd-conf/rsyncd.conf --daemon --no-detach"


예전 sshd 처럼 서비스 표시이름이 CYGWIN rsyncd 로 보이게 하려면 -d 또는 --disp 옵션을 추가한다. 

cygrunsrv.exe -I rsyncd -d "CYGWIN rsyncd" -e CYGWIN=nontsec -p c:/cygwin/bin/rsync.exe -a "--config=c:/cygwin/etc/rsyncd-conf/rsyncd.conf --daemon --no-detach"


경로명을 cygpath 로 쓰라는 경고가 나오긴 하지만 무시해도 괜찮음. 신경쓰인다면 -p 옵션 뒤의 rsync.exe 경로를 /bin/rsync.exe 로 바꿔줄 것. 

시작 -> 실행 에서 services.msc 를 입력 후 "CYGWIN  rsyncd" 가 제대로 표시된다면 제대로 따라온 것임. 



마지막으로 방화벽에서 ryncd 가 사용할 873번 포트를 열어주자. 

  • Name: rsyncd
    Port number: 873
    Type: TCP




또는 cygwin-rsyncd-2.6.8_0.zip 내의 배치파일에 쓰인 netsh 명령어를 사용할 수 있다. 

netsh firewall set portopening protocol = TCP port = 873 name = rsyncd mode = enable scope = CUSTOM addresses = LocalSubnet


모든 준비가 끝났으면 다음 명령어로 이 rsyncd 서비스를 시작해본다.  

cygrunsrv --start rsyncd
    또는
 net start rsyncd 


telnet localhost 873 명령을 내려보면 rsyncd 가 제대로 동작하는지 확인할 수 있다. 또한 c:\cygwin\etc\rsyncd-conf 폴더에는 rsyncd.pid 파일이 생성되며 이 파일의 내용을 열어보면 ps -eaf | grep rsync 로 출력되는 rsyncd 의 PID와 일치한다. 앞에서도 언급했듯이 데몬 종료는 이 프로세스를 Kill로 죽이면 되지만... 지금은 서비스로 구동시키고 있으므로 참고만 하자. 




이제 rsyncd 의 파일을 임의 폴더에 동기화시켜보자. 
아까 설정해 둔 'C:\SyncWork-Test' 폴더에 적당한 파일들을 넣어둔다. 
그리고 임의 폴더로 이동한 뒤 다음과 같은 명령어를 내려보자. (-a 는 아카이브, -v 는 목록표시 옵션이다.)

$ rsync -av oranke@localhost::SyncWork-Test
Password:
receiving incremental file list
drwx------           0 2010/10/27 16:37:10 .
-rwx------      863778 2010/10/27 16:12:22 aaa.bmp
-rwx------        7037 2010/10/27 16:12:26 aaa.png
-rwx------        2884 2005/11/25 02:54:51 curl_stack.h
-rwx------      196360 2006/06/22 21:40:25 curl_stack.o
-rwx------        5112 2006/06/22 21:39:25 main.cc
-rwx------      443728 2006/06/22 21:40:27 main.o

sent 65 bytes  received 203 bytes  107.20 bytes/sec
total size is 1518899  speedup is 5667.53



싱크를 위해 올려둔 파일의 목록이 잘 출력되는 것을 볼 수 있다. 
뒷부분에 받을 경로를 지정함으로서 실제로 파일을 전송받을 수 있다. 다음과 같이 명령어를 바꿔보자. 

$ rsync -avz --delete oranke@localhost::SyncWork-Test ./
Password:
receiving incremental file list
./
aaa.bmp
aaa.png
curl_stack.h
curl_stack.o
main.cc
main.o

sent 182 bytes  received 153356 bytes  43868.00 bytes/sec
total size is 1518899  speedup is 9.89


잘 전송된다. 처음 실행했을 때와 옵션을 조금 바꾸어주었는데, -z 는 전송시 정보를 압축, --delete 는 서버에서 지워진 파일을 여기서도 지우라는 옵션이다. 제대로 잘 지워지는지 C:\SyncWork-Test 폴더에서 임의의 파일을 삭제하고 다시 저 명령을 내려보자. 

$ rsync -avz --delete oranke@localhost::SyncWork-Test ./
Password:
receiving incremental file list
deleting main.cc
./

sent 68 bytes  received 194 bytes  104.80 bytes/sec
total size is 1513787  speedup is 5777.81


일단 이정도의 기능만으로도 충분히 증분백업 환경 구축이 가능할 듯 싶은데... 나머지는 차차 뒤져보자구.



* rsyncd.conf 의 auth users, secrets files 항목을 주석처리하면 암호입력 없이 접근할 수 있다. 

rsync -avz --delete localhost::SyncWork-Test ./

또한 접속시 콜론을 한개만 사용하면, 접속에 rsh 를 사용하게 된다. 

rsync -avz --delete localhost:SyncWork-Test
oranke_f@localhost's password:
receiving incremental file list
rsync: link_stat "/home/oranke_f/SyncWork-Test" failed: No such file or directory (2)

sent 8 bytes  received 12 bytes  3.08 bytes/sec
total size is 0  speedup is 0.00
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at /home/lapo/packaging/rsync-3.0.7-1/src/rsync-3.0.7/main.c(1508) [Receiver=3.0.7]

에러난 화면이지만 자세히보면 rsync 는 localhost 의 oranke_f 계정의 SyncWork-Test 디렉토리를 찾게 된다. oranke_f 가 지정된 것은 현재 작업중인 세션의 로그온 유저이기 때문이다. 따라서 다음과 같은 방식으로

$ rsync -avz --delete user@localhost:Some-Dir ./

특정 유저의 디렉토리를 동기화 할 수도 있다. (SomeDir 에 ./ 를 넣어봐도 재밌다.)

역으로 이런 처리도 가능. 

$ rsync -avz ./ oranke_f@localhost:test
oranke_f@localhost's password:
sending incremental file list
./
1232612189_IMG_0861.jpg
1271342789_QUvpyA8N_EAB7B8EB94B4EAB1B4.jpg
1282266251_fVObgSNQ_bman.png
200308187.jpg
20060912k_2.gif
83221693.jpg

sent 1788868 bytes  received 129 bytes  511142.00 bytes/sec
total size is 1787778  speedup is 1.00


localhost 의 oranke_f 홈의 test 디렉토리로 현재 디렉토리의 파일을 전송하는 예이다. 

단순히 rsyncd 데몬만 띄운 상태로 rsh 가 결합되니 참 유용해지는군. ssh를 쓰고 키교환을 미리 해두면 말 그대로 끝내주겠는걸~~
(잘 이해가 안되는건... 현재 테스트중인 PC에 rsh 데몬따위는 없는데... ssh가 기본으로 돌아버리는데... 신기함... )

중간에 몇가지 주의해야 할 부분이 있기는 하지만 ssh를 사용하는 참고자료. -e ssh 옵션이 눈에 띈다. 




rsyncd 가 사용하는 포트변경은 리눅스의 경우 xinetd 에서 할 수 있다. 

cygwin 은 잘 모르겠네... 
아무튼 변경된 포트로 접속할 때는 --port 옵션을 사용한다. 

rsync --port=NNN ....


찾아봐도 없어서 도움을 구합니다...
-e ssh -p 5000 .. 이런 씩으로 하면 작동이 안돼네요

==> rsync으로 백업하고자 할경우에 ssh보안 프로토콜의 사용자 정의 포트 (예: 5000)로
연결하여 백업하고 자 할경우 아래와 같이 --rsh 옵션을 활용하시기 바랍니다.

#> rsync -av --progress --inplace --rsh='ssh -p5000' somefile user@host:somedir/  [엔터]





rsync 를 이용하여 윈도우즈 백업하기 Mini-HOWTO  


시간날 때 cygwin 에서 rsync, sshd 에 필요한 요소만 추려서 설치할 수 있는 방법도 고민해보자. 

윈도에서 쓰기 좋게 패키징된 물건
http://sourceforge.net/projects/backuppc/files/cygwin-rsyncd/
http://backuppc.sourceforge.net/


숫자를 한글로 바꾸기

탐구생활/Delphi

간단할 것 같았는데 만단위 절삭, "일"을 표시하거나 말아야 하는 경우 등  생각해야 할 부분이 꽤 많다. 
사실 델마당 강좌란에 이미 병규형님이 만들어두신 숫자 -> 한글 함수가 있지만, "일"을 생략해야 하는 경우의 처리가 없어서 이번 기회에 잘 버무려놓기로 한다. 

문제는 귀차니즘... 잠시 고민하다 최종욱님 블로그에서 자바로 된 코드를 걷어다 델파이로 바꾸어보았다. 



(*
  임의의 숫자를 받아 한국어 숫자 표기 기법에 따라 변환한 문자열을 반환

  최종욱님의 자바코드를 델파이로 변환.
  http://link.egloos.com/4165494

  파라미터
    Number    : 한국어로 바꿀 숫자
    DecimalSp : 만단위 단위로 공백 삽입 여부. 
    Official  : 이 값이 True 면 '일'을 생략하지 않는다.
*)

function IntToKorean(Number: Int64;
  DecimalSp: Boolean = false;
  Official: Boolean = false): String;
const
  NumberStr : Array [0..7] of String = (
    '이', '삼', '사', '오', '육', '칠', '팔', '구'
  );
  LevelStr : Array [0..2] of String = (
    '십', '백', '천'
  );
  DecimalStr : Array [0..4] of String = (
    '만', '억', '조', '경', '해'
  );

var
  Digits: String;
  Len: Integer;
  SubCount: Boolean;

  Base: Integer;     // 현재 다루고 있는 자릿수. 
  BaseMod4: Integer; // 만단위내 일십백천 단위.
  Digit: Integer;

  i: Integer; 
begin
  if Number = 0 then
    Result := '영'
  else
  if (Number < 0) then
    Result := '마이너스 ' + IntToKorean(-Number, Official)
  else
  begin
    Digits := IntToStr(Number);
    Len := Length(Digits);
    SubCount := false;

    for i:= 0 to Len-1 do
    begin
      Base := Len -i -1;
      BaseMod4 := Base mod 4;
      Digit := Ord(Digits[i+1]) - Ord('0');

      if Digit <> 0 then
      begin
        SubCount := true;

        if Digit = 1 then
        begin
          if Official or (BaseMod4 = 0) then
            Result := Result + '일';
        end else
          Result := Result + NumberStr[Digit-2];

        if BaseMod4 > 0 then
          Result := Result + LevelStr[BaseMod4-1];
      end;


      if BaseMod4 = 0 then
      begin
        if SubCount and (Base > 0) then
        begin
          Result := Result + DecimalStr[Base div 4 - 1];

          if DecimalSp then
            Result := Result + ' ';
        end;
      end;
    end;
  end;
end;


숫자값 123523121821 을 변환할 경우 

IntToKorean(123523121821);
  결과 : 천이백삼십오억이천삼백십이만천팔백이십일

IntToKorean(123523121821, true);
  결과 : 천이백삼십오억 이천삼백십이만 천팔백이십일

IntToKorean(123523121821, true, true);
  결과 : 일천이백삼십오억 이천삼백일십이만 일천팔백이십일