Release v0.1.0

This commit is contained in:
nikit3s
2026-09-26 21:51:08 +03:00
parent 1975c1ceff
commit 7ac8c78d8b
12 changed files with 317 additions and 52 deletions
+3
View File
@@ -0,0 +1,3 @@
* text=auto eol=lf
build.cmd text eol=crlf
*.ps1 text eol=crlf
+3
View File
@@ -13,3 +13,6 @@ doc/
# ── результат сборки ───────────────────────────────────────────────── # ── результат сборки ─────────────────────────────────────────────────
*.exe *.exe
*.pdb *.pdb
# Локальные архивы релизов
dist/
-10
View File
@@ -140,7 +140,6 @@ namespace SbLauncher
readonly System.Windows.Forms.ToolStripMenuItem _miToggle; readonly System.Windows.Forms.ToolStripMenuItem _miToggle;
readonly Window _w; readonly Window _w;
readonly MainVm _vm; readonly MainVm _vm;
bool _balloonShown;
public Tray(Window w, MainVm vm) public Tray(Window w, MainVm vm)
{ {
@@ -180,15 +179,6 @@ namespace SbLauncher
public void HideToTray() public void HideToTray()
{ {
_w.Hide(); _w.Hide();
if (!_balloonShown)
{
_balloonShown = true;
_ni.ShowBalloonTip(3000, "sb launcher",
_vm.IsConnected
? "Свернулся в трей — туннель продолжает работать."
: "Свернулся в трей.",
System.Windows.Forms.ToolTipIcon.Info);
}
} }
public void Restore() public void Restore()
+17
View File
@@ -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.
+82 -10
View File
@@ -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` не меняется.
Приложение запрашивает права администратора для настройки сетевых маршрутов.
## Запуск ![Светлая тема](preview/glass-light.png)
В папке уже лежат локальные `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 ```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 cmd /c build.cmd
.\SbLauncher.exe .\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` ещё не настроен,
сначала добавьте адрес своего удалённого репозитория.
+9 -6
View File
@@ -674,14 +674,17 @@ namespace SbLauncher
} }
} }
static void ApplyEnv(ProcessStartInfo psi) static void ApplyEnv()
{ {
// те же переменные, что были в startN.bat: конфиги пользуются // те же переменные, что были в startN.bat: конфиги пользуются
// wireguard-outbound'ом, который в sing-box 1.12 объявлен // wireguard-outbound'ом, который в sing-box 1.12 объявлен
// устаревшим и без этого флага не поднимается // устаревшим и без этого флага не поднимается
psi.EnvironmentVariables["ENABLE_DEPRECATED_WIREGUARD_OUTBOUND"] = "true"; // ProcessStartInfo.EnvironmentVariables может упасть, если Windows
psi.EnvironmentVariables["ENABLE_DEPRECATED_TUN_ADDRESS_X"] = "true"; // передала одновременно Path и PATH. Дочерний процесс наследует
psi.EnvironmentVariables["ENABLE_DEPRECATED_SPECIAL_OUTBOUNDS"] = "true"; // переменные текущего процесса и без обращения к этому словарю.
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 — синхронно, до запуска. Ошибку конфига лучше увидеть // sb.exe check — синхронно, до запуска. Ошибку конфига лучше увидеть
@@ -702,7 +705,7 @@ namespace SbLauncher
psi.RedirectStandardError = true; psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8; psi.StandardOutputEncoding = Encoding.UTF8;
psi.StandardErrorEncoding = Encoding.UTF8; psi.StandardErrorEncoding = Encoding.UTF8;
ApplyEnv(psi); ApplyEnv();
using (Process p = new Process()) using (Process p = new Process())
{ {
@@ -749,7 +752,7 @@ namespace SbLauncher
psi.RedirectStandardError = true; psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8; psi.StandardOutputEncoding = Encoding.UTF8;
psi.StandardErrorEncoding = Encoding.UTF8; psi.StandardErrorEncoding = Encoding.UTF8;
ApplyEnv(psi); ApplyEnv();
Process p = new Process(); Process p = new Process();
p.StartInfo = psi; p.StartInfo = psi;
+15 -7
View File
@@ -815,12 +815,18 @@
</Border> </Border>
<!-- вкладки --> <!-- вкладки -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="6,16,0,0"> <Grid Grid.Row="2" Margin="6,14,4,3">
<Button x:Name="BtnTabLog" Style="{StaticResource Tab}" Content="ЛОГ" <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
Foreground="{Binding TabLogBrush}"/> <Button x:Name="BtnTabLog" Style="{StaticResource Tab}" Content="ЛОГ"
<Button x:Name="BtnTabSettings" Style="{StaticResource Tab}" Content="НАСТРОЙКИ" Foreground="{Binding TabLogBrush}"/>
Foreground="{Binding TabSettingsBrush}"/> <Button x:Name="BtnTabSettings" Style="{StaticResource Tab}" Content="НАСТРОЙКИ"
</StackPanel> Foreground="{Binding TabSettingsBrush}"/>
</StackPanel>
<Button Style="{StaticResource Soft}" Content="Очистить лог"
Command="{Binding ClearLogCommand}" Visibility="{Binding LogVis}"
HorizontalAlignment="Right" VerticalAlignment="Center"
ToolTip="Удалить записи из окна лога"/>
</Grid>
<!-- содержимое вкладки: два слоя, видим всегда ровно один --> <!-- содержимое вкладки: два слоя, видим всегда ровно один -->
<Grid Grid.Row="3"> <Grid Grid.Row="3">
@@ -831,7 +837,9 @@
BorderBrush="{DynamicResource GlassEdge}" BorderThickness="1" Padding="4"> BorderBrush="{DynamicResource GlassEdge}" BorderThickness="1" Padding="4">
<TextBox x:Name="LogBox" Text="{Binding LogText, Mode=OneWay}" <TextBox x:Name="LogBox" Text="{Binding LogText, Mode=OneWay}"
IsReadOnly="True" BorderThickness="0" Background="Transparent" IsReadOnly="True" BorderThickness="0" Background="Transparent"
Foreground="{DynamicResource LogText}" FontFamily="Consolas" FontSize="11.5" Foreground="{Binding LogBrush}" SelectionBrush="{DynamicResource Accent}"
IsInactiveSelectionHighlightEnabled="False"
FontFamily="Consolas" FontSize="11.5"
Padding="14,10" TextWrapping="NoWrap" Padding="14,10" TextWrapping="NoWrap"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"/> VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"/>
</Border> </Border>
+6
View File
@@ -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")]
+32 -7
View File
@@ -239,6 +239,8 @@ namespace SbLauncher
readonly DispatcherTimer _saveTimer = new DispatcherTimer(); readonly DispatcherTimer _saveTimer = new DispatcherTimer();
readonly DispatcherTimer _logTimer = new DispatcherTimer(); readonly DispatcherTimer _logTimer = new DispatcherTimer();
readonly ConcurrentQueue<string> _pendingLog = new ConcurrentQueue<string>(); readonly ConcurrentQueue<string> _pendingLog = new ConcurrentQueue<string>();
readonly object _logGate = new object();
static readonly Regex AnsiEscape = new Regex(@"\x1B\[[0-9;]*[A-Za-z]", RegexOptions.Compiled);
int _pendingLogCount, _adapterRefreshVersion, _ipVersion; int _pendingLogCount, _adapterRefreshVersion, _ipVersion;
bool _connecting, _checking, _pinging, _editingProfiles; bool _connecting, _checking, _pinging, _editingProfiles;
List<AdapterInfo> _adapterSnapshot = new List<AdapterInfo>(); List<AdapterInfo> _adapterSnapshot = new List<AdapterInfo>();
@@ -271,6 +273,7 @@ namespace SbLauncher
_logTimer.Start(); _logTimer.Start();
ConnectCommand = new RelayCommand(delegate { ToggleConnect(); }); ConnectCommand = new RelayCommand(delegate { ToggleConnect(); });
ClearLogCommand = new RelayCommand(delegate { ClearLog(); });
PingAllCommand = new RelayCommand(delegate { PingAll(); }); PingAllCommand = new RelayCommand(delegate { PingAll(); });
KillStrayCommand = new RelayCommand(delegate { KillStray(); }); KillStrayCommand = new RelayCommand(delegate { KillStray(); });
OpenFolderCommand = new RelayCommand(delegate { OpenFolder(); }); OpenFolderCommand = new RelayCommand(delegate { OpenFolder(); });
@@ -285,6 +288,7 @@ namespace SbLauncher
} }
public ICommand ConnectCommand { get; private set; } public ICommand ConnectCommand { get; private set; }
public ICommand ClearLogCommand { get; private set; }
public ICommand PingAllCommand { get; private set; } public ICommand PingAllCommand { get; private set; }
public ICommand KillStrayCommand { get; private set; } public ICommand KillStrayCommand { get; private set; }
public ICommand OpenFolderCommand { 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 string ConnectionPhase { get { return _connectionPhase; } set { _connectionPhase = value; Raise(); } }
public bool IsConnected { get { return _runner.IsRunning; } } public bool IsConnected { get { return _runner.IsRunning; } }
public string LogText { get { return _log; } set { _log = value; Raise(); } } 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 string ImportStatus { get { return _importStatus; } set { _importStatus = value; Raise(); } }
public bool IsBusy { get { return _busy; } set { _busy = value; Raise(); } } public bool IsBusy { get { return _busy; } set { _busy = value; Raise(); } }
@@ -463,6 +468,7 @@ namespace SbLauncher
if (ThemeApplier != null) ThemeApplier(_theme); if (ThemeApplier != null) ThemeApplier(_theme);
Raise("ThemeName"); Raise("ThemeGlyph"); Raise("ThemeName"); Raise("ThemeGlyph");
Raise("StatusBrush"); Raise("StatusBrush");
Raise("LogBrush");
Raise("TabLogBrush"); Raise("TabSettingsBrush"); Raise("TabLogBrush"); Raise("TabSettingsBrush");
Raise("PrivateKeyHintBrush"); Raise("LocalAddressHintBrush"); Raise("AdapterHintBrush"); Raise("PrivateKeyHintBrush"); Raise("LocalAddressHintBrush"); Raise("AdapterHintBrush");
foreach (ServerVm s in Servers) s.RefreshBrushes(); foreach (ServerVm s in Servers) s.RefreshBrushes();
@@ -1005,13 +1011,29 @@ namespace SbLauncher
public void AppendLog(string line) 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; string stamped = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture) + " " + line;
_pendingLog.Enqueue(stamped); lock (_logGate)
if (Interlocked.Increment(ref _pendingLogCount) > 2000) {
_pendingLog.Enqueue(stamped);
if (++_pendingLogCount > 2000)
{
string discarded;
if (_pendingLog.TryDequeue(out discarded)) --_pendingLogCount;
}
}
}
public void ClearLog()
{
lock (_logGate)
{ {
string discarded; string discarded;
if (_pendingLog.TryDequeue(out discarded)) Interlocked.Decrement(ref _pendingLogCount); while (_pendingLog.TryDequeue(out discarded)) { }
_pendingLogCount = 0;
} }
LogText = "";
} }
void FlushLog() void FlushLog()
@@ -1019,11 +1041,14 @@ namespace SbLauncher
StringBuilder batch = new StringBuilder(); StringBuilder batch = new StringBuilder();
string line; string line;
int n = 0; int n = 0;
while (n++ < 200 && _pendingLog.TryDequeue(out line)) lock (_logGate)
{ {
Interlocked.Decrement(ref _pendingLogCount); while (n++ < 200 && _pendingLog.TryDequeue(out line))
if (batch.Length > 0) batch.AppendLine(); {
batch.Append(line); --_pendingLogCount;
if (batch.Length > 0) batch.AppendLine();
batch.Append(line);
}
} }
if (batch.Length == 0) return; if (batch.Length == 0) return;
string old = LogText; string old = LogText;
+6 -6
View File
@@ -48,12 +48,12 @@ if not exist app.ico (
exit /b 1 exit /b 1
) )
echo [*] Building %APP_OUT% ... 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 if errorlevel 1 goto failed
echo [+] Done: %CD%\%APP_OUT% echo [+] Done: %CD%\%APP_OUT%
if defined DODEPLOY goto docopy if defined DODEPLOY goto docopy
echo. 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 goto end
:docopy :docopy
@@ -92,7 +92,7 @@ goto end
:selftest :selftest
echo [*] Test build without manifest (so it does not trigger UAC) ... 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 if errorlevel 1 goto failed
echo [+] Built. Running --selftest echo [+] Built. Running --selftest
echo. echo.
@@ -100,7 +100,7 @@ if "%~2"=="" ("%CD%\SbLauncher.selftest.exe" --selftest) else ("%CD%\SbLauncher.
goto end goto end
:stress :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 if errorlevel 1 goto failed
"%CSC%" /nologo /target:exe /platform:x64 /out:UiStress.exe /reference:SbLauncher.selftest.exe %REFS% tests\UiStress.cs "%CSC%" /nologo /target:exe /platform:x64 /out:UiStress.exe /reference:SbLauncher.selftest.exe %REFS% tests\UiStress.cs
if errorlevel 1 goto failed if errorlevel 1 goto failed
@@ -109,7 +109,7 @@ if errorlevel 1 goto failed
goto end goto end
:profiletest :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 if errorlevel 1 goto failed
"%CSC%" /nologo /target:exe /platform:x64 /out:ProfileSmoke.exe /reference:SbLauncher.selftest.exe %REFS% tests\ProfileSmoke.cs "%CSC%" /nologo /target:exe /platform:x64 /out:ProfileSmoke.exe /reference:SbLauncher.selftest.exe %REFS% tests\ProfileSmoke.cs
if errorlevel 1 goto failed if errorlevel 1 goto failed
@@ -118,7 +118,7 @@ if errorlevel 1 goto failed
goto end goto end
:importtest :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 if errorlevel 1 goto failed
"%CSC%" /nologo /target:exe /platform:x64 /out:ImportSmoke.exe /reference:SbLauncher.selftest.exe %REFS% tests\ImportSmoke.cs "%CSC%" /nologo /target:exe /platform:x64 /out:ImportSmoke.exe /reference:SbLauncher.selftest.exe %REFS% tests\ImportSmoke.cs
if errorlevel 1 goto failed if errorlevel 1 goto failed
+111
View File
@@ -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
}
+33 -6
View File
@@ -5,6 +5,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Markup; using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading; using System.Windows.Threading;
using SbLauncher; using SbLauncher;
@@ -15,16 +17,19 @@ public static class UiStress
{ {
var app = new Application(); var app = new Application();
app.ShutdownMode = ShutdownMode.OnExplicitShutdown; app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var root = AppDomain.CurrentDomain.BaseDirectory; var sourceRoot = AppDomain.CurrentDomain.BaseDirectory;
var window = (Window)XamlReader.Parse(File.ReadAllText(Path.Combine(root, "Ui.xaml"))); 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(); var vm = new MainVm();
window.DataContext = vm; window.DataContext = vm;
vm.ThemeApplier = delegate(string theme) { Theme.Apply(window, theme); }; vm.ThemeApplier = delegate(string theme) { Theme.Apply(window, theme); };
vm.Load(new Paths(root)); vm.Load(new Paths(dataRoot));
vm.SetTheme(Theme.Dark); vm.SetTheme(Theme.Dark);
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
long previous = 0, longest = 0; long previous = 0, longest = 0;
int ticks = 0; int ticks = 0;
bool cleared = false;
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) }; var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
timer.Tick += delegate timer.Tick += delegate
{ {
@@ -38,7 +43,17 @@ public static class UiStress
// проверить запуск и остановку Storyboard при нагрузке. // проверить запуск и остановку Storyboard при нагрузке.
if (ticks % 10 == 0) if (ticks % 10 == 0)
vm.ConnectionPhase = new[] { "connecting", "connected", "disconnecting", "idle" }[(ticks / 10) % 4]; 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(); window.Show();
timer.Start(); timer.Start();
@@ -48,7 +63,19 @@ public static class UiStress
}); });
app.Run(); app.Run();
vm.DisposeTimers(); vm.DisposeTimers();
Console.WriteLine("UI ticks: " + ticks + ", max interval: " + longest + " ms, log length: " + vm.LogText.Length); Console.WriteLine("UI ticks: " + ticks + ", max interval: " + longest +
return ticks >= 40 && longest < 500 && vm.LogText.Length <= 50000 ? 0 : 1; " 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);
} }
} }