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
|
*.exe
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
|
# Локальные архивы релизов
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
```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
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user