특징

- 업그레이드 및 설치 시, 기존 서비스는 중지한 후 설치함

- 언인스톨러는 설정 > 앱 > 설치된 앱에서 확인 가능

- 언인스톨 시, 기존 서비스를 중지하고 제거함

- GUID가 같으면 설치되지 않음

 

테스트 서비스 프로그램 만들기

1. Visual Studio 2022 → 새 프로젝트 만들기 → 작업자 서비스 선택하여 프로젝트 생성

2. NuGet 패키지 관리자로 아래 두 개 설치

3. Program.cs 를 아래와 같이 코딩 후 빌드

using WorkerService1;
using Microsoft.Extensions.Hosting.WindowsServices;

var builder = Host.CreateApplicationBuilder(args);
// Windows 서비스로 동작하도록
builder.Services.AddWindowsService(o =>
{
    o.ServiceName = "My Worker Service";
});
builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();

4. 단일 배포 파일 생성

dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:SelfContained=true

 

WiX v4 설치 후 구성

다운로드 및 설치 : https://marketplace.visualstudio.com/items?itemName=FireGiant.FireGiantHeatWaveDev17

1. Visual Studio 2022 → 새 프로젝트 만들기 → MSI Package 선택하여 프로젝트 생성

2. 코드 수정 및 빌드

// ExampleComponents.wxs
<!-- ExampleComponents.wxs -->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
     xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
	<Fragment>
		<ComponentGroup Id="ExampleComponents" Directory="INSTALLFOLDER">
			<Component>
				<File Id="Service1"
					  Source="C:\Users\codetronik\source\repos\WorkerService1\WorkerService1\bin\Release\net8.0\win-x64\publish\WorkerService1.exe"
					  KeyPath="yes" />

				<!-- 서비스 등록 -->
				<ServiceInstall
					Name="MyService1"
					DisplayName="My Service"
					Description="Service example"
					Type="ownProcess"
					Start="auto"
					Account="LocalSystem"
					ErrorControl="normal"/>

				<!-- 설치/업그레이드/제거 시 제어 -->
				<ServiceControl
					Name="MyService1"
					Stop="both"
					Start="install"
					Remove="uninstall"
					Wait="yes"/>

			</Component>
		</ComponentGroup>
	</Fragment>
</Wix>

// Folder.wxs
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
	<Fragment>
		<StandardDirectory Id="ProgramFiles64Folder">
			<Directory Id="INSTALLFOLDER"
					   Name="!(bind.Property.Manufacturer) !(bind.Property.ProductName)" />
		</StandardDirectory>
	</Fragment>
</Wix>

// Package.en-us.wxl
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">

  <String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />

</WixLocalization>

// Package.wxs
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
	<Package Id="MyServicePkg"
			 Name="ServiceExample"
			 Manufacturer="Codetronik"
			 Version="1.0.0.0"
			 UpgradeCode="{1B29FC40-CA47-1067-131D-00DD010662DA}">
		<MajorUpgrade Schedule="afterInstallValidate"
					  DowngradeErrorMessage="!(loc.DowngradeError)" />
		<Feature Id="Main">
			<ComponentGroupRef Id="ExampleComponents" />
		</Feature>
	</Package>
</Wix>

 

 

'Windows > Dev' 카테고리의 다른 글

[poco] 로그 예제  (0) 2025.07.22
[C++20] std::span  (0) 2025.05.25
[Visual Studio] NDK 툴체인 업그레이드 하는 방법  (0) 2024.11.09
windows + mac 동시 개발 시 주의 사항  (0) 2024.10.15
[C++] 함수 const 선언 정리  (0) 2024.09.12

환경 : Ubuntu 24.04.3 / 램 16G

SaaS를 사용 못하는 내부망에서 사용하기 위함

2025.08.12 기준

