특정 시간에 컴퓨터를 깨우는 방법

@codemaru · October 04, 2011 · 10 min read

중학교 때 가끔 굿모닝 팝스를 들었습니다. 그런데 잘 아시겠지만 그 때나 지금이나(?!) 굿모닝 팝스는 굉장히 아침 일찍 합니다. 아침잠 많은 제가 듣기에는 제법 벅찬 방송이었죠. 그 시절 뭐 지금처럼 mp3나 인터넷 스트리밍이 흔한 시절도 아니었으니 방송을 놓치면 그냥 그날은 공치는 셈이었습니다. 그래서 전 가끔 내 라디오가 그 시간에 나를 깨워주면 참 좋겠다는 생각을 많이 했었습니다. 제가 그 때 사용하던 구형 라디오는 정말 6.25 때나 쓰던 것처럼 생겼던 것이었지요. 당연히 알람 기능 따위는 없었습니다. 그래서 저는 알람 시계에 연결을 해서 그 녀석이 알람 시계가 울릴 때 켜지도록 하는 기능을 만들었었죠. 한동안 그렇게 잘 썼는데 알람시계를 라디오 내부에 연결을 해두니 미관상 별로 좋지가 않았습니다. 그래서 선을 깔끔하게 한다는 명분아래 시계를 밖으로 빼서 220볼트에 물렸죠. 그리곤 전원을 꽂았습니다. ㅋㅋ~ 미친 짓이었죠. 콘덴서가 펑하고 터지더니 안에서 휴지 같은 것들이 튀어 나오더군요. 그 때 이후로 그런 장난은 다시는 치지 않습니다.

요즘은 머 라디오는 잘 쓰지도 않으니 종종 컴퓨터로 이런 짓들을 하고 있을 학생들이 있을지도 모르겠다는 생각을 해봅니다. 알람 시계를 컴퓨터에다 연결하는 학생들 말이죠. 컴퓨터가 아침에 자동으로 일어나서는 나에게 정보를 알려주는 일은 상상만으로도 즐겁잖아요. 뭐 일어나서 오늘의 날씨를 알려준다던지, 뉴스를 읽어준다든지 말이죠. 어쨌든 그런 니즈가 많이 있습니다. 물론 똑똑한 학생이라면 알람 시계를 연결하기 전에 웹서핑을 해볼 것이고, 그러면 자신이 사용할 수 있는 프로그램이 제법 많이 있다는 사실에 놀랄지도 모르겠군요. ㅋㅋ~ 오늘은 그런 프로그램들이 어떻게 해당 기능을 구현하는지에 대해서 알아보도록 하겠습니다.

윈도우의 전원 상태는 크게 5가지 상태가 있습니다. 전원 상태에 대한 세부 설명은 아래 페이지에 자세하게 나와 있습니다.

http://msdn.microsoft.com/en-us/library/windows/desktop/aa373229(v=vs.85).aspx

크게 구분을 지어 보자면 동작 상태(S0), 수면 상태(S1, S2, S3), 하이버네이션 상태(S4), 소프트 오프 상태(S5), 그리고 완전 오프 상태로(G3) 나뉩니다. 여기서 소프트웨어 적으로 깨어날 수 있는 상태는 동작, 수면, 하이버네이션 상태가 전부입니다. 소프트 오프와 완전 오프 상태에서는 소프트웨어 적으로 깨어날 수가 없습니다. 그 중에서도 컴퓨터를 특정 시간에 깨워주는 소프트웨어들이 자주 애용하는 상태는 다름아닌 하이버네이션입니다. 소프트웨어적으로 깨어날 수 있는 상태 중에서 가장 적은 에너지를 소모하기 때문입니다. 사실 뭐 거의 오프 상태나 다름 없습니다.

자 그럼 이제 구체적으로 컴퓨터를 특정 시간에 어떻게 깨우는지에 대해서 방법을 알아보도록 하겠습니다. 여기에 사용되는 가장 기본적인 테크닉은 컴퓨터를 하이버네이션으로 끄고, 특정 시간 이후에 다시 깨우는 것입니다. 그렇담 당연히 하이버네이션으로 끄는 것을 먼저 알아야겠죠. 하이버네이션을 시키는 방법은 정말 간단합니다. 아래와 같은 단 한줄의 코드면 충분합니다.

  1. SetSystemPowerState(FALSE, FALSE);
SetSystemPowerState(FALSE, FALSE);

하지만 세상 일이란게 그리 쉽진 않죠. 생각처럼 잘 동작하지 않을 겁니다. 왜냐하면 우리에겐 권한이 없거든요. 전원을 꺼버릴 권한이요. 그렇다면 당연히 해당 권한을 획득해야겠죠. SeShutdownPrivilege가 그런 역할을 하는 권한입니다. 아래 코드는 그런 권한을 획득하는 역할을 합니다. 아래 코드를 통해서 권한을 획득하고 SetSystemPowerState를 하면 정상적으로 꺼지는 것을 확인하실 수 있을 겁니다.

