파일 버전

이 글은 월간 마이크로소프트웨어(일명 마소) 2010년 6월 뉴스캐스트에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.

꼭 상용프로그램에만 해당하는 이야기는 아니다. 심지어 개인이 혼자 북치고 장구치는 소규모 프로젝트일지라도 인기 있는 프로그램이라면 업데이트, 특히 자동 업데이트는 필수적이다. 한 명이 간신히 발걸음을 옮길만한 오솔길에서 16차선 고속도로로 확장된 인터넷 대역폭 덕분에 사용자들은 소프트웨어가 제 몸을 스스로 돌보길 원하게 됐다. 하여튼 세상이 좋아진다고 다 좋은 건 아니다. 편하게 먹고 살면 좋은데 업데이트 기능까지 만들라니.

그런데 자동으로 업데이트를 하려면 프로그램이 자신의 몸 상태를 잘 알아야 한다. 무쇠 주먹을 티타늄 주먹으로 바꾼 지 하루가 지나서 그 사실을 까맣게 잊고 티타늄 주먹을 또 주문한다면 국방비를 내는 시민들이 시청 앞에서 데모라도 벌이게 될지 모를 일이다. 컨테이너 성벽이 들어서는 불미스런 사태를 미연에 방지하려면 소프트웨어가 자신의 신체 하나하나, 그러니까 파일의 상태를 정확히 알아야 한다. 예를 들어, 오픈 소스로 개발 중인 게임 서버 라이브러리의 실행파일을 선택하고 ‘속성’창을 열면 아래와 같이 구체적인 정보가 드러난다.

파일의 속성

속성 창

그런데 자동으로 업데이트를 하려면 프로그램이 자신의 몸 상태를 잘 알아야 한다. 무쇠 주먹을 티타늄 주먹으로 바꾼 지 하루가 지나서 그 사실을 까맣게 잊고 티타늄 주먹을 또 주문한다면 국방비를 내는 시민들이 시청 앞에서 데모라도 벌이게 될지 모를 일이다. 컨테이너 성벽이 들어서는 불미스런 사태를 미연에 방지하려면 소프트웨어가 자신의 신체 하나하나, 그러니까 파일의 상태를 정확히 알아야 한다. 예를 들어, 오픈 소스로 개발 중인 게임 서버 라이브러리의 실행파일을 선택하고 ‘속성’창을 열면 아래와 같이 구체적인 정보가 드러난다.

리소스 파일을 이용한 파일 정보 관리

비주얼 스튜디오에서 파일 정보 입력하기

리소스 파일을 이용한 파일 정보 관리

위와 같이 속성 창을 이용해 뭔가 있어 보이게 파일의 정보를 보여주기는 그다지 어렵지 않다. 파일 정보가 들어간 리소스 파일을 소스 코드와 함께 빌드하면 끝이다. 비주얼 스튜디오 사용자라면 임의로 이름 지은 리소스 파일을 하나 넣고 ‘리소스 추가’에서 ‘Version’을 선택하면 준비 완료, 행복 시작이다.

C++에서 파일 정보 조회하기

여러분이 마징가의 수석 엔지니어라고 해보자. 국방부에서 업그레이드 예산을 승인해준 덕분에 미사일 한방이면 구겨진 휴지마냥 찌그러질 무쇠 대신 골프채에 널리 쓰인다는 티타늄으로 바꾸게 됐다. 부품 교체는 오른팔, 왼팔, 오른다리 등의 순으로 진행된다. 수석 엔지니어로서 여러분은 업그레이드가 완료되기 전까진 마징가가 불의로 작동하지 않길 바란다. 주먹은 티타늄인데 팔은 무쇠라면 제대로 된 펀치가 나올 리 없기 때문이다. 주먹은 멀쩡한데 팔이 찌그러질지도 모른다.

이런 경우라면 운영체제를 띄우는 과정에서 각 부품의 상태를 확인하고 제 몸에 무쇠 한 조각이라도 남아 있는지 점검해야 한다. 이를테면 리소스 파일에 담긴 파일 정보를 확인하는 작업이 선행되어야 한다. 임베디드 운영체제에서 쓰는 프로그래밍 언어도 다양해지긴 했지만 마징가는 오래된 로봇이다 보니 여전히 C++로 운영된다고 치고 어떻게 파일 버전을 가져오는지 알아보기로 한다. 구닥다리로 치자면 C가 C++의 할아버지 격은 되지만 뭐 절차지향적인 기술은 선호하는 바가 아니라서 말이다. (왜 운영체제가 유서 깊은 xNix가 아니라 Windows인지는 따져 묻지 말자.)

