S/W 개발 기반 시스템 구성 – 10. 심볼 서버 구성하기
이 글은 S/W 개발에 가장 기본이 되는 이슈 추적(Issue Tracker), 버전 관리(Version Control), 빌드(Build), 지속적인 통합(CI) 시스템을 구성하는 방법에 대한 일련의 글 중 열 번째이다. 프로그램에 오류가 발생해 종료되는 경우가 있다. 항상 발생하는 경우라면 그나마 찾기가 쉬울 텐데 드물게 발생할 때는 원인을 찾기가 참 어렵다. 게다가 자신이 개발 중인 환경에서는 오류가 발생하지 않는다면 정말 답답하기 그지 없다. 그렇다고 사용자에게 디버깅을 맡길 수도 없으니까.
프로그램에 오류가 발생해 종료될 때 관련 정보를 보낼 것인지 묻는 창을 본 적이 있을 것이다. 오류가 발생할 당시 메모리, 레지스터 등의 정보를 수집해 분석하기 위한 것인데 이와 같은 정보만 있으면 문제를 쉽게 찾을 수 있다. 문제 발생 당시 상황을 그대로 재현해 볼 수 있기 때문이다. 하지만 이런 정보가 있다고 모든 일이 자동으로 해결되지는 않는다. 만든 프로그램에서 사용하는 변수와 함수 등에 대한 정보를 미리 수집해 두고 소스 코드 어느 부분에 해당하는지 등 관련 정보를 미리 만들어 둬야 한다. 이렇게 처리한 정보를 보관해 두고 필요할 때 불러와 쓰기 위한 시스템이 심볼 서버이다.
1. 미리 준비할 것
- Debugging Tools for Windows: 여기에서 이 링크를 통해 받을 수 있다. 최근에는 Windows SDK나 DDK에 포함되어 있으며 Windows XP SP3 이상을 지원한다. 웹에서 다운로드해 설치하거나 ISO 이미지를 받아 설치할 수 있다. 이미지를 받아 설치할 때 해당 O/S에 맞춰 x86과 x64 버전 중 하나를 받으면 된다. 여기서는 Windows Server 2008 R2를 사용하나 빌드할 때 심볼 정보 처리에만 사용할 것이므로
x86x64 버전 이미지를 받아 설치했다. 32비트와 64비트 패키지 선택은 이 내용을 참조한다. - ActivePerl: 여기에서 5.14.2.1402 버전을 받을 수 있다.
- Mercurial source server indexing module
2. 설치
먼저 Windows SDK 설치를 시작한다. Next 버턴을 누른다.
라이선스 동의를 하고 Next 버턴을 누른다.
Windows SDK는 Visual Studio 2005 이상이 필요한데 Visual Studio가 이미 설치되어 있을 경우 적절한 버전인지 먼저 확인한다. 없으면 표시하지 않는다.
설치 위치를 지정하는데 기본값 그대로 사용하면 된다. Next 버턴을 선택한다.
Debugging Tools는 두 곳에서 선택할 수 있다. Common Utilities 아래에서 선택하면 CPU에 따라 x64와 x86 버전 중 하나를 자동으로 설치하며 C:\Program Files\Debugging Tools for Windows (x64) 에 설치한다. 참고로 32비트 운영체제에서는 C:\Progrem Files\Debugging Tools for Windows 가 된다. Redistributable Packages 아래에서 선택하면 x64, x86, Itanium 버전 설치 파일을 모두 설치하며 툴 설치 경로 아래 RedistDebugging Tools for Windows 에서 설치 파일을 찾을 수 있다. 심볼 서버만 설치하면 되므로 필요하지 않은 내용은 모두 해제하면 된다. 선택 후 Next 버턴을 누른다.
Next를 선택하면 설치를 시작한다.
설치 진행화면이 나타난다.
설치를 마치면 Finish를 선택해 완료한다.
설치를 마친 후에 는 C:\Program Files\Debugging Tools for Windows (x64);C:\Program Files\Debugging Tools for Windows (x64)\srcsrv
디렉터리를 환경 변수 중 path 정보에 추가한다.
이번에는 ActivePerl을 설치한다. 설치 파일을 실행하고 Next를 선택한다.
라이선스에 동의하고 Next 버턴을 누른다.
설치 경로와 설치 내용은 기본 설정으로 하고 Next를 선택한다.
설치 경로에 추가하고 Perl 파일을 ActivePerl에 연결하도록 한 후 Next를 선택한다.
설치 준비가 됐으면 Install을 선택해 설치한다.
설치를 마치면 Finish 버턴을 선택해 완료한다.
설치를 마친 후에는 재부팅한다. 그렇지 않으면 Jenkins에서 Perl 실행 경로는 인식하지 못해 다음과 같은 내용과 함께 빌드를 실패할 수 있다.
1 2 3 4 5 6 7 |
[exec] 'perl' is not recognized as an internal or external command, [exec] operable program or batch file. BUILD FAILED - 0 non-fatal error(s), 2 warning(s) E:jenkins_workspaceprocesstestbuilddefault.build(273,10): External Program Failed: C:\Program Files\Debugging Tools for Windows (x64)\srcsrv\hgindex.cmd (return code was 9009) |
3. 심볼 서버 설정
hg.pm과 hgindex.cmd 파일을 C:\Program Files\Debugging Tools for Windows\srcsrv
에 복사한다. 참고로 x64 운영체제에서는 Debugging Tools for Windows (x64)가 된다. 그런 다음 심볼 정보를 인덱싱하도록 스크립트를 수정한다.
주요 변경 내용은 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!-- symbol indexing --> <target name="symbol" description="symbol indexing, and storing"> <foreach item="Folder" property="foldername"> <in> <items> <include name="**/src/**/${build}" /> </items> </in> <do> <exec program="C:\Program Files\Debugging Tools for Windows (x64)\srcsrv\hgindex.cmd"> <arg line='-source="${project::get-base-directory()}" -symbols="${foldername}" -http /debug' /> </exec> <exec program="C:\Program Files\Debugging Tools for Windows (x64)\symstore.exe"> <arg line='add /o /r /f "${foldername}" /s d:\symbols /t "${project::get-name()}" /compress' /> </exec> </do> </foreach> </target> |
전체 내용은 다음과 같다. 프로젝트 이름, PDB 파일을 출력하는 디렉터리 지정, 타겟 호출 등이 바뀌었다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
<?xml version="1.0" encoding='UTF-8'?> <project name="ProcessTest" default="build" basedir=".."> <!-- 서버 빌드인지 확인 (서버 빌드할 때 지정): ci, Daily, Release --> <property name="server_build" value="no" unless="${property::exists('server_build')}" /> <!-- 제품 버전 : 빌드와 태그 만들 때 사용 (서버 빌드할 때 지정) --> <property name="product_major" value="0" unless="${property::exists('product_major')}" /> <property name="product_minor" value="0" unless="${property::exists('product_minor')}" /> <property name="product_patch" value="0" unless="${property::exists('product_patch')}" /> <if test="${server_build!='no'}"> <!-- Jenkins environment variable : build number --> <property name="product_build" value="${environment::get-variable('BUILD_NUMBER')}" /> <echo message="Build Product Ver. No: ${product_major}.${product_minor}.${product_patch}.${product_build}" /> </if> <!-- 빌드 설정 (서버 또는 개발자 빌드할 때 지정) --> <property name="build" value="Release" unless="${property::exists('build')}" /> <property name="vc6_release" value="Win32 Release" /> <property name="vc6_debug" value="Win32 Debug" /> <if test="${build!='Release' and build!='Debug'}"> <fail message="The build configuration doesn't exist! So exit the process." /> </if> <!-- 빌드 설정 : VC6 --> <if test="${build=='Release'}"> <property name="vc6_build" value="${vc6_release}" /> </if> <if test="${build=='Debug'}"> <property name="vc6_build" value="${vc6_debug}" /> </if> <!-- 빌드 결과 출력 디렉터리 (심볼 인덱싱에 사용) --> <property name="output" value="${build}" /> <!-- 빌더 환경 변수 : VC6 --> <if test="${environment::variable-exists('MSDevDir')}"> <property name="vc6_path" value="${environment::get-variable('MSDevDir')}" /> <echo message="${vc6_path}" /> </if> <!-- 빌더 환경 변수 : VC10 --> <if test="${environment::variable-exists('VS100COMNTOOLS')}"> <property name="vcnet_path" value="${environment::get-variable('VS100COMNTOOLS')}" /> <echo message="${vcnet_path}" /> </if> <if test="${not property::exists('vc6_path') and not property::exists('vcnet_path')}" > <fail message="Cann't find builder! So exit the process." /> </if> <!-- clean --> <target name="clean" description="remove all generated files"> <delete verbose="true"> <fileset> <exclude name="**/vendor/**" /> <include name="bin/**" /> <include name="src/lib/**" /> <include name="**/*.aps" /> <include name="**/*.ncb" /> <include name="**/*.opt" /> <include name="**/*.plg" /> <include name="**/*.suo" /> <include name="**/*.user" /> <include name="**/*.sdf" /> <include name="**/Release/**" /> <include name="**/Debug/**" /> <include name="**/Release-Temp/**" /> <include name="**/Debug-Temp/**" /> </fileset> </delete> </target> <!-- make file version information --> <target name="make_file_version" description="make file build information"> <foreach item="File" property="filename"> <in> <items> <include name="**/*.rc2" /> </items> </in> <do> <loadfile file="${filename}" property="fileContent" verbose="false" /> <regex pattern="(?'major'\d+)*,(?'minor'\d+)*,(?'patch'\d+)*,(?'oldBuild'\d+)" input="${fileContent}" /> <property name="newBuild" value="${int::parse(oldBuild)+1}" /> <copy file="${filename}" tofile="${filename}.tmp" overwrite="true" > <filterchain> <replacestring from="${major},${minor},${patch},${oldBuild}" to="${major},${minor},${patch},${newBuild}" ignorecase="true" /> <replacestring from="${major}, ${minor}, ${patch}, ${oldBuild}" to="${major}, ${minor}, ${patch}, ${newBuild}" ignorecase="true" /> </filterchain> </copy> <delete file="${filename}" /> <move file="${filename}.tmp" tofile="${filename}" /> </do> </foreach> </target> <!-- make product version information --> <target name="make_product_version" description="make product build information"> <foreach item="File" property="filename"> <in> <items> <include name="**/product_version.h" /> </items> </in> <do> <loadfile file="${filename}" property="fileContent" verbose="false" /> <regex pattern="(?'major'\d+)*,(?'minor'\d+)*,(?'patch'\d+)*,(?'oldBuild'\d+)" input="${fileContent}" /> <copy file="${filename}" tofile="${filename}.tmp" overwrite="true" > <filterchain> <replacestring from="${major},${minor},${patch},${oldBuild}" to="${product_major},${product_minor},${product_patch},${product_build}" ignorecase="true" /> <replacestring from="${major}, ${minor}, ${patch}, ${oldBuild}" to="${product_major}, ${product_minor}, ${product_patch}, ${product_build}" ignorecase="true" /> </filterchain> </copy> <delete file="${filename}" /> <move file="${filename}.tmp" tofile="${filename}" /> </do> </foreach> </target> <!-- build --> <target name="build" description="compiles the source code"> <mkdir dir="bin" /> <mkdir dir="bin/tools" /> <mkdir dir="src/lib" /> <!-- <call target="make_file_version" /> --> <if test="${server_build!='no'}"> <call target="make_product_version" /> </if> <call target="build_core" /> <call target="build_app" /> <call target="build_tool" /> <if test="${server_build=='Daily' or server_build=='Release'}"> <call target="tagging" /> <call target="symbol" /> </if> </target> <!-- build : core libraries --> <target name="build_core" description="compiles the core libraries"> <!-- build : VC6 --> <foreach item="File" property="filename"> <in> <items> <include name="**/core/**/*.dsp" /> </items> </in> <do> <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" /> <exec program="${vc6_path}\Bin\msdev.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /MAKE "${name} - ${vc6_build}" /REBUILD' /> </exec> </do> </foreach> <!-- build : VC10 --> <foreach item="File" property="filename"> <in> <items> <include name="**/core/**/*.sln" /> </items> </in> <do> <exec program="${vcnet_path}\..\ide\devenv.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /rebuild "${build}"' /> </exec> </do> </foreach> </target> <!-- build : application programs --> <target name="build_app" description="compiles the application programs"> <!-- build : VC6 --> <foreach item="File" property="filename"> <in> <items> <include name="**/app/**/*.dsp" /> </items> </in> <do> <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" /> <exec program="${vc6_path}\Bin\msdev.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /MAKE "${name} - ${vc6_build}" /REBUILD' /> </exec> </do> </foreach> <!-- build : VC10 --> <foreach item="File" property="filename"> <in> <items> <include name="**/app/**/*.sln" /> </items> </in> <do> <exec program="${vcnet_path}\..\ide\devenv.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /rebuild "${build}"' /> </exec> </do> </foreach> </target> <!-- build : tool programs --> <target name="build_tool" description="compiles the tool programs"> <!-- build : VC6 --> <foreach item="File" property="filename"> <in> <items> <include name="**/tools/**/*.dsp" /> </items> </in> <do> <regex pattern="^(?'path'.*(\\|/)|(/|\\))(?'name'.*)\.(?'extension'\w+)$" input="${filename}" /> <exec program="${vc6_path}\Bin\msdev.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /MAKE "${name} - ${vc6_build}" /REBUILD' /> </exec> </do> </foreach> <!-- build : VC10 --> <foreach item="File" property="filename"> <in> <items> <include name="**/tools/**/*.sln" /> </items> </in> <do> <exec program="${vcnet_path}\..\ide\devenv.com"> <!-- ${filename} 내용에 공백 있으므로 따옴표로 묶어야 함 --> <arg line='"${filename}" /rebuild "${build}"' /> </exec> </do> </foreach> </target> <!-- tagging --> <target name="tagging" description="commit modified files, and tagging"> <!-- commmit modified files (version information) --> <exec program="C:\Python26\Scripts\hg.bat"> <arg line='commit -m "${server_build} build(Build No: ${product_build}). Added modified version information files"'/> </exec> <!-- get changeset --> <exec program="C:\Python26\Scripts\hg.bat" output="changeset.txt"> <arg line="id -i" /> </exec> <loadfile file="changeset.txt" property="changeset" /> <delete file="changeset.txt" /> <!-- tagging --> <exec program="C:\Python26\Scripts\hg.bat"> <arg line='tag -m "${server_build} build(Build No: ${product_build}). Added tag ${product_major}.${product_minor}.${product_patch}.${product_build} for changeset ${string::trim(changeset)}" ${product_major}.${product_minor}.${product_patch}.${product_build}'/> </exec> <!-- push --> <exec program="C:\Python26\Scripts\hg.bat"> <arg line="push"/> </exec> </target> <!-- symbol indexing --> <target name="symbol" description="symbol indexing, and storing"> <foreach item="Folder" property="foldername"> <in> <items> <include name="**/src/**/${build}" /> </items> </in> <do> <exec program="C:\Program Files\Debugging Tools for Windows (x64)\srcsrv\hgindex.cmd"> <arg line='-source="${project::get-base-directory()}" -symbols="${foldername}" -http /debug' /> </exec> <exec program="C:\Program Files\Debugging Tools for Windows (x64)\symstore.exe"> <arg line='add /o /r /f "${foldername}" /s d:\symbols /t "${project::get-name()}" /compress' /> </exec> </do> </foreach> </target> </project> |
심볼 정보는 웹으로 접근(주소는 http://plab.net/symbols
)하도록 이전 가상 호스트 설정에 다음 내용을 추가한다. LDAP 인증 후 받을 수 있다. 만약 인증을 하지 않고 누구나 접근하게 하려면 <Location></Location>
내용을 지우면 된다. 설정 후에는 웹 서버를 다시 시작해야 적용된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<VirtualHost *:80> ... # Symbols Server Alias /symbols "D:/symbols" <Directory "D:/symbols"> Options Indexes Allow from all AllowOverride None </Directory> <Location "/symbols"> AuthName "PLAB Hg Repositories" AuthType Basic AuthBasicProvider ldap AuthzLDAPAuthoritative off AuthLDAPURL "ldap://localhost/ou=People,dc=plab,dc=net?uid?sub?(objectClass=*)" Require valid-user </Location> </VirtualHost> |
빌드 설정에서 Repository URL을 다음과 같이 웹으로 접근하도록 바꾼다. 그렇지 않으면 심볼 정보를 인덱싱하는 스크립트와 맞지 않아 빌드가 되지 않는다.
심볼 서버를 사용하려면 웹으로 소스를 받아 빌드해야 하므로 Mercurial 인증 정보 저장 팁을 참고해 Mercurial 설정 파일에 인증 정보를 추가한다. 그렇지 않으면 인증을 통과하지 못해 빌드를 진행하지 못한다.
빌드 후 PDB 파일 정보를 확인해 보면 다음과 같이 웹 서버 주소가 기록된 것을 확인할 수 있다.
메모리 덤프 파일을 더블 클릭하거나 Visual Studio 2010에서 직접 열면 다음처럼 덤프 정보에 대한 간략한 내용을 볼 수 있다. 디버그를 시작하려면 화면 오른쪽 위에 있는 Actions 중 ‘Debug with Native Only’를 선택하면 된다.
먼저 인증 정보를 넣어 줘야 한다.
인증을 마친 후에는 심볼과 필요한 소스 파일을 모두 서버에서 가져 온 후 오류 창과 문제가 있는 소스 위치를 표시한다.
참고로 심볼 서버를 인덱싱하는 빌드 스크립트에서 -http 옵션을 사용하지 않으면 다음과 같은 문제가 있다. 이 상태에서 빌드 후 PDB 파일 정보를 확인해 보면 소스 파일 경로가 서버 로컬 경로가 된다.
이런 경우 자신의 PC에서 해당 소스를 제대로 찾지 못하는 문제가 있으며 빌드할 당시와 동일한 소스를 직접 찾아 지정해야 하므로 매우 불편하다. 메모리 덤프 파일로 디버깅을 시작하면 아래와 같은 경고 창이 뜨는데 Run을 선택하면 된다.
서버 로컬 경로로 지정되어 있어 소스를 찾지 못하므로 직접 찾아 지정해야 한다.
하지만 빌드할 때와 동일한 소스가 아니면 다음과 같은 경고 창이 뜬다.
이 상태에서 디버깅을 하는 건 그리 도움도 되지 않을 뿐더러 일일이 빌드 당시 소스를 찾아야 하는 것 역시 매우 불편하다.
마지막으로 팁 하나.
위 내용에 따라 진행하면 Visual C++ 6으로 만든 pdb 파일에 정보를 기록할 때 mspdb60.dll 파일을 로드하지 못해 실패한다. 이 문제는 위에서 설치한 디버깅 툴에 있는 pdbsrc.exe가 x64용이라 해당 DLL을 로드하지 못하기 때문이므로 x86용 pdbsrc.exe 파일을 사용하면 된다. x64와 x86용 프로그램을 모두 사용해야 하므로 x86용 pdbsrc.exe 파일은 pdbsrc_x86.exe로 이름을 바꿔 C:\Program Files\Debugging Tools for Windows (x64)\srcsrv
디렉터리에 넣고 같은 디렉터리에 있는 ssindex.cmd 파일에서 error writing
이라는 내용을 찾아 다음처럼 고친다.
1 2 3 4 5 6 7 8 9 10 11 12 |
my $result = `pdbstr -w -s:srcsrv -p:\"$pdb\" -i:\"$filename\"`; chomp($result); if ( $result =~ /error/i ) { $result = `pdbstr_x86 -w -s:srcsrv -p:\"$pdb\" -i:\"$filename\"`; chomp($result); } if ( $result =~ /error/i ) { warn_message("... error writing $filename to $pdb ($result)"); } else { info_message("... wrote $filename to $pdb ..."); } unlink("$filename"); |