윈도우즈 서비스 프로그램

Windows Service 프로그램 작성

Go에서 Windows Service 프로그램을 작성하기 위해 golang.org/x/sys/windows/svc 에 있는 svc 패키지를 사용할 수 있다. 윈도우즈 OS에서 서비스 프로그램은 Service Control Manager(SCM)에 의해 Start, Stop, Pause, Resume 과 같은 컨트롤을 받게되는 특별한 형태의 프로그램이다. 또한 Windows Service 프로세스는 사용자가 보는 데스크탑 세션에 있지 않고, 화면이 보이지 않는 세션 0에서 동작하므로 콘솔 출력을 하더라도 이를 사용자가 볼 수 없는 특징이 있다.

Go 프로그램에서 서비스용 프로그램을 만들기 위해 svc 패키지를 사용하는데, 먼저 다음과 같이 go get 명령을 사용하여 해당 패키지를 다운받는다.

C\GoApp\Src> go get golang.org/x/sys/windows/svc

서비스 프로그램은 main() 에서 svc.Run() 메서드를 호출하는 것으로 시작한다. svc.Run()은 첫번째 파라미터로 서비스명을, 두번째 파라미터로 svc.Handler 인터페이스를 구현한 오브젝트를 갖는다. svc.Handler 인터페이스는 Execute() 라는 하나의 메서드를 갖는데, 실제 이 메서드 안에서 서비스의 실제 동작 코드가 모두 작성된다. (서비스에서 svc.Run()을 호출하면 콘솔출력을 볼 수 없으므로, 디버깅 시에는 대신 debug.Run()을 사용하여 콘솔 출력을 볼 수 있도록 한다)

아래 예제를 살펴보면, 우선 dummyService라는 서비스 Type을 하나 정의한 후, dummyService 타입에 대해 svc.Handler 인터페이스 메서드인 Execute()를 구현하고 있음을 볼 수 있다. 특히, Execute() 메서드의 파라미터 중 두번째 svc.ChangeRequest 채널은 SCM 으로부터 서비스가 Start 되거나 Stop 될 때 이러한 요청을 받아들이는 역활을 하고, 세번째 파라미터 svc.Status 채널은 서비스 프로그램의 상태를 SCM으로 전달하는 역활을 한다. Execute() 메서드는 서비스가 처음 시작될 때 실행되며, 따라서 Execute() 메서드의 처음 부분은 서비스 상태를 StartPending으로 한 후, 서비스할 실제 코드(여기서는 runBody)를 실행하고 서비스 상태를 Running으로 지정한다. 여기서 runBody()는 간단한 예로 10초마다 현재 시간을 로그파일에 새로 쓰는 코드를 작성하였다.

package main

import (
	"golang.org/x/sys/windows/svc"
	//"golang.org/x/sys/windows/svc/debug"
	"io/ioutil"
	"time"
)

// 서비스 Type
type dummyService struct {
}

// svc.Handler 인터페이스 구현
func (srv *dummyService) Execute(args []string, req <-chan svc.ChangeRequest, stat chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
	stat <- svc.Status{State: svc.StartPending}

	// 실제 서비스 내용
	stopChan := make(chan bool, 1)
	go runBody(stopChan)

	stat <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}

LOOP:
	for {
		// 서비스 변경 요청에 대해 핸들링
		switch r := <-req; r.Cmd {
		case svc.Stop, svc.Shutdown:
			stopChan <- true
			break LOOP

		case svc.Interrogate:
			stat <- r.CurrentStatus
			time.Sleep(100 * time.Millisecond)
			stat <- r.CurrentStatus

		//case svc.Pause:
		//case svc.Continue:
		}
	}

	stat <- svc.Status{State: svc.StopPending}
	return
}

/*** 서비스에서 실제 하는 일 ***/
func runBody(stopChan chan bool) {
	for {
		select {
		case <-stopChan:
			return
		default:
			// 10초 마다 현재시간 새로 쓰기
			time.Sleep(10 * time.Second)
			ioutil.WriteFile("C:/temp/log.txt", []byte(time.Now().String()), 0)
		}
	}
}

func main() {
	err := svc.Run("DummyService", &dummyService{})
	//err := debug.Run("DummyService", &dummyService{}) //콘솔출력 디버깅시
	if err != nil {
		panic(err)
	}
}

Execute()의 중간은 for 무한루프로 묶여 있는 부분으로서, 이는 SCM 으로부터 서비스가 Stop되거나 Pause, Resume 이 될 때, 각각 필요한 조치를 취해 주는 코드이다. 즉, 사용자가 서비스 관리자(services.msc) 에서 DummyService를 Stop 하면, stopChan 채널에 true를 보내 runBody()를 중지시키게 한다. 또한 SCM에서 현재 서비스 상태를 쿼리하면(Interrogate), 적정한 상태를 알려주는 코드를 작성한다. Pause나 Resume 요청에 대해서도 case 별로 특정한 코드를 실행할 수 있지만, 실제 많은 윈도우즈 서비스들이 이 두 개의 기능을 사용하지 않고 있다.

프로그램이 작성되었으면, 아래와 같이 프로그램을 빌드하고, sc.exe (윈도우즈 서비스 유틸러티)를 사용하여 서비스를 등록한다. 이때 binPath= 과 exe 경로 사이에 빈칸 하나가 있음에 주의하자. 서비스가 생성되었으면, 서비스 관리자(services.msc)에서 해당 서비스를 Start하여 테스트한다.

C\GoApp\Src> go build dummyService.go
C\GoApp\Src> sc create DummyService binPath= c:\goapp\src\dummyService.exe

테스트 후 서비스를 삭제하기 위해서는 다음 명령을 실행한다.

C\GoApp\Src> sc delete DummyService