1. 도커 설치 (https://docs.docker.com/engine/install/ubuntu/ 참조)

$ wget https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/containerd.io_1.7.27-1_amd64.deb && \
wget https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/docker-ce-cli_28.3.3-1~ubuntu.24.04~noble_amd64.deb && \
wget https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/docker-ce_28.3.3-1~ubuntu.24.04~noble_amd64.deb && \
wget https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/docker-buildx-plugin_0.26.1-1~ubuntu.24.04~noble_amd64.deb && \
wget https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/docker-compose-plugin_2.39.1-1~ubuntu.24.04~noble_amd64.deb
$ sudo dpkg -i ./containerd.io_1.7.27-1_amd64.deb ./docker-ce-cli_28.3.3-1~ubuntu.24.04~noble_amd64.deb ./docker-ce_28.3.3-1~ubuntu.24.04~noble_amd64.deb ./docker-buildx-plugin_0.26.1-1~ubuntu.24.04~noble_amd64.deb ./docker-compose-plugin_2.39.1-1~ubuntu.24.04~noble_amd64.deb
$ sudo service docker start & sudo docker run hello-world

2. 리버스 프록시 설치

$ sudo apt-get install nginx
# 인증서 만들기
$ IP=192.168.130.189
$ sudo openssl req -x509 -newkey rsa:2048 -days 825 -nodes -subj "/CN=${IP}" -addext "subjectAltName = IP:${IP}" -keyout /etc/ssl/private/sentry-ip.key -out /etc/ssl/certs/sentry-ip.crt
$ sudo vim /etc/nginx/sites-available/sentry.conf
# 80 -> 443 리다이렉트
server {
  listen 80;
  return 301 https://$host$request_uri;
}

# HTTPS 종단 (IP 기반, 기본 서버)
server {
  listen 443 ssl http2;

  ssl_certificate     /etc/ssl/certs/sentry-ip.crt;
  ssl_certificate_key /etc/ssl/private/sentry-ip.key;

  client_max_body_size 50m;

  location / {
    proxy_set_header Host               $host;
    proxy_set_header X-Forwarded-Host   $host;
    proxy_set_header X-Forwarded-Proto  https;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_pass http://127.0.0.1:9000;
  }
}
$ sudo ln -s /etc/nginx/sites-available/sentry.conf /etc/nginx/sites-enabled/sentry.conf
$ sudo nginx -t
$ sudo systemctl reload nginx

3. Self-Hosted Sentry 설치 (https://develop.sentry.dev/self-hosted/ 참조)

$ sudo apt-get install git curl
$ VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/getsentry/self-hosted/releases/latest)
$ VERSION=${VERSION##*/}
$ git clone https://github.com/getsentry/self-hosted.git
4 cd self-hosted
$ git checkout ${VERSION}
$ sudo ./install.sh

$ vim sentry/config.yml
system.url-prefix: "https://192.168.130.189"
$ vim sentry/sentry.conf.py
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SOCIAL_AUTH_REDIRECT_IS_HTTPS = True
CSRF_TRUSTED_ORIGINS = ["https://192.168.130.189"]
$ sudo sed -i -E 's|proxy_set_header[[:space:]]+X-Forwarded-Proto[[:space:]]+\$scheme;|proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;|g' nginx.conf

$ sudo docker compose up --wait

4. curl  + sentry 에러 전송 예제

라이브러리 설치

$ sudo apt install -y libcurl4-openssl-dev
$ git clone --recursive https://github.com/getsentry/sentry-native.git
$ cd sentry-native
$ cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo   # 백엔드는 기본값 사용
$ cmake --build build --parallel
$ sudo cmake --install build

CMakeLists.txt 생성

cmake_minimum_required(VERSION 3.20)
project(app CXX)

find_package(sentry CONFIG REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE sentry::sentry)
#include <sentry.h>
#include <cstdio>
#include <stdexcept>

int main() {
    const char* DSN = "https://님_공개키@192.168.130.189/2";

    sentry_options_t* options = sentry_options_new();
    sentry_options_set_debug(options, 1);
    sentry_options_set_logger(options,
        [](sentry_level_e lvl, const char* msg, void*) {
            std::fprintf(stderr, "[sentry:%d] %s\n", (int)lvl, msg);
        }, nullptr);

  
    sentry_options_set_dsn(options, DSN);
    sentry_options_set_database_path(options, ".sentry-native");
    sentry_options_set_ca_certs(options, "아까만든인증서.crt");
    sentry_options_set_release(options, "myproject@1.0.0");    
    sentry_options_set_handler_path(options, "/usr/local/bin/crashpad_handler");

    // 원한다면 환경/태그
    sentry_options_set_environment(options, "dev");
    
    if (sentry_init(options) != 0) {
        throw std::runtime_error("sentry_init failed");
    }

    // 테스트 이벤트
    std::array<void*, 256> frames{};
    int n = backtrace(frames.data(), frames.size());

    sentry_value_t st  = sentry_value_new_stacktrace(frames.data(), n);
    sentry_value_t exc = sentry_value_new_exception("error", std::string(msg).c_str());
    sentry_value_set_by_key(exc, "stacktrace", st);

    sentry_value_t evt = sentry_value_new_event();
    sentry_event_add_exception(evt, exc);
    sentry_capture_event(evt);

    // 크래시
    int* p = nullptr; *p = 42;

    sentry_close();
}

4. sentry-cli 설치 및 환경 설정

https://192.168.130.189/settings/account/api/auth-tokens/new-token/ 에서 토큰 생성

$ curl -sL https://sentry.io/get-cli/ | bash
# 환경 파일 설정
$ vim ~/.sentryclirc
[defaults]
url = https://192.168.130.189
org = sentry # 조직 slug
project = agent # 프로젝트 slug

[http]
verify_ssl = false

[auth]
token = 님_토큰

# 정상 동작 확인
$ sentry-cli info
# 조직 slug 확인
$ sentry-cli organizations list
+----+--------+--------+--------------+---------------+--------------+
| ID | Name   | Slug   | Date Created | Early Adopter | Requires 2FA |
+----+--------+--------+--------------+---------------+--------------+
| 1  | Sentry | sentry | 2025-08-12   | false         | false        |
+----+--------+--------+--------------+---------------+--------------+
# 프로젝트 slug 확인
$ sentry-cli projects list -o sentry
+----+----------+--------+----------+
| ID | Slug     | Team   | Name     |
+----+----------+--------+----------+
| 1  | internal | Sentry | Internal |
| 2  | agent    | Sentry | agent    |
+----+----------+--------+----------+

# 파일 유효성 확인
$ sentry-cli debug-files check 님_실행파일
# 분리 심볼 생성
objcopy --only-keep-debug 님_실행파일 님_실행파일.debug
objcopy --add-gnu-debuglink=님_실행파일.debug 님_실행파일
# 파일 업로드
$ sentry-cli debug-files upload --include-sources 님_실행파일 님_실행파일.debug

5. 크래시패드 정상 전송을 위해 프로그램을 설치한 pc에 인증서 설치

# sentry 서버에서 인증서 가져와서 루트 인증서에 설정
$ sudo cp sentry-ip.crt /usr/local/share/ca-certificates/sentry-ip.crt
$ sudo update-ca-certificates
#include <iostream>
#include <Poco/Logger.h>
#include <Poco/PatternFormatter.h>
#include <Poco/FormattingChannel.h>
#include <Poco/ConsoleChannel.h>
#include <Poco/FileChannel.h>
#include <Poco/SplitterChannel.h>
#include <Poco/AutoPtr.h>
#include <Poco/Thread.h>
#include <Poco/Runnable.h>

using namespace Poco;

class LevelFilterFileChannel : public FileChannel
{
public:
    LevelFilterFileChannel() : FileChannel() {}

    void log(const Message& msg) override
    {
        // debug, trace 로그 제외
        if (msg.getPriority() <= Message::PRIO_INFORMATION)
        {
            FileChannel::log(msg);
        }
    }
};

class MyWorker : public Runnable
{
public:
    void run() override
    {
        Logger& logger = Logger::get("aLogger");
        for (int i = 0; i < 5; ++i)
        {
            logger.notice("Poco Thread is running! i = " + std::to_string(i));
            Poco::Thread::sleep(500); // 0.5초 대기
        }
        logger.information("Poco Thread done.");
    }
};

int main()
{
    AutoPtr<ColorConsoleChannel> colorConsoleChannel(new ColorConsoleChannel);
    AutoPtr<LevelFilterFileChannel> fileChannel(new LevelFilterFileChannel);
    fileChannel->setProperty("path", "myapp.log");
    fileChannel->setProperty("rotation", "50000 K"); // 50,000KB 초과시 회전
    fileChannel->setProperty("archive", "timestamp");
    fileChannel->setProperty("purgeAge", "30 days"); // 30일 초과 로그 파일 삭제

    AutoPtr<PatternFormatter> patternFormatter(new PatternFormatter);
    patternFormatter->setProperty(
        "pattern",
        "[%Y-%m-%d %H:%M:%S] [%p] [PID:%P][TID:%T][TH:%I] %t"
    );
    patternFormatter->setProperty("times", "local");
   
    AutoPtr<FormattingChannel> fcConsole(new FormattingChannel(patternFormatter, colorConsoleChannel));  // 콘솔은 모든 로그
    AutoPtr<FormattingChannel> fcFile(new FormattingChannel(patternFormatter, fileChannel));

    // 콘솔 및 파일 채널로 메시지를 전달
    AutoPtr<SplitterChannel> splitter(new SplitterChannel);
    splitter->addChannel(fcConsole);
    splitter->addChannel(fcFile);  

    Logger& logger = Logger::get("Logger");
    logger.setChannel(splitter);
    logger.setLevel(Message::PRIO_TRACE); // 모든 로그 레벨 허용

    logger.fatal("FATAL 메시지");
    logger.critical("CRITICAL 메시지");
    logger.error("ERROR 메시지");
    logger.warning("WARNING 메시지");
    logger.notice("NOTICE 메시지");
    logger.information("INFO 메시지");
    logger.debug("DEBUG 메시지 - 콘솔만");
    logger.trace("TRACE 메시지 - 콘솔만");

    MyWorker worker;
    Thread thread;
    thread.start(worker);

    // 메인 스레드도 로그 남기기
    for (int i = 0; i < 3; ++i)
    {
        logger.notice("Main thread working... i = " + std::to_string(i));
        std::this_thread::sleep_for(std::chrono::milliseconds(400));
    }

    thread.join();
    logger.information("모든 작업 종료");
}

+ Recent posts