static BOOL WINAPI
_EnableShutDownPriv()
{
	BOOL ret = FALSE;
	HANDLE process = GetCurrentProcess();
	HANDLE token;

	if(!OpenProcessToken(process
						, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
						, &token))
		return FALSE;

	LUID luid;
	LookupPrivilegeValue(NULL, "SeShutdownPrivilege", &luid);

	TOKEN_PRIVILEGES priv;
	priv.PrivilegeCount = 1;
	priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	priv.Privileges[0].Luid = luid;
	ret = AdjustTokenPrivileges(token, FALSE, &priv, 0, NULL, NULL);
	CloseHandle(token);

	return TRUE;
}

이제는 다시 깨우기 위한 준비를 배워봅시다. 하이버네이션 상태에서 깨어나기 위해서는 윈도우에서 제공해주는 대기 타이머라는 것을 사용합니다. 웨이터블 타이머(waitable timer)가 정식 명친인데요, 이 녀석은 하이버네이션 상태가 되더라도 동작한다는 끈질긴 특징이 있습니다. 기본적인 기능은 타이머와 동일하다고 생각하면 되겠습니다. 즉, 우리가 지정한 시점이되면 이벤트가 트리거되고 타이머를 대기하던 코드가 깨어나서 수행되는 겁니다. 다음 코드는 대기 타이머를 사용하는 방법을 보여주고 있습니다. 여려운 건 없죠. 단지 시간이 100나노초 단위라는 것에 주의하면 되겠습니다. 양수를 지정하면 절대 시간을, 음수를 지정하면 상대 시간이 됩니다. 절대 시간은 윈도우에서 사용하는 FILETIME과 동일한 값을 가집니다. 아래 코드에서는 음수를 지정했으니 타이머를 대기하는 순간부터 secs초 이후에 해당 타이머가 발동하는 구조가 됩니다.

HANDLE timer = CreateWaitableTimer(NULL, TRUE, "MyWaitableTimer");
if(timer == NULL)
	return FALSE;

__int64 nanoSecs;
LARGE_INTEGER due;

nanoSecs = -secs * 1000 * 1000 * 10;
due.LowPart = (DWORD) (nanoSecs & 0xFFFFFFFF);
due.HighPart = (LONG) (nanoSecs >> 32);

if(!SetWaitableTimer(timer, &due, 0, 0, 0, TRUE))
	return FALSE;

끝으로 한 가지 더 기억해야 할 것이 있습니다. 하이버네이션 상태에서 깨어난 다음에 모니터 전원이 필요하다면 윈도우에 해당 전원이 필요하다는 사실을 알려주어야 합니다. 어떻게 요청하냐구요? SetThreadExecutionState(ES_DISPLAY_REQUIRED); 이 한 줄이면 충분합니다. 해당 함수를 호출하면 윈도우는 모니터 전원이 필요하다는 것을 눈치채고 모니터에도 전원을 공급해 줍니다. 즉, 모두 다 종합해보면 이렇습니다. 권한을 얻고, 웨이터블 타이머를 설정하고, 하이버네이션을 시킵니다. 웨이터블 타이머가 동작하면 시스템에 우리가 모니터 전원이 필요로 하다는 의사를 알려주고는 하고 싶은 나머지 작업을 진행하면 됩니다. 아래 코드에 모두 다 들어 있습니다.

DWORD CALLBACK
ShutDownProc(LPVOID p)
{
	PHANDLE timer = (PHANDLE) p;

	WaitForSingleObject(*timer, INFINITE);
	CloseHandle(*timer);
	SetThreadExecutionState(ES_DISPLAY_REQUIRED); 

	return 0;
}

PWRMGMT_API BOOL WINAPI
HibernateAndReboot(int secs)
{
	if(!_EnableShutDownPriv())
		return FALSE;

	HANDLE timer = CreateWaitableTimer(NULL, TRUE, "MyWaitableTimer");
	if(timer == NULL)
		return FALSE;

	__int64 nanoSecs;
	LARGE_INTEGER due;

	nanoSecs = -secs * 1000 * 1000 * 10;
	due.LowPart = (DWORD) (nanoSecs & 0xFFFFFFFF);
	due.HighPart = (LONG) (nanoSecs >> 32);

	if(!SetWaitableTimer(timer, &due, 0, 0, 0, TRUE))
		return FALSE;

	if(GetLastError() == ERROR_NOT_SUPPORTED)
		return FALSE;

	HANDLE thread = CreateThread(NULL, 0, ShutDownProc, &timer, 0, NULL);
	if(!thread)
	{
		CloseHandle(timer);
		return FALSE;
	}

	CloseHandle(thread);
	SetSystemPowerState(FALSE, FALSE);
	return TRUE;
}

깨우고 난 다음 무엇을 할지는 전적으로 여러분의 몫입니다.

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중