우선 파일 버전 정보 클래스(FileVersionInfo)의 외형부터 설계해본다. 물론 밑바닥부터 하나하나 논의하자는 이야기는 아니고 얼그레이에 개발한 소스코드가 있으므로 여기선 헤더 파일을 살펴보고 설계 패턴을 논의한다. 번쩍하고 창의적인 아이디어가 떠올라 설계한 것은 아니다. 디자인은 닷넷 프레임워크를 본 땄다. 쓸데 없는 노력을 기울이며 자뻑하는 취미가 없는 삭막한 남자라서 말이다.

#pragma once
#pragma comment(lib,"version.lib")

#include "Uncopyable.h"
#include "tstring.h"
#include "tsstream.h"
#include "RAII.h"

namespace Earlgrey
{
	class FileVersionInfo : private Uncopyable
	{
	private:
		explicit FileVersionInfo(
			const _tstring& fileVersion
			, const _tstring& productName
			, const _tstring& productVersion
			, const _tstring& fileDescription
			);

	public: // class methods
		static FileVersionInfo GetVersionInfo(const _tstring& fileName);

	public: // instance methods
		inline _tstring& FileVersion()
		{
			return m_fileVersion;
		}
		inline const _tstring& FileVersion() const
		{
			return m_fileVersion;
		}

		inline _tstring& ProductName()
		{
			return m_productName;
		}
		inline const _tstring& ProductName() const
		{
			return m_productName;
		}

		inline _tstring& ProductVersion()
		{
			return m_productVersion;
		}
		inline const _tstring& ProductVersion() const
		{
			return m_productVersion;
		}

		inline _tstring& FileDescription()
		{
			return m_fileDescription;
		}
		inline const _tstring& FileDescription() const
		{
			return m_fileDescription;
		}

	private:
		_tstring m_fileVersion;
		_tstring m_productName;
		_tstring m_productVersion;
		_tstring m_fileDescription;

	};
}

가만 보면 FileVersionInfo 인스턴스는 파일 정보(FileVersion), 제품 이름(ProductName) 등을 멤버 변수에 담는 게 전부다. FileVersionInfo 클래스의 핵심은 정적 메서드인 GetVersionnfo이다. 이 메서드를 호출하면 지정한 파일에 담긴 파일 버전 정보를 가져다 FileVersionInfo 인스턴스에 담는다. 결국 GetVersionInfo만 알면 오늘의 강의(?)는 끝, 행복한 일상으로의 복귀만이 남을 뿐이다.

