Release v0.1.0
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
build.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
@@ -13,3 +13,6 @@ doc/
|
||||
# ── результат сборки ─────────────────────────────────────────────────
|
||||
*.exe
|
||||
*.pdb
|
||||
|
||||
# Локальные архивы релизов
|
||||
dist/
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
@@ -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` ещё не настроен,
|
||||
сначала добавьте адрес своего удалённого репозитория.
|
||||
|
||||
+9
-6
@@ -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;
|
||||
|
||||
@@ -815,12 +815,18 @@
|
||||
</Border>
|
||||
|
||||
<!-- вкладки -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="6,16,0,0">
|
||||
<Grid Grid.Row="2" Margin="6,14,4,3">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button x:Name="BtnTabLog" Style="{StaticResource Tab}" Content="ЛОГ"
|
||||
Foreground="{Binding TabLogBrush}"/>
|
||||
<Button x:Name="BtnTabSettings" Style="{StaticResource Tab}" Content="НАСТРОЙКИ"
|
||||
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">
|
||||
@@ -831,7 +837,9 @@
|
||||
BorderBrush="{DynamicResource GlassEdge}" BorderThickness="1" Padding="4">
|
||||
<TextBox x:Name="LogBox" Text="{Binding LogText, Mode=OneWay}"
|
||||
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"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"/>
|
||||
</Border>
|
||||
|
||||
@@ -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")]
|
||||
+28
-3
@@ -239,6 +239,8 @@ namespace SbLauncher
|
||||
readonly DispatcherTimer _saveTimer = new DispatcherTimer();
|
||||
readonly DispatcherTimer _logTimer = new DispatcherTimer();
|
||||
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;
|
||||
bool _connecting, _checking, _pinging, _editingProfiles;
|
||||
List<AdapterInfo> _adapterSnapshot = new List<AdapterInfo>();
|
||||
@@ -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,26 +1011,45 @@ 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;
|
||||
lock (_logGate)
|
||||
{
|
||||
_pendingLog.Enqueue(stamped);
|
||||
if (Interlocked.Increment(ref _pendingLogCount) > 2000)
|
||||
if (++_pendingLogCount > 2000)
|
||||
{
|
||||
string discarded;
|
||||
if (_pendingLog.TryDequeue(out discarded)) Interlocked.Decrement(ref _pendingLogCount);
|
||||
if (_pendingLog.TryDequeue(out discarded)) --_pendingLogCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearLog()
|
||||
{
|
||||
lock (_logGate)
|
||||
{
|
||||
string discarded;
|
||||
while (_pendingLog.TryDequeue(out discarded)) { }
|
||||
_pendingLogCount = 0;
|
||||
}
|
||||
LogText = "";
|
||||
}
|
||||
|
||||
void FlushLog()
|
||||
{
|
||||
StringBuilder batch = new StringBuilder();
|
||||
string line;
|
||||
int n = 0;
|
||||
lock (_logGate)
|
||||
{
|
||||
while (n++ < 200 && _pendingLog.TryDequeue(out line))
|
||||
{
|
||||
Interlocked.Decrement(ref _pendingLogCount);
|
||||
--_pendingLogCount;
|
||||
if (batch.Length > 0) batch.AppendLine();
|
||||
batch.Append(line);
|
||||
}
|
||||
}
|
||||
if (batch.Length == 0) return;
|
||||
string old = LogText;
|
||||
string combined = old.Length == 0 ? batch.ToString() : old + Environment.NewLine + batch;
|
||||
|
||||
@@ -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
|
||||
|
||||
+111
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user