diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..0da5f7a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+* text=auto eol=lf
+build.cmd text eol=crlf
+*.ps1 text eol=crlf
diff --git a/.gitignore b/.gitignore
index ed6d54a..66b2247 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ doc/
# ── результат сборки ─────────────────────────────────────────────────
*.exe
*.pdb
+
+# Локальные архивы релизов
+dist/
diff --git a/App.cs b/App.cs
index 14a7404..0a8302a 100644
--- a/App.cs
+++ b/App.cs
@@ -140,7 +140,6 @@ namespace SbLauncher
readonly System.Windows.Forms.ToolStripMenuItem _miToggle;
readonly Window _w;
readonly MainVm _vm;
- bool _balloonShown;
public Tray(Window w, MainVm vm)
{
@@ -180,15 +179,6 @@ namespace SbLauncher
public void HideToTray()
{
_w.Hide();
- if (!_balloonShown)
- {
- _balloonShown = true;
- _ni.ShowBalloonTip(3000, "sb launcher",
- _vm.IsConnected
- ? "Свернулся в трей — туннель продолжает работать."
- : "Свернулся в трей.",
- System.Windows.Forms.ToolTipIcon.Info);
- }
}
public void Restore()
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..87819ae
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# История изменений
+
+Здесь фиксируются заметные изменения каждой выпущенной версии. Версии имеют вид
+`MAJOR.MINOR.PATCH`; Git-тег для версии `1.2.3` называется `v1.2.3`.
+
+## Ещё не выпущено
+
+Пока нет записей.
+
+## 0.1.0 — 2026-09-26
+
+- Первый рабочий прототип WPF-лаунчера с интерфейсом glass.
+- Импорт и выбор серверов, три режима подключения, запуск sing-box и просмотр лога.
+- Очистка лога из интерфейса и удаление управляющих ANSI-кодов.
+- Исправлен запуск sing-box в окружении с дублирующимися `Path` и `PATH`.
+- Локальные настройки и профили отделены от исходников и исключены из Git.
+- Сборка ZIP-архива релиза с проверками и контрольной суммой SHA-256.
diff --git a/README.md b/README.md
index 9a8b5d9..389113d 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,95 @@
-# sb launcher · glass preview
+# sb launcher · glass
-Отдельный тестовый репозиторий интерфейса для Windows 11. Рабочий `sb_launcher_rev2` не меняется. Это функционирующий лаунчер на WPF/.NET Framework 4.x: выбор и импорт серверов, проверка конфигурации, запуск и остановка sing-box, логи и настройки остались на месте.
+Лаунчер для sing-box с интерфейсом WPF. Работает в Windows 10/11 x64 с
+.NET Framework 4.x. Поддерживает импорт и выбор серверов, три режима
+подключения (`wg`, `direct`, `wgonly`), проверку конфигурации, запуск и остановку
+sing-box, логи и светлую/тёмную тему.
-Дизайн использует полупрозрачные панели, мягкие градиенты и логотип «щит с арбузом». При подключении кольцо вокруг щита вращается по часовой стрелке; при отключении — обратно. Успешное соединение отмечает короткий световой импульс. Анимации меняют только поворот и прозрачность, без непрерывного размытия фона.
+Это отдельный прототип интерфейса; рабочая копия `sb_launcher_rev2` не меняется.
+Приложение запрашивает права администратора для настройки сетевых маршрутов.
-## Запуск
+
-В папке уже лежат локальные `profiles.json`, `bin/sb.exe` и `doc/doc.srs` для теста; они не входят в Git. `local.json` создан с пустым приватным ключом. Заполните ключ и локальный адрес на вкладке «Настройки», затем в PowerShell:
+[Тёмная тема](preview/glass-dark.png)
+
+## Подготовка
+
+Для сборки нужен `csc.exe` из установленного .NET Framework 4.x. `build.cmd`
+использует `%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\csc.exe`; Visual Studio
+и .NET SDK не требуются.
+
+Для подключения дополнительно нужны совместимый исполняемый файл sing-box и
+файл правил. Они хранятся только локально и в репозиторий или релизный архив
+не входят:
+
+```text
+bin/sb.exe
+doc/doc.srs
+```
+
+Из чистого клона создайте локальные настройки и профили:
```powershell
-cd F:\gitea\sb_launcher_glass
+Copy-Item .\local.example.json .\local.json
+Copy-Item .\profiles.example.json .\profiles.json
cmd /c build.cmd
.\SbLauncher.exe
```
-Сборка использует `csc.exe` из установленного .NET Framework 4.x. Запуск основного exe запросит права администратора: они нужны sing-box для сетевых маршрутов. Для проверки интерфейса без запуска сети: `cmd /c build.cmd stress`.
+Демонстрационные адреса из `profiles.example.json` не подключаются. Добавьте
+свои серверы через импорт или в `profiles.json`, затем укажите приватный ключ
+WireGuard, адрес туннеля и сетевой адаптер на вкладке «Настройки». Секреты в
+`local.json`, а также серверы в `profiles.json` исключены из Git. Не добавляйте
+их в коммиты и релизные архивы.
-Для чистого клона скопируйте `profiles.example.json` в `profiles.json` и `local.example.json` в `local.json`, затем добавьте реальные серверы и ключ. Примерные адреса из `profiles.example.json` служат только для демонстрации. Профили и локальные настройки исключены из Git, поскольку могут содержать учётные данные.
+Готовый ZIP из релиза распаковывается аналогично: поместите рядом с лаунчером
+`bin/sb.exe` и `doc/doc.srs`, скопируйте два файла `.example.json` без слова
+`.example` в имени и заполните свои данные. Сборка из исходников для ZIP не нужна.
-`build.cmd deploy` принимает явный путь назначения; папка рабочего лаунчера никогда не выбирается по умолчанию.
+## Сборка и проверки
-Офлайн-превью с демонстрационными адресами: [светлая тема](preview/glass-light.png) и [тёмная тема](preview/glass-dark.png).
+```powershell
+cmd /c build.cmd # SbLauncher.exe
+cmd /c build.cmd importtest # импорт ссылок
+cmd /c build.cmd profiletest # сохранение профилей
+cmd /c build.cmd stress # нагрузка интерфейса
+```
+
+`build.cmd selftest` проверяет локальную конфигурацию и может использовать
+текущие профили. `build.cmd deploy "путь"` копирует сборку в явно указанную
+тестовую папку; локальный `local.json` в ней не перезаписывается.
+
+## Git и релизы
+
+Основная ветка — `main`. Разработку фиксируйте коммитами в Git. Для очередного
+релиза обновите [CHANGELOG.md](CHANGELOG.md), три версии в `Version.cs` и номер
+`assemblyIdentity` в `app.manifest`; используйте формат `MAJOR.MINOR.PATCH`.
+После этого, например для версии `0.1.1`:
+
+```powershell
+git status
+git add -A
+git diff --cached --stat
+git commit -m "Release v0.1.1"
+.\release.ps1 -Version 0.1.1
+git tag -a v0.1.1 -m "SbLauncher v0.1.1"
+```
+
+Перед коммитом проверьте список добавленных файлов: в нём должны быть только
+изменения для этого релиза. `release.ps1` требует чистую рабочую копию, запускает сборку и три
+проверки, сверяет версию EXE и собирает `dist/SbLauncher-vX.Y.Z-win-x64.zip`
+с SHA-256 в соседнем `.sha256`. Архив содержит только приложение, шаблоны,
+примеры конфигурации и документацию. Сборка идёт через `build.cmd staged`,
+поэтому открытый лаунчер не блокирует выпуск. Для пробного запуска на незакоммиченных
+изменениях есть `-AllowDirty`; архив получает суффикс `-dev` с хешем коммита.
+
+При настроенном удалённом репозитории отправьте ветку и тег:
+
+```powershell
+git push origin main
+git push origin v0.1.1
+```
+
+Затем создайте релиз `v0.1.1` в GitHub/Gitea, вставьте заметки из
+`CHANGELOG.md` и прикрепите ZIP и `.sha256`. Если `origin` ещё не настроен,
+сначала добавьте адрес своего удалённого репозитория.
diff --git a/SbLauncher.cs b/SbLauncher.cs
index 3353056..b534c15 100644
--- a/SbLauncher.cs
+++ b/SbLauncher.cs
@@ -674,14 +674,17 @@ namespace SbLauncher
}
}
- static void ApplyEnv(ProcessStartInfo psi)
+ static void ApplyEnv()
{
// те же переменные, что были в startN.bat: конфиги пользуются
// wireguard-outbound'ом, который в sing-box 1.12 объявлен
// устаревшим и без этого флага не поднимается
- psi.EnvironmentVariables["ENABLE_DEPRECATED_WIREGUARD_OUTBOUND"] = "true";
- psi.EnvironmentVariables["ENABLE_DEPRECATED_TUN_ADDRESS_X"] = "true";
- psi.EnvironmentVariables["ENABLE_DEPRECATED_SPECIAL_OUTBOUNDS"] = "true";
+ // ProcessStartInfo.EnvironmentVariables может упасть, если Windows
+ // передала одновременно Path и PATH. Дочерний процесс наследует
+ // переменные текущего процесса и без обращения к этому словарю.
+ Environment.SetEnvironmentVariable("ENABLE_DEPRECATED_WIREGUARD_OUTBOUND", "true");
+ Environment.SetEnvironmentVariable("ENABLE_DEPRECATED_TUN_ADDRESS_X", "true");
+ Environment.SetEnvironmentVariable("ENABLE_DEPRECATED_SPECIAL_OUTBOUNDS", "true");
}
// sb.exe check — синхронно, до запуска. Ошибку конфига лучше увидеть
@@ -702,7 +705,7 @@ namespace SbLauncher
psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8;
psi.StandardErrorEncoding = Encoding.UTF8;
- ApplyEnv(psi);
+ ApplyEnv();
using (Process p = new Process())
{
@@ -749,7 +752,7 @@ namespace SbLauncher
psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8;
psi.StandardErrorEncoding = Encoding.UTF8;
- ApplyEnv(psi);
+ ApplyEnv();
Process p = new Process();
p.StartInfo = psi;
diff --git a/Ui.xaml b/Ui.xaml
index addf02f..167f28d 100644
--- a/Ui.xaml
+++ b/Ui.xaml
@@ -815,12 +815,18 @@
-
-
-
-
+
+
+
+
+
+
+
@@ -831,7 +837,9 @@
BorderBrush="{DynamicResource GlassEdge}" BorderThickness="1" Padding="4">
diff --git a/Version.cs b/Version.cs
new file mode 100644
index 0000000..72b74b4
--- /dev/null
+++ b/Version.cs
@@ -0,0 +1,6 @@
+using System.Reflection;
+
+// Обновляйте вместе с CHANGELOG.md перед выпуском новой версии.
+[assembly: AssemblyVersion("0.1.0.0")]
+[assembly: AssemblyFileVersion("0.1.0.0")]
+[assembly: AssemblyInformationalVersion("0.1.0")]
diff --git a/ViewModels.cs b/ViewModels.cs
index 2a12431..3423c36 100644
--- a/ViewModels.cs
+++ b/ViewModels.cs
@@ -239,6 +239,8 @@ namespace SbLauncher
readonly DispatcherTimer _saveTimer = new DispatcherTimer();
readonly DispatcherTimer _logTimer = new DispatcherTimer();
readonly ConcurrentQueue _pendingLog = new ConcurrentQueue();
+ readonly object _logGate = new object();
+ static readonly Regex AnsiEscape = new Regex(@"\x1B\[[0-9;]*[A-Za-z]", RegexOptions.Compiled);
int _pendingLogCount, _adapterRefreshVersion, _ipVersion;
bool _connecting, _checking, _pinging, _editingProfiles;
List _adapterSnapshot = new List();
@@ -271,6 +273,7 @@ namespace SbLauncher
_logTimer.Start();
ConnectCommand = new RelayCommand(delegate { ToggleConnect(); });
+ ClearLogCommand = new RelayCommand(delegate { ClearLog(); });
PingAllCommand = new RelayCommand(delegate { PingAll(); });
KillStrayCommand = new RelayCommand(delegate { KillStray(); });
OpenFolderCommand = new RelayCommand(delegate { OpenFolder(); });
@@ -285,6 +288,7 @@ namespace SbLauncher
}
public ICommand ConnectCommand { get; private set; }
+ public ICommand ClearLogCommand { get; private set; }
public ICommand PingAllCommand { get; private set; }
public ICommand KillStrayCommand { get; private set; }
public ICommand OpenFolderCommand { get; private set; }
@@ -304,6 +308,7 @@ namespace SbLauncher
public string ConnectionPhase { get { return _connectionPhase; } set { _connectionPhase = value; Raise(); } }
public bool IsConnected { get { return _runner.IsRunning; } }
public string LogText { get { return _log; } set { _log = value; Raise(); } }
+ public Brush LogBrush { get { return Theme.Res("LogText"); } }
public string ImportStatus { get { return _importStatus; } set { _importStatus = value; Raise(); } }
public bool IsBusy { get { return _busy; } set { _busy = value; Raise(); } }
@@ -463,6 +468,7 @@ namespace SbLauncher
if (ThemeApplier != null) ThemeApplier(_theme);
Raise("ThemeName"); Raise("ThemeGlyph");
Raise("StatusBrush");
+ Raise("LogBrush");
Raise("TabLogBrush"); Raise("TabSettingsBrush");
Raise("PrivateKeyHintBrush"); Raise("LocalAddressHintBrush"); Raise("AdapterHintBrush");
foreach (ServerVm s in Servers) s.RefreshBrushes();
@@ -1005,13 +1011,29 @@ namespace SbLauncher
public void AppendLog(string line)
{
+ if (line == null) line = "";
+ if (line.IndexOf('\x1b') >= 0) line = AnsiEscape.Replace(line, "");
string stamped = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture) + " " + line;
- _pendingLog.Enqueue(stamped);
- if (Interlocked.Increment(ref _pendingLogCount) > 2000)
+ lock (_logGate)
+ {
+ _pendingLog.Enqueue(stamped);
+ if (++_pendingLogCount > 2000)
+ {
+ string discarded;
+ if (_pendingLog.TryDequeue(out discarded)) --_pendingLogCount;
+ }
+ }
+ }
+
+ public void ClearLog()
+ {
+ lock (_logGate)
{
string discarded;
- if (_pendingLog.TryDequeue(out discarded)) Interlocked.Decrement(ref _pendingLogCount);
+ while (_pendingLog.TryDequeue(out discarded)) { }
+ _pendingLogCount = 0;
}
+ LogText = "";
}
void FlushLog()
@@ -1019,11 +1041,14 @@ namespace SbLauncher
StringBuilder batch = new StringBuilder();
string line;
int n = 0;
- while (n++ < 200 && _pendingLog.TryDequeue(out line))
+ lock (_logGate)
{
- Interlocked.Decrement(ref _pendingLogCount);
- if (batch.Length > 0) batch.AppendLine();
- batch.Append(line);
+ while (n++ < 200 && _pendingLog.TryDequeue(out line))
+ {
+ --_pendingLogCount;
+ if (batch.Length > 0) batch.AppendLine();
+ batch.Append(line);
+ }
}
if (batch.Length == 0) return;
string old = LogText;
diff --git a/build.cmd b/build.cmd
index b3468a4..0386740 100644
--- a/build.cmd
+++ b/build.cmd
@@ -48,12 +48,12 @@ if not exist app.ico (
exit /b 1
)
echo [*] Building %APP_OUT% ...
-"%CSC%" /nologo /target:winexe /platform:x64 /optimize+ /out:%APP_OUT% /win32manifest:app.manifest /win32icon:app.ico %RES% %REFS% SbLauncher.cs ViewModels.cs App.cs Import.cs
+"%CSC%" /nologo /target:winexe /platform:x64 /optimize+ /out:%APP_OUT% /win32manifest:app.manifest /win32icon:app.ico %RES% %REFS% Version.cs SbLauncher.cs ViewModels.cs App.cs Import.cs
if errorlevel 1 goto failed
echo [+] Done: %CD%\%APP_OUT%
if defined DODEPLOY goto docopy
echo.
-echo Run SbLauncher.exe here, or deploy to an explicit test target.
+echo Run %APP_OUT% here, or deploy to an explicit test target.
goto end
:docopy
@@ -92,7 +92,7 @@ goto end
:selftest
echo [*] Test build without manifest (so it does not trigger UAC) ...
-"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% SbLauncher.cs ViewModels.cs App.cs Import.cs
+"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% Version.cs SbLauncher.cs ViewModels.cs App.cs Import.cs
if errorlevel 1 goto failed
echo [+] Built. Running --selftest
echo.
@@ -100,7 +100,7 @@ if "%~2"=="" ("%CD%\SbLauncher.selftest.exe" --selftest) else ("%CD%\SbLauncher.
goto end
:stress
-"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% SbLauncher.cs ViewModels.cs App.cs Import.cs
+"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% Version.cs SbLauncher.cs ViewModels.cs App.cs Import.cs
if errorlevel 1 goto failed
"%CSC%" /nologo /target:exe /platform:x64 /out:UiStress.exe /reference:SbLauncher.selftest.exe %REFS% tests\UiStress.cs
if errorlevel 1 goto failed
@@ -109,7 +109,7 @@ if errorlevel 1 goto failed
goto end
:profiletest
-"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% SbLauncher.cs ViewModels.cs App.cs Import.cs
+"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% Version.cs SbLauncher.cs ViewModels.cs App.cs Import.cs
if errorlevel 1 goto failed
"%CSC%" /nologo /target:exe /platform:x64 /out:ProfileSmoke.exe /reference:SbLauncher.selftest.exe %REFS% tests\ProfileSmoke.cs
if errorlevel 1 goto failed
@@ -118,7 +118,7 @@ if errorlevel 1 goto failed
goto end
:importtest
-"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% SbLauncher.cs ViewModels.cs App.cs Import.cs
+"%CSC%" /nologo /target:exe /platform:x64 /out:SbLauncher.selftest.exe %RES% %REFS% Version.cs SbLauncher.cs ViewModels.cs App.cs Import.cs
if errorlevel 1 goto failed
"%CSC%" /nologo /target:exe /platform:x64 /out:ImportSmoke.exe /reference:SbLauncher.selftest.exe %REFS% tests\ImportSmoke.cs
if errorlevel 1 goto failed
diff --git a/release.ps1 b/release.ps1
new file mode 100644
index 0000000..bdb4d11
--- /dev/null
+++ b/release.ps1
@@ -0,0 +1,111 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [ValidatePattern('^\d+\.\d+\.\d+$')]
+ [string]$Version,
+
+ # Только для проверки сборки из рабочей копии. Такой архив помечен dev.
+ [switch]$AllowDirty
+)
+
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $MyInvocation.MyCommand.Path
+Push-Location $root
+try {
+ $inside = & git rev-parse --is-inside-work-tree 2>$null
+ if ($LASTEXITCODE -ne 0 -or $inside -ne 'true') {
+ throw 'Запустите скрипт из Git-репозитория проекта.'
+ }
+
+ $commit = (& git rev-parse --short=12 HEAD).Trim()
+ if ($LASTEXITCODE -ne 0) { throw 'Не удалось определить Git-коммит.' }
+ $changes = @(& git status --porcelain=v1 --untracked-files=normal)
+ if ($LASTEXITCODE -ne 0) { throw 'Не удалось проверить состояние Git.' }
+ $dirty = $changes.Count -gt 0
+ if ($dirty -and -not $AllowDirty) {
+ throw 'Для релиза нужен чистый Git-коммит. Для пробной сборки используйте -AllowDirty.'
+ }
+
+ $tag = 'v' + $Version
+ $tagCommit = & git rev-list -n 1 $tag 2>$null
+ if ($LASTEXITCODE -eq 0) {
+ $headCommit = (& git rev-parse HEAD).Trim()
+ if ($tagCommit.Trim() -ne $headCommit) {
+ throw "Тег $tag уже указывает на другой коммит. Выберите новую версию."
+ }
+ }
+
+ $dist = Join-Path $root 'dist'
+ New-Item -ItemType Directory -Path $dist -Force | Out-Null
+ $stressRoot = Join-Path $dist ('stress-' + [guid]::NewGuid().ToString('N'))
+ New-Item -ItemType Directory -Path $stressRoot | Out-Null
+ $previousStressRoot = $env:SB_STRESS_ROOT
+ $env:SB_STRESS_ROOT = $stressRoot
+ try {
+ foreach ($mode in @('staged', 'importtest', 'profiletest', 'stress')) {
+ Write-Host "[*] build.cmd $mode"
+ & $env:ComSpec /d /c build.cmd $mode
+ if ($LASTEXITCODE -ne 0) { throw "Сборка или проверка $mode завершилась с ошибкой." }
+ }
+ }
+ finally {
+ $env:SB_STRESS_ROOT = $previousStressRoot
+ $stressFull = [IO.Path]::GetFullPath($stressRoot)
+ $distFull = [IO.Path]::GetFullPath($dist).TrimEnd('\') + '\'
+ if ($stressFull.StartsWith($distFull, [StringComparison]::OrdinalIgnoreCase)) {
+ Remove-Item -LiteralPath $stressFull -Recurse -Force
+ }
+ }
+
+ $exe = Join-Path $root 'SbLauncher.updated.exe'
+ $builtVersion = [Reflection.AssemblyName]::GetAssemblyName($exe).Version.ToString(3)
+ if ($builtVersion -ne $Version) {
+ throw "Версия сборки $builtVersion не совпадает с $Version. Обновите Version.cs."
+ }
+
+ $suffix = if ($dirty) { "-dev-$commit-$([DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))" } else { '' }
+ $baseName = "SbLauncher-v$Version-win-x64$suffix"
+ $archive = Join-Path $dist ($baseName + '.zip')
+ $checksum = $archive + '.sha256'
+ if ((Test-Path -LiteralPath $archive) -or (Test-Path -LiteralPath $checksum)) {
+ throw "Файл $baseName уже существует в dist. Удалите его вручную или выберите новую версию."
+ }
+
+ $stage = Join-Path $dist ('stage-' + [guid]::NewGuid().ToString('N'))
+ New-Item -ItemType Directory -Path $stage | Out-Null
+ try {
+ Copy-Item -LiteralPath $exe -Destination (Join-Path $stage 'SbLauncher.exe')
+ foreach ($name in @('Ui.xaml', 'README.md', 'CHANGELOG.md',
+ 'profiles.example.json', 'local.example.json')) {
+ Copy-Item -LiteralPath (Join-Path $root $name) -Destination $stage
+ }
+ $templates = Join-Path $stage 'templates'
+ New-Item -ItemType Directory -Path $templates | Out-Null
+ foreach ($name in @('wg.json', 'direct.json', 'wgonly.json')) {
+ Copy-Item -LiteralPath (Join-Path (Join-Path $root 'templates') $name) -Destination $templates
+ }
+ $preview = Join-Path $stage 'preview'
+ New-Item -ItemType Directory -Path $preview | Out-Null
+ foreach ($name in @('glass-light.png', 'glass-dark.png')) {
+ Copy-Item -LiteralPath (Join-Path (Join-Path $root 'preview') $name) -Destination $preview
+ }
+ @("Version: $tag", "Commit: $commit", "Build: win-x64") |
+ Set-Content -LiteralPath (Join-Path $stage 'VERSION.txt') -Encoding UTF8
+ Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $archive -CompressionLevel Optimal
+ }
+ finally {
+ $stageFull = [IO.Path]::GetFullPath($stage)
+ $distFull = [IO.Path]::GetFullPath($dist).TrimEnd('\') + '\'
+ if ($stageFull.StartsWith($distFull, [StringComparison]::OrdinalIgnoreCase)) {
+ Remove-Item -LiteralPath $stageFull -Recurse -Force
+ }
+ }
+
+ $hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
+ "$hash $($baseName).zip" | Set-Content -LiteralPath $checksum -Encoding ASCII
+ Write-Host "[+] $archive"
+ Write-Host "[+] $checksum"
+ if ($dirty) { Write-Warning 'Это пробная сборка из изменённой рабочей копии. Не публикуйте её как релиз.' }
+}
+finally {
+ Pop-Location
+}
diff --git a/tests/UiStress.cs b/tests/UiStress.cs
index 0ff08c8..51915be 100644
--- a/tests/UiStress.cs
+++ b/tests/UiStress.cs
@@ -5,6 +5,8 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Markup;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
using System.Windows.Threading;
using SbLauncher;
@@ -15,16 +17,19 @@ public static class UiStress
{
var app = new Application();
app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
- var root = AppDomain.CurrentDomain.BaseDirectory;
- var window = (Window)XamlReader.Parse(File.ReadAllText(Path.Combine(root, "Ui.xaml")));
+ var sourceRoot = AppDomain.CurrentDomain.BaseDirectory;
+ var dataRoot = Environment.GetEnvironmentVariable("SB_STRESS_ROOT");
+ if (string.IsNullOrEmpty(dataRoot)) dataRoot = sourceRoot;
+ var window = (Window)XamlReader.Parse(File.ReadAllText(Path.Combine(sourceRoot, "Ui.xaml")));
var vm = new MainVm();
window.DataContext = vm;
vm.ThemeApplier = delegate(string theme) { Theme.Apply(window, theme); };
- vm.Load(new Paths(root));
+ vm.Load(new Paths(dataRoot));
vm.SetTheme(Theme.Dark);
var stopwatch = Stopwatch.StartNew();
long previous = 0, longest = 0;
int ticks = 0;
+ bool cleared = false;
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
timer.Tick += delegate
{
@@ -38,7 +43,17 @@ public static class UiStress
// проверить запуск и остановку Storyboard при нагрузке.
if (ticks % 10 == 0)
vm.ConnectionPhase = new[] { "connecting", "connected", "disconnecting", "idle" }[(ticks / 10) % 4];
- if (now >= 4000) { timer.Stop(); app.Shutdown(); }
+ if (ticks == 32 && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SB_STRESS_SCREENSHOT")))
+ {
+ vm.SetTheme(Theme.Light);
+ Capture(window, Environment.GetEnvironmentVariable("SB_STRESS_SCREENSHOT"));
+ }
+ if (ticks == 20)
+ {
+ vm.ClearLogCommand.Execute(null);
+ cleared = vm.LogText.Length == 0;
+ }
+ if (now >= 5000) { timer.Stop(); app.Shutdown(); }
};
window.Show();
timer.Start();
@@ -48,7 +63,19 @@ public static class UiStress
});
app.Run();
vm.DisposeTimers();
- Console.WriteLine("UI ticks: " + ticks + ", max interval: " + longest + " ms, log length: " + vm.LogText.Length);
- return ticks >= 40 && longest < 500 && vm.LogText.Length <= 50000 ? 0 : 1;
+ Console.WriteLine("UI ticks: " + ticks + ", max interval: " + longest +
+ " ms, log length: " + vm.LogText.Length + ", clear: " + cleared);
+ return ticks >= 30 && longest < 500 && vm.LogText.Length <= 50000 && cleared ? 0 : 1;
+ }
+
+ static void Capture(Window window, string path)
+ {
+ window.UpdateLayout();
+ var bmp = new RenderTargetBitmap((int)window.Width, (int)window.Height,
+ 96, 96, PixelFormats.Pbgra32);
+ bmp.Render((Visual)window.Content);
+ var encoder = new PngBitmapEncoder();
+ encoder.Frames.Add(BitmapFrame.Create(bmp));
+ using (var stream = new FileStream(path, FileMode.Create)) encoder.Save(stream);
}
}