FileVersionInfo FileVersionInfo::GetVersionInfo(const _tstring& fileName)
{
	using namespace std::tr1;

	// 파일로부터 버전정보 데이터의 크기가 얼마인지를 구합니다.
	DWORD infoSize = GetFileVersionInfoSize(fileName.c_str(), 0);
	if(infoSize == 0)
		throw std::exception("Getting version information failed!");

	// 버퍼할당
	BYTE * version = new BYTE[infoSize];
	shared_ptr<BYTE> bufferPtr(version, ArrayDeleter<BYTE>() );

	if(version == NULL)
		throw std::exception("Memory allocation failed!");

	// 버전 정보 데이터를 가져옵니다.
	if(GetFileVersionInfo(fileName.c_str(), 0, infoSize, version) == 0)
		throw std::exception("Getting version information failed!");

	VS_FIXEDFILEINFO* pFileInfo = NULL;
	UINT fileInfoSize = 0;
	// buffer로부터 VS_FIXEDFILEINFO 정보를 가져옵니다.
	if(VerQueryValue(version, _T("\\"),(LPVOID*)&pFileInfo, &fileInfoSize) == 0)
		throw std::exception("Getting version information failed!");

	// FileVersion
	WORD majorVer, minorVer, buildNum, revisionNum;
	majorVer = HIWORD(pFileInfo->dwFileVersionMS);
	minorVer = LOWORD(pFileInfo->dwFileVersionMS);
	buildNum = HIWORD(pFileInfo->dwFileVersionLS);
	revisionNum = LOWORD(pFileInfo->dwFileVersionLS);

	_tstringstream ss;
	ss << majorVer << _T(".") << minorVer << _T(".") << buildNum << _T(".") << revisionNum;
	_tstring fileVersion(ss.str());


	void  * buffer = NULL;
	UINT bufLen = 0;
	DWORD * translation = NULL;

	// TRANSLATION
	if(VerQueryValue(
		version
		, _T("\\VarFileInfo\\Translation")
		, (LPVOID*)&translation, &bufLen
		) == 0
	)
		throw std::exception("Getting translation failed!");

	EARLGREY_ASSERT(translation != NULL);


	// PRODUCT NAME
	TCHAR path[MAX_PATH];
	wsprintf(path
		, _T("\\StringFileInfo\\%04x%04x\\ProductName")
		, LOWORD(*translation), HIWORD(*translation)
		);

	if( ::VerQueryValue(version, path, &buffer, &bufLen) == 0 )
		throw std::exception("Getting a product name failed!");

	EARLGREY_ASSERT(buffer != NULL);

	_tstring productName( (LPCTSTR)buffer );

	// PRODUCT VERSION
	wsprintf(path
		, _T("\\StringFileInfo\\%04x%04x\\ProductVersion")
		, LOWORD(*translation), HIWORD(*translation)
		);

	if( ::VerQueryValue(version, path, &buffer, &bufLen) == 0 )
		throw std::exception("Getting a product version failed!");

	EARLGREY_ASSERT(buffer != NULL);

	_tstring productVersion( (LPCTSTR)buffer );

	// File Description
	wsprintf(path
		, _T("\\StringFileInfo\\%04x%04x\\FileDescription")
		, LOWORD(*translation), HIWORD(*translation)
		);

	if( ::VerQueryValue(version, path, &buffer, &bufLen) == 0 )
		throw std::exception("Getting a file description failed!");

	EARLGREY_ASSERT(buffer != NULL);

	_tstring fileDescription( (LPCTSTR)buffer ); // "1, 0, 0, 1

	return FileVersionInfo(fileVersion, productName, productVersion, fileDescription);
}

GetVersionInfo 메서드의 소스 코드는 제법 길지만 실제론 비스무레한 일을 반복할 뿐이라 별 게 없다. 주목해서 볼 API는 셋에 불과하다.

  • GetFileVersionInfoSize

  • GetFileVersionInfo

  • VerQueryValue

먼저 GetFileVersionInfoSize 함수를 호출해서 파일 버전 정보를 가져오려면 메모리가 어느 정도 필요한지 가늠한다. 그러고 나서 GetFileVersionInfo를 이용해 실제 파일 버전 정보를 버퍼에 담는다. 이제 제품 이름이나 버전 번호 같은 세부 항목에 접근할 일만 남았다. 이제 VerQueryValue 함수가 등장할 차례다. VerQueryValue는 레지스트리를 조회할 때처럼 트리 형태의 경로 주소로 데이터에 접근한다. 그러므로 내가 원하는 데이터가 어느 경로에 있는지 MSDN 등의 문서를 보고 확인할 필요가 있다. 그 외에는 그리 어려운 점이 없다. GetVersionInfo의 소스 코드가 제법 길긴 하지만 버전 번호를 사람이 보기 좋게 문자열로 조합한다던가 하는 일을 할 뿐이다.

맺음말

여기서 다룬 내용은 MSDN 라이브러리를 열심히 읽으면 누구나 알 수 있다. 그러나 누구나 그렇듯 귀찮다. 특별하고 어려운 내용이라 다룬 게 아니라, “바빠죽겠는데 언제 MSDN 라이브러리를 읽냐”라는 독자를 위해 밥을 떠 먹여주려는 것뿐이다. 빨리 일 끝내고 퇴근하자. (정작 글 쓰는 이 사람은 지금 백수지만).

참고

얼그레이 프로젝트에서만 쓰는 클래스 등이 있어 소스 코드를 이해하기 힘들까 봐. 나는야 친절한 Jay Z.

  • _tstring : 문자열 클래스. std::wstring 과 동일하다고 보면 된다.

  • tstringstream : std::wstringstream과 동일하다.

  • EARLGREY_ASSSERT : assert, Assert 등과 동일하다.

Buy me a coffeeBuy me a coffee

최 재훈

블로그, 페이스북, 트위터 고성능 서버 엔진, 데이터베이스, 지속적인 통합 등 다양한 주제에 관심이 많다.
Close Menu