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를 사용하나 빌드할 때 심볼 정보 처리에만 사용할 것이므로 x86 x64 버전 이미지를 받아 설치했다. 32비트와 64비트 패키지 선택은 이 내용을 참조한다.
  • ActivePerl: 여기에서 5.14.2.1402 버전을 받을 수 있다.
  • Mercurial source server indexing module

2. 설치

먼저 Windows SDK 설치를 시작한다. Next 버턴을 누른다.

Windows SDK Setup - Welcome

라이선스 동의를 하고 Next 버턴을 누른다.

Windows SDK Setup - License

Windows SDK는 Visual Studio 2005 이상이 필요한데 Visual Studio가 이미 설치되어 있을 경우 적절한 버전인지 먼저 확인한다. 없으면 표시하지 않는다.

Windows SDK Setup - Uninstall Options

설치 위치를 지정하는데 기본값 그대로 사용하면 된다. Next 버턴을 선택한다.

Windows SDK Setup - Install Location

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 버턴을 누른다.

Windows SDK Setup - Installation Options

Next를 선택하면 설치를 시작한다.

Windows SDK Setup - Begin Installation

설치 진행화면이 나타난다.

Windows SDK Setup - Installation Progress

설치를 마치면 Finish를 선택해 완료한다.

Windows SDK Setup - Installation Complete

설치를 마친 후에 는 C:\Program Files\Debugging Tools for Windows (x64);C:\Program Files\Debugging Tools for Windows (x64)\srcsrv 디렉터리를 환경 변수 중 path 정보에 추가한다.

이번에는 ActivePerl을 설치한다. 설치 파일을 실행하고 Next를 선택한다.

ActivePerl Setup - Welcome

라이선스에 동의하고 Next 버턴을 누른다.

ActivePerl Setup - License

설치 경로와 설치 내용은 기본 설정으로 하고 Next를 선택한다.

ActivePerl Setup - Select Options

설치 경로에 추가하고 Perl 파일을 ActivePerl에 연결하도록 한 후 Next를 선택한다.

ActivePerl Setup - Select Options

설치 준비가 됐으면 Install을 선택해 설치한다.

ActivePerl Setup - Ready to Install

ActivePerl Setup - Installation Progress

설치를 마치면 Finish 버턴을 선택해 완료한다.

ActivePerl Setup - Installation Complete

설치를 마친 후에는 재부팅한다. 그렇지 않으면 Jenkins에서 Perl 실행 경로는 인식하지 못해 다음과 같은 내용과 함께 빌드를 실패할 수 있다.

[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)가 된다. 그런 다음 심볼 정보를 인덱싱하도록 스크립트를 수정한다.

주요 변경 내용은 다음과 같다.

  <!-- 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 파일을 출력하는 디렉터리 지정, 타겟 호출 등이 바뀌었다.

<?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> 내용을 지우면 된다. 설정 후에는 웹 서버를 다시 시작해야 적용된다.

<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을 다음과 같이 웹으로 접근하도록 바꾼다. 그렇지 않으면 심볼 정보를 인덱싱하는 스크립트와 맞지 않아 빌드가 되지 않는다.

Repository URL for Symbol Server

심볼 서버를 사용하려면 웹으로 소스를 받아 빌드해야 하므로 Mercurial 인증 정보 저장 팁을 참고해 Mercurial 설정 파일에 인증 정보를 추가한다. 그렇지 않으면 인증을 통과하지 못해 빌드를 진행하지 못한다.

빌드 후 PDB 파일 정보를 확인해 보면 다음과 같이 웹 서버 주소가 기록된 것을 확인할 수 있다.

srctool - HTTP URL Information

메모리 덤프 파일을 더블 클릭하거나 Visual Studio 2010에서 직접 열면 다음처럼 덤프 정보에 대한 간략한 내용을 볼 수 있다. 디버그를 시작하려면 화면 오른쪽 위에 있는 Actions 중 ‘Debug with Native Only’를 선택하면 된다.

Dump Summary

먼저 인증 정보를 넣어 줘야 한다.

Symbol Server Authentication

인증을 마친 후에는 심볼과 필요한 소스 파일을 모두 서버에서 가져 온 후 오류 창과 문제가 있는 소스 위치를 표시한다.

Error Window

Error Source Code

참고로 심볼 서버를 인덱싱하는 빌드 스크립트에서 -http 옵션을 사용하지 않으면 다음과 같은 문제가 있다. 이 상태에서 빌드 후 PDB 파일 정보를 확인해 보면 소스 파일 경로가 서버 로컬 경로가 된다.

srctool - Local Path Information

이런 경우 자신의 PC에서 해당 소스를 제대로 찾지 못하는 문제가 있으며 빌드할 당시와 동일한 소스를 직접 찾아 지정해야 하므로 매우 불편하다. 메모리 덤프 파일로 디버깅을 시작하면 아래와 같은 경고 창이 뜨는데 Run을 선택하면 된다.

Source Server Security Warning - Local Path

서버 로컬 경로로 지정되어 있어 소스를 찾지 못하므로 직접 찾아 지정해야 한다.

Find Source

하지만 빌드할 때와 동일한 소스가 아니면 다음과 같은 경고 창이 뜬다.

Different Souce File Warning

이 상태에서 디버깅을 하는 건 그리 도움도 되지 않을 뿐더러 일일이 빌드 당시 소스를 찾아야 하는 것 역시 매우 불편하다.

마지막으로 팁 하나.

위 내용에 따라 진행하면 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이라는 내용을 찾아 다음처럼 고친다.

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");