반디집 광고 팝업 자동 종료 프로그램 만들기
서론 – 왜 이런 걸 만들었는가?
회사에서 파일 압축/해제를 할 때 나는 항상 반디집을 사용한다.
윈도우 기본 압축 기능도 있지만, 치명적인 문제가 하나 있다.
압축을 해제하면 그냥 현재 폴더에 전부 풀어버린다.
예를 들어 다운로드 폴더 안에 압축을 풀면 프로그램 설정 파일, 데이터 파일, 실행 파일이 그냥 난장판으로 흩어진다.
그걸 하나씩 찾으라고? 이건 거의 드래곤볼 모으기급이다.
반디집은 기본값으로:
“대상 폴더 하위에 ‘압축파일명’ 폴더 생성 후 압축 풀기”
이 옵션이 켜져 있어서 굉장히 편하다.
문제점 – 업데이트 알림/광고 팝업
문제는 이것이다.
- 가끔 압축/해제 작업 후
- 반박자 느리게
- 개별 프로그램 형태로
- 업데이트 알림 창이 뜬다
광고는 아니라고 하지만… 솔직히 사용자 입장에선 광고랑 다를 게 없다.
더 웃긴 건:
- 업데이트 알림 설정 변경 → 유료 플랜 결제 요구
- pre-release 버전까지 업데이트하라고 권장
- 광고 제거는 유료 플랜
크랙판? 보안 리스크 + 업데이트 불가 = 시한폭탄
그래서 결론:
그냥 뜨면 자동으로 꺼버리면 되지 않나?
설계 방향
광고 창은 별도 프로세스였다
반디집 광고/업데이트 창은 본 프로그램과 다른 프로세스로 실행된다.
아이콘도 다름 프로세스도 분리됨
→ 창 인식이 쉬움 → 자동 종료 가능성 ↑
서비스 vs 트레이 앱
Windows 서비스 방식
- Session 0에서 실행
- 사용자 데스크톱(Session 1)과 분리
- UI 창 제어 어려움
- 결국 “서비스 + 사용자 에이전트” 이중 구조 필요
너무 복잡하다.
트레이 백그라운드 프로그램 방식
- NotifyIcon 기반
- 사용자 세션에서 직접 실행
- Win32 API로 창 제어 가능
- 구현 난이도 적절
→ 이걸로 결정.
프로젝트 구조
프로그램 이름은 GPT 추천 중 하나인:
Nope.exe
(광고 창: “업데이트 하시겠습니까?” Nope.exe: “아니.”)
개발 과정
프로젝트 스캐폴딩
- .NET 8
- net8.0-windows
- WinForms
- WinExe
왜 WinForms?
- NotifyIcon
- Win32 API 호출
- 트레이 앱 구현
에 적합하기 때문.
실행 진입점 구성
Program.cs에서 CLI 플래그 분기 처리:
--install-startup--uninstall-startup--headless--console
기본은 트레이 모드 실행.
[STAThread]
static void Main(string[] args)
WinForms 안정성을 위해 STA 유지.
설정 모델 구성
AppConfig
public sealed class AppConfig
{
public int PollIntervalMs { get; set; } = 750;
public int CooldownSeconds { get; set; } = 10;
public string LogFilePath { get; set; } = Path.Combine("logs", "app.log");
public long LogMaxBytes { get; set; } = 5 * 1024 * 1024;
public int LogMaxFiles { get; set; } = 5;
public List<RuleConfig> Rules { get; set; } = new();
}
전체 앱 설정
RuleConfig
public sealed class RuleConfig
{
public string Name { get; set; } = "Unnamed rule";
public bool Enabled { get; set; } = true;
public MatchConfig Match { get; set; } = new();
public ActionConfig Action { get; set; } = new();
}
개별 규칙
현재 단순화된 매칭 조건:
- processNameExact
- processPathExact
public sealed class MatchConfig
{
public string? ProcessNameExact { get; set; }
public string? ProcessPathExact { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum RuleActionType
{
Close,
Minimize,
Hide,
KillProcess,
}
Action enum:
- Close
- Minimize
- Hide
- KillProcess
public sealed class ActionConfig
{
public RuleActionType Type { get; set; } = RuleActionType.Close;
public uint SendMessageTimeoutMs { get; set; } = 1500;
}
설정 파일 관리
rules.json 자동 생성
- 기본 규칙은 빈 배열
- 과거 예시 규칙 자동 정리 로직 포함
- 저장 즉시 반영
var rulesPath = Path.Combine(AppContext.BaseDirectory, "rules.json");
var bootstrapLogger = new CompositeLogger(new ConsoleLogSink());
var configService = new ConfigService(rulesPath);
var config = configService.LoadOrCreateDefault(bootstrapLogger);
public AppConfig LoadOrCreateDefault(ILogger logger)
{
if (!File.Exists(_path))
{
var defaultConfig = DefaultConfigFactory.Create();
Save(defaultConfig);
logger.Info($"Created default rules file at '{_path}'.");
return defaultConfig;
}
var json = File.ReadAllText(_path);
var config = JsonSerializer.Deserialize<AppConfig>(json, _jsonOptions) ?? new AppConfig();
var originalCount = config.Rules.Count;
config.Rules = config.Rules
.Where(r => !IsLegacyExampleRule(r))
.ToList();
if (config.Rules.Count != originalCount)
{
logger.Info("Removed legacy bundled example rules from configuration.");
Save(config);
}
return config;
}
Win32 창 수집
사용 API:
- EnumWindows
- GetWindowText
- GetClassName
- GetWindowThreadProcessId
- SendMessageTimeout
- ShowWindow
주기적으로 최상위 창 열거 → PID/프로세스 정보 수집.
룰 엔진
RuleEngine
- enabled 규칙만 순회
- AND 매칭
- 쿨다운 적용
- 동일 창 재처리 방지
public sealed class RuleEngine
{
private readonly AppConfig _config;
private readonly ILogger _logger;
private readonly Dictionary<string, DateTimeOffset> _cooldowns = new();
public RuleEngine(AppConfig config, ILogger logger)
{
_config = config;
_logger = logger;
}
public void EvaluateAndAct(WindowSnapshot window)
{
foreach (var rule in _config.Rules.Where(r => r.Enabled))
{
if (!IsMatch(rule, window))
{
continue;
}
var key = $"{rule.Name}:{window.Handle}:{window.ProcessId}";
if (_cooldowns.TryGetValue(key, out var nextAllowed) && nextAllowed > DateTimeOffset.Now)
{
continue;
}
var actionResult = ExecuteAction(rule, window);
_cooldowns[key] = DateTimeOffset.Now.AddSeconds(_config.CooldownSeconds);
_logger.Info($"Rule '{rule.Name}' matched hwnd=0x{window.Handle:X}, pid={window.ProcessId}, title='{window.WindowTitle}', action={rule.Action.Type}, success={actionResult}");
}
}
private bool IsMatch(RuleConfig rule, WindowSnapshot window)
{
var m = rule.Match;
if (!IsBlank(m.ProcessNameExact) && !EqualsIgnoreCase(window.ProcessName, m.ProcessNameExact))
return false;
if (!IsBlank(m.ProcessPathExact) && !EqualsIgnoreCase(window.ProcessPath, m.ProcessPathExact))
return false;
return true;
}
private bool ExecuteAction(RuleConfig rule, WindowSnapshot window)
{
return rule.Action.Type switch
{
RuleActionType.Close => CloseWindow(window.Handle, rule.Action.SendMessageTimeoutMs),
RuleActionType.Minimize => Win32.ShowWindow(window.Handle, Win32.SwMinimize),
RuleActionType.Hide => Win32.ShowWindow(window.Handle, Win32.SwHide),
RuleActionType.KillProcess => KillProcess(window.ProcessId),
_ => false,
};
}
private static bool CloseWindow(nint hWnd, uint timeoutMs)
{
var result = Win32.SendMessageTimeout(hWnd, Win32.WmClose, nint.Zero, nint.Zero, Win32.SmtoAbortIfHung, timeoutMs, out _);
return result != nint.Zero;
}
private bool KillProcess(uint pid)
{
try
{
using var process = Process.GetProcessById((int)pid);
process.Kill(true);
return true;
}
catch (Exception ex)
{
_logger.Warn($"Failed to kill process {pid}: {ex.Message}");
return false;
}
}
private static bool IsBlank(string? value) => string.IsNullOrWhiteSpace(value);
private static bool EqualsIgnoreCase(string a, string? b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
}
public sealed class MonitorService
{
private readonly WindowEnumerator _enumerator;
private readonly RuleEngine _engine;
private readonly AppConfig _config;
private readonly ILogger _logger;
public bool IsPaused { get; private set; }
public MonitorService(WindowEnumerator enumerator, RuleEngine engine, AppConfig config, ILogger logger)
{
_enumerator = enumerator;
_engine = engine;
_config = config;
_logger = logger;
}
public void SetPaused(bool paused)
{
IsPaused = paused;
_logger.Info(paused ? "Monitoring paused." : "Monitoring resumed.");
}
public async Task RunAsync(CancellationToken cancellationToken)
{
_logger.Info($"Monitor started. PollIntervalMs={_config.PollIntervalMs}, CooldownSeconds={_config.CooldownSeconds}");
while (!cancellationToken.IsCancellationRequested)
{
if (!IsPaused)
{
var windows = _enumerator.Enumerate(_logger);
foreach (var window in windows)
{
_engine.EvaluateAndAct(window);
}
}
try
{
await Task.Delay(_config.PollIntervalMs, cancellationToken);
}
catch (TaskCanceledException)
{
break;
}
}
_logger.Info("Monitor stopped.");
}
}
액션 실행 방식
SendMessageTimeout(hwnd, WM_CLOSE)
Close는 WM_CLOSE 기반.
모니터 루프
MonitorService:
- poll interval 기반 주기 실행
- 일시정지/재개 가능
public sealed class MonitorService
{
private readonly WindowEnumerator _enumerator;
private readonly RuleEngine _engine;
private readonly AppConfig _config;
private readonly ILogger _logger;
public bool IsPaused { get; private set; }
public MonitorService(WindowEnumerator enumerator, RuleEngine engine, AppConfig config, ILogger logger)
{
_enumerator = enumerator;
_engine = engine;
_config = config;
_logger = logger;
}
public void SetPaused(bool paused)
{
IsPaused = paused;
_logger.Info(paused ? "Monitoring paused." : "Monitoring resumed.");
}
public async Task RunAsync(CancellationToken cancellationToken)
{
_logger.Info($"Monitor started. PollIntervalMs={_config.PollIntervalMs}, CooldownSeconds={_config.CooldownSeconds}");
while (!cancellationToken.IsCancellationRequested)
{
if (!IsPaused)
{
var windows = _enumerator.Enumerate(_logger);
foreach (var window in windows)
{
_engine.EvaluateAndAct(window);
}
}
try
{
await Task.Delay(_config.PollIntervalMs, cancellationToken);
}
catch (TaskCanceledException)
{
break;
}
}
_logger.Info("Monitor stopped.");
}
로깅 시스템
- ILogger 인터페이스
- CompositeLogger
- 콘솔 출력
- 파일 로그 (rolling)
logs/app.log
백그라운드 앱 운영 시 추적 가능.
트레이 UI
NotifyIcon + 컨텍스트 메뉴:
- 일시정지 / 재개
- 로그 폴더 열기
- 설정창 열기
- 종료
설정창은 단일 인스턴스 보장.
public sealed class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _notifyIcon;
private readonly MonitorService _monitor;
private readonly CancellationTokenSource _cts;
private readonly ILogger _logger;
private readonly ConfigService _configService;
private readonly AppConfig _config;
private readonly ToolStripMenuItem _pauseResumeItem;
private SettingsForm? _settingsForm;
public TrayApplicationContext(
MonitorService monitor,
CancellationTokenSource cts,
ILogger logger,
ConfigService configService,
AppConfig config)
{
_monitor = monitor;
_cts = cts;
_logger = logger;
_configService = configService;
_config = config;
_pauseResumeItem = new ToolStripMenuItem("일시정지", null, (_, _) => TogglePause());
var menu = new ContextMenuStrip();
menu.Items.Add(_pauseResumeItem);
menu.Items.Add(new ToolStripMenuItem("로그 보기", null, (_, _) => OpenLogsFolder()));
menu.Items.Add(new ToolStripMenuItem("설정 열기", null, (_, _) => OpenSettingsUi()));
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add(new ToolStripMenuItem("종료", null, (_, _) => Exit()));
_notifyIcon = new NotifyIcon
{
Text = "Nope.exe - Window Auto Closer",
Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location) ?? SystemIcons.Application,
ContextMenuStrip = menu,
Visible = true,
};
_notifyIcon.DoubleClick += (_, _) => TogglePause();
_ = Task.Run(() => _monitor.RunAsync(_cts.Token));
_logger.Info("Tray mode started.");
}
private void TogglePause()
{
var newPausedState = !_monitor.IsPaused;
_monitor.SetPaused(newPausedState);
_pauseResumeItem.Text = newPausedState ? "재개" : "일시정지";
}
private void OpenLogsFolder()
{
var logDir = Path.Combine(AppContext.BaseDirectory, Path.GetDirectoryName(_config.LogFilePath) ?? "logs");
try
{
Directory.CreateDirectory(logDir);
OpenPath(logDir);
}
catch (Exception ex)
{
_logger.Warn($"Failed to open logs folder '{logDir}': {ex.Message}");
}
}
private void OpenSettingsUi()
{
if (_settingsForm is not null && !_settingsForm.IsDisposed)
{
_settingsForm.BringToFront();
_settingsForm.Focus();
return;
}
_settingsForm = new SettingsForm(_config, _configService, _logger);
_settingsForm.FormClosed += (_, _) => _settingsForm = null;
_settingsForm.Show();
}
private void OpenPath(string path)
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = path,
UseShellExecute = true,
});
}
catch (Exception ex)
{
_logger.Warn($"Failed to open '{path}': {ex.Message}");
}
}
private void Exit()
{
_notifyIcon.Visible = false;
_cts.Cancel();
_logger.Info("Tray exit requested.");
ExitThread();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_settingsForm?.Dispose();
_notifyIcon.Dispose();
_cts.Dispose();
}
base.Dispose(disposing);
}
}
설정창
기능:
- pollInterval 수정
- cooldown 수정
- 규칙 추가/삭제
- exe 파일 선택 → 자동 processName/Path 입력
- DataGrid 예외 방지 처리
private readonly AppConfig _config;
private readonly ConfigService _configService;
private readonly ILogger _logger;
private readonly NumericUpDown _pollIntervalInput;
private readonly NumericUpDown _cooldownInput;
private readonly DataGridView _rulesGrid;
private readonly BindingList<RuleRow> _rows;
public SettingsForm(AppConfig config, ConfigService configService, ILogger logger)
{
_config = config;
_configService = configService;
_logger = logger;
Text = "Nope.exe 설정";
Width = 1250;
Height = 760;
StartPosition = FormStartPosition.CenterScreen;
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 4,
Padding = new Padding(10),
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var generalPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
WrapContents = true,
};
var pollLabel = new Label { Text = "PollInterval(ms)", AutoSize = true, Margin = new Padding(0, 8, 5, 0) };
_pollIntervalInput = new NumericUpDown { Minimum = 100, Maximum = 10000, Value = Math.Clamp(_config.PollIntervalMs, 100, 10000), Width = 100 };
var cooldownLabel = new Label { Text = "Cooldown(sec)", AutoSize = true, Margin = new Padding(12, 8, 5, 0) };
_cooldownInput = new NumericUpDown { Minimum = 1, Maximum = 3600, Value = Math.Clamp(_config.CooldownSeconds, 1, 3600), Width = 100 };
generalPanel.Controls.Add(pollLabel);
generalPanel.Controls.Add(_pollIntervalInput);
generalPanel.Controls.Add(cooldownLabel);
generalPanel.Controls.Add(_cooldownInput);
var helpTip = new ToolTip();
helpTip.SetToolTip(pollLabel, "창 목록을 다시 검사하는 주기(밀리초). 낮을수록 반응은 빠르지만 CPU 사용량이 늘어납니다.");
helpTip.SetToolTip(_pollIntervalInput, "권장: 500~1000ms");
helpTip.SetToolTip(cooldownLabel, "같은 창/같은 규칙에 액션을 다시 적용하기 전 대기 시간(초).");
helpTip.SetToolTip(_cooldownInput, "반복 처리 방지용 쿨다운");
var guideLabel = new Label
{
AutoSize = true,
Dock = DockStyle.Fill,
Margin = new Padding(0, 8, 0, 8),
Text =
"빠른 설정: 아래에서 규칙 행 선택 → '프로세스 파일 선택' 버튼 클릭 → exe 선택하면 이름/경로가 자동 입력됩니다.\n" +
"규칙 설명: 모든 match 조건은 AND(모두 만족)로 평가되며, 빈 칸은 무시됩니다.\n" +
"- ProcessNameExact: 프로세스 이름 정확히 일치 (예: notepad)\n" +
"- ProcessPathExact: 실행 파일 전체 경로 정확히 일치\n" +
"- Action: Close(정상 닫기), Minimize(최소화), Hide(숨김), KillProcess(강제 종료)\n" +
"- CloseTimeoutMs: Close 동작 시 WM_CLOSE 응답 대기 시간(ms)",
};
_rows = new BindingList<RuleRow>(_config.Rules.Select(RuleRow.FromRule).ToList());
_rulesGrid = new DataGridView
{
Dock = DockStyle.Fill,
AutoGenerateColumns = false,
DataSource = _rows,
AllowUserToAddRows = true,
AllowUserToDeleteRows = true,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells,
};
_rulesGrid.DataError += OnRulesGridDataError;
_rulesGrid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleRow.Enabled), HeaderText = "Enabled", ToolTipText = "체크 시 이 규칙 사용" });
_rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleRow.Name), HeaderText = "Name", Width = 180, ToolTipText = "규칙 표시 이름" });
_rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleRow.ProcessNameExact), HeaderText = "ProcessNameExact", ToolTipText = "프로세스 이름 정확히 일치" });
_rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleRow.ProcessPathExact), HeaderText = "ProcessPathExact", Width = 240, ToolTipText = "실행 파일 경로 정확히 일치" });
_rulesGrid.Columns.Add(new DataGridViewComboBoxColumn
{
DataPropertyName = nameof(RuleRow.ActionType),
HeaderText = "Action",
DataSource = Enum.GetValues<RuleActionType>(),
ToolTipText = "매칭 시 실행할 동작",
});
_rulesGrid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleRow.SendMessageTimeoutMs), HeaderText = "CloseTimeoutMs", ToolTipText = "Close 액션의 응답 대기 시간(ms)" });
var buttonPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft,
AutoSize = true,
};
var saveButton = new Button { Text = "저장", AutoSize = true };
saveButton.Click += (_, _) => SaveAndClose();
var applyButton = new Button { Text = "적용", AutoSize = true };
applyButton.Click += (_, _) => SaveOnly();
var cancelButton = new Button { Text = "취소", AutoSize = true };
cancelButton.Click += (_, _) => Close();
var addRuleButton = new Button { Text = "규칙 추가", AutoSize = true };
addRuleButton.Click += (_, _) => _rows.Add(new RuleRow());
var deleteRuleButton = new Button { Text = "선택 규칙 삭제", AutoSize = true };
deleteRuleButton.Click += (_, _) => DeleteSelectedRules();
var pickProcessButton = new Button { Text = "프로세스 파일 선택", AutoSize = true };
pickProcessButton.Click += (_, _) => PickProcessFileForSelectedRule();
buttonPanel.Controls.Add(saveButton);
buttonPanel.Controls.Add(applyButton);
buttonPanel.Controls.Add(cancelButton);
buttonPanel.Controls.Add(addRuleButton);
buttonPanel.Controls.Add(deleteRuleButton);
buttonPanel.Controls.Add(pickProcessButton);
root.Controls.Add(generalPanel, 0, 0);
root.Controls.Add(guideLabel, 0, 1);
root.Controls.Add(_rulesGrid, 0, 2);
root.Controls.Add(buttonPanel, 0, 3);
Controls.Add(root);
}
private void OnRulesGridDataError(object? sender, DataGridViewDataErrorEventArgs e)
{
_logger.Warn($"Rules grid edit error at row={e.RowIndex}, column={e.ColumnIndex}: {e.Exception?.Message}");
e.ThrowException = false;
}
private void SaveAndClose()
{
if (!ApplyToConfig())
{
return;
}
Close();
}
private void SaveOnly() => ApplyToConfig();
private void PickProcessFileForSelectedRule()
{
_rulesGrid.EndEdit();
var row = GetSelectedRuleRow();
if (row is null)
{
MessageBox.Show("먼저 규칙 행 하나를 선택하세요.", "Nope.exe", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string? selectedPath;
try
{
selectedPath = SelectExecutableFileWithStaSupport(this);
}
catch (Exception ex)
{
MessageBox.Show($"프로세스 파일 선택 중 오류: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
_logger.Error($"Process file picker failed: {ex}");
return;
}
if (string.IsNullOrWhiteSpace(selectedPath))
{
return;
}
var processName = Path.GetFileNameWithoutExtension(selectedPath);
row.ProcessPathExact = selectedPath;
row.ProcessNameExact = processName;
if (string.IsNullOrWhiteSpace(row.Name) || row.Name == "New Rule")
{
row.Name = $"{processName} rule";
}
_rulesGrid.Refresh();
}
private void DeleteSelectedRules()
{
_rulesGrid.EndEdit();
var selectedRows = _rulesGrid.SelectedRows.Cast<DataGridViewRow>()
.Select(r => r.DataBoundItem)
.OfType<RuleRow>()
.Distinct()
.ToList();
if (_rulesGrid.CurrentRow?.DataBoundItem is RuleRow current && !selectedRows.Contains(current))
{
selectedRows.Add(current);
}
if (selectedRows.Count == 0)
{
MessageBox.Show("삭제할 규칙을 선택하세요.", "Nope.exe", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var result = MessageBox.Show($"선택한 규칙 {selectedRows.Count}개를 삭제할까요?", "규칙 삭제", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result != DialogResult.Yes)
{
return;
}
foreach (var row in selectedRows)
{
_rows.Remove(row);
}
}
private RuleRow? GetSelectedRuleRow()
{
if (_rulesGrid.CurrentRow?.DataBoundItem is RuleRow current)
{
return current;
}
var selected = _rulesGrid.SelectedRows.Cast<DataGridViewRow>()
.Select(r => r.DataBoundItem)
.OfType<RuleRow>()
.FirstOrDefault();
return selected;
}
private static string? SelectExecutableFileWithStaSupport(IWin32Window? owner)
{
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
{
return ShowExecutableDialog(owner);
}
string? selectedPath = null;
Exception? dialogError = null;
using var completed = new ManualResetEventSlim(false);
var thread = new Thread(() =>
{
try
{
selectedPath = ShowExecutableDialog(null);
}
catch (Exception ex)
{
dialogError = ex;
}
finally
{
completed.Set();
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
completed.Wait();
if (dialogError is not null)
{
throw new InvalidOperationException("프로세스 파일 선택 창을 여는 중 오류가 발생했습니다.", dialogError);
}
return selectedPath;
}
private static string? ShowExecutableDialog(IWin32Window? owner)
{
using var dialog = new OpenFileDialog
{
Title = "프로세스 실행 파일 선택",
Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*",
CheckFileExists = true,
Multiselect = false,
};
var result = owner is null ? dialog.ShowDialog() : dialog.ShowDialog(owner);
return result == DialogResult.OK ? dialog.FileName : null;
}
private bool ApplyToConfig()
{
try
{
_rulesGrid.EndEdit();
var nextRules = new List<RuleConfig>();
foreach (var row in _rows)
{
if (string.IsNullOrWhiteSpace(row.Name) &&
string.IsNullOrWhiteSpace(row.ProcessNameExact) &&
string.IsNullOrWhiteSpace(row.ProcessPathExact))
{
continue;
}
var rule = row.ToRule();
nextRules.Add(rule);
}
_config.PollIntervalMs = (int)_pollIntervalInput.Value;
_config.CooldownSeconds = (int)_cooldownInput.Value;
_config.Rules = nextRules;
_configService.Save(_config);
_logger.Info("Settings updated via UI and saved.");
MessageBox.Show("설정이 저장되었습니다.", "Nope.exe", MessageBoxButtons.OK, MessageBoxIcon.Information);
return true;
}
catch (Exception ex)
{
MessageBox.Show($"설정 저장 실패: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
_logger.Error($"Settings save failed: {ex}");
return false;
}
}
private sealed class RuleRow
{
public string Name { get; set; } = "New Rule";
public bool Enabled { get; set; } = true;
public string? ProcessNameExact { get; set; }
public string? ProcessPathExact { get; set; }
public RuleActionType ActionType { get; set; } = RuleActionType.Close;
public uint SendMessageTimeoutMs { get; set; } = 1500;
public RuleConfig ToRule() => new()
{
Name = string.IsNullOrWhiteSpace(Name) ? "Unnamed rule" : Name,
Enabled = Enabled,
Match = new MatchConfig
{
ProcessNameExact = NullIfWhitespace(ProcessNameExact),
ProcessPathExact = NullIfWhitespace(ProcessPathExact),
},
Action = new ActionConfig
{
Type = ActionType,
SendMessageTimeoutMs = SendMessageTimeoutMs == 0 ? 1500u : SendMessageTimeoutMs,
},
};
public static RuleRow FromRule(RuleConfig rule) => new()
{
Name = rule.Name,
Enabled = rule.Enabled,
ProcessNameExact = rule.Match.ProcessNameExact,
ProcessPathExact = rule.Match.ProcessPathExact,
ActionType = rule.Action.Type,
SendMessageTimeoutMs = rule.Action.SendMessageTimeoutMs,
};
private static string? NullIfWhitespace(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;
}
시작프로그램 등록
schtasks 래핑 지원:
- 설치
- 제거
- 실패 시 권한 안내 출력
private static int RunSchtasks(string args)
{
var startInfo = new ProcessStartInfo
{
FileName = "schtasks",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
using var process = Process.Start(startInfo);
if (process is null)
{
Console.WriteLine("Failed to launch schtasks.exe.");
return 1;
}
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrWhiteSpace(output))
{
Console.WriteLine(output.Trim());
}
if (!string.IsNullOrWhiteSpace(error))
{
Console.Error.WriteLine(error.Trim());
}
if (process.ExitCode != 0)
{
Console.WriteLine("Task Scheduler command failed. Try running terminal as administrator if required by your policy.");
}
return process.ExitCode;
}
결과
- 원하는 프로세스 실행 시 즉시 종료
- 최소화 / 숨김 / 강제 종료 가능
- 반디집 광고 자동 제거 성공
- 확장 가능한 범용 광고 차단 유틸 완성
마무리
이제 더 이상 반디집 광고에 시달리지 않아도 된다.
광고 창:
“업데이트 하시겠습니까?”
Nope.exe:
“아니.”
끝.
프로그램 코드 보기 및 플로그램 다운 (프로그램 다운은 우측 Releases 클릭하면 버전 별 설치 파일 있음)