SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11

Посетители выразили интерес о ходе разработке SophiApp в комментариях к статье, потому выполняю обещанное. Это копия моей статьи на Хабре.

Я от лица команды хочу показать вам SophiApp — графический наследник Sophia Script for Windows: бесплатная, портативная и полностью опенсорная программа для тонкой настройки Windows 10 и Windows 11.

В этой статье я расскажу, как оброненная мной фраза в комментарии 3 года назад под моей статьей из цикла про тонкую настройку Windows развернула мою жизнь на 180°, а чуть позже — и еще одного человека.

Все это время у меня была идея сделать графическую версию моего модуля на PowerShell, чтобы показать пользователям, каким должен быть современный твикер для Windows, какие функции может в себе нести, а главное — посыл программы: настроить (а не оптимизировать) ОС официальным образом, задокументированным Microsoft, ничего не сломав и не обещая мнимое увеличение производительности, чем грешат аналогичные программы, целенаправленно вводя пользователей в заблуждение.

Уже есть идеи насчет версии 2.0 с более современными UI а-ля Windows 11 и UX, а также расширенной функциональностью, но первый блин, вроде как, не оказался комом. Программа все это время делалась на голом энтузиазме, и мы искренне хотим, чтобы пользователи Windows перестали воспринимать так называемые твикеры как что-то по определению вредное, не несущее пользы, а узнали, как можно настроить современные Windows 10 и 11 и что они в себе таят.

Как появилась идея программы, и знакомство с Дмитрием

Как-то летом 2019 года в моей первой статье Скрипт настройки Windows 10 в ответ на предложение о создании графической версии моего PowerShell-скрипта я посетовал, что PowerShell-грамоте не обучены мы, но есть желание что-нибудь сотворить. На тот момент знания в PowerShell-ремесле были скудны (как и сейчас), потому максимум, на что я рассчитывал, — сварганить что-нибудь на Windows Forms, как делают многие на GitHub. Но вдруг 3 сентября 2019 года в личные сообщения на Хабре мне написал некто, представившись Дмитрием (старый аккаунт на Хабре, GitHub), с предложением сделать то, о чем я мечтал! Сказать, что я был удивлен, что кто-то откликнулся мне помочь, — ничего не сказать. Как выяснилось, он уже собаку съел на такого рода GUI-окнах с кнопками, так как это была часть его работы. А показав нам реальные примеры своих работ, он укрепил меня во мнении, что у нас все получится. Ну, скажем, месяца за 2—3. Кто бы мог помыслить, во что это все выльется для нас обоих…

Что не так с «рынком» твикеров

Наверное, надо немного отвлечься и затронуть тему особенности «рынка» так называемых твикеров для Windows. Фундаментально все программы такого назначения можно разделить на 2 категории:

  • Настраивают внешний вид ОС;
  • Вмешиваются в работу ОС (и иногда ломая ее работоспособность на корню): Удаление Microsoft Defender, вырывая его с корнем из системы; Удаление несчастных UWP-приложений, варварски выкорчевывая файлы из %ProgramFiles%\WindowsApps;
  • Отключение получения обновлений через Центр обновления Windows.

В основном эта сборная солянка сдобрена парочкой настроек проводника в том или ином виде, добавлением пары политик и так далее. Не забудем также про маниакальное желание пользователей ежесекундно чистить папки по всей ОС, где могут быть хоть какие-то временные файлы. Сие действие возведено просто в абсолют.

Но это все крайности. В основном, когда выходит очередной обзор такого рода программы на YouTube, на превью-картинку ставится текст, что во всех играх у вас повысятся до 300 FPS (вне зависимости от текущих характеристик ПК) и также обязательно употребляется слово «оптимизация».

В случае с англоязычным сегментом интернета обязательно употребляются «debloat« или «debotnet», намекая, что Windows состоит чуть менее, чем полностью из ненужных программ. Ведь только разработчики такого рода программ знают, что Windows »из коробки» работает нестабильно, а Microsoft скрывает от нас секретные ключи реестра, которые-то и сделают из вашего ПК ракету. И вообще всему виной, по их мнению, Microsoft Defender, сжирающий МБ ОЗУ, предустановленные UWP-приложения и логи, создаваемые бесчисленными сборщиками из Просмотрщика событий, — практически всадники Апокалипсиса!

Пользователи, недовольные быстродействием Windows
Пользователи, недовольные быстродействием Windows

Такого рода действия могут совершаться по следующим причинам:

  • Привлечение внимания пользователей, у которых в большинстве своем могут быть не самые мощные ПК;
  • Искренняя убежденность в правильности своих действий в силу отсутствия знаний о работе Windows;
  • Целенаправленное введение в заблуждение пользователей с целью создания вокруг себя ауры гуру в вопросах работы Windows и того, как ее «ускорить»;
  • Распространение зловредных программ с целью извлечения прибыли от доверчивых пользователей.

Первые попытки на PowerShell, или осознание, что надо писать на C#

Мы с Дмитрием быстро нашли общий язык, и, обговорив вектор разработки, я объяснил, как работают функции в скрипте, чтобы он перенес их в графику.

Долго ли, коротко ли, но уже к концу сентября, когда количество строк кода в его PowerShell-скрипте перевалило за 20 000, powershell. exe встал колом: запуск уже занимал около 10 секунд, и никто не понимал, что там написано и как это работает. Надо было идти дальше. Но куда? Мы явно не рассчитали ни наши силы, ни знания, ни время, необходимое на такой проект. Ответ родился сам по себе: я позвонил Дмитрию и робко предложил ему написать программу на чистом C#. Судя по звукам, которые он начал издавать, я подумал, случилось примерно следующее:

Он не отрицал, что пишет иногда для себя простенькие консольные программы на C# для облегчения своей работы, но это не входит в его круг обязанностей — это как хобби, и он не потянет. Уж не знаю, какими словами, но я убедил его попробовать. Тут же встала новая проблема: как на предыдущем GUI-приложении не напихаешь кнопок — тут нужен настоящий дизайн программы! Наверное, именно в то время Дмитрий начал догадываться, что его втягивают в какую-то авантюру.

Такого твикера не дай бог никому! Кадры из к/ф Ширли-мырли

Страшные наброски 3000

Все прочие скриншоты с видео утрачены или удалены, и никто уже не увидит, как плохо у нас все выглядело. :)

Где-то к июню 2020 мы окончательно признались себе, что ничего у нас не выйдет с таким подходом, и надо заказывать у какого-нибудь фрилансера UI с нормальным, проработанным UX. И вот впервые нам улыбнулась удача, когда на сайте фрилансеров мы наткнулись на Владимира.

Не будем заострять внимание на этой итерации разработки, так как и она потерпела крах даже с привлеченным разработчиком интерфейсов. Вся проблема в том, что если ты сам до конца не понимаешь, что тебе надо, то и результат будет соответствующий. После получения готового макета в Zeplin наша эйфория длилась недолго: его вариант даже нельзя было сравнивать с нашими потугами, но все разбилось о скалы «незакладывания масштабирования». Мы не учли столько вещей, что было даже стыдно признаться ему, что по сути надо все переделывать. Но осознание провала еще не накрыло нас, и мы барахтались с тем вариантом дизайна еще где-то до декабря 2020, когда пришло окончательно понимание, что так больше не может продолжаться.

В декабре 2020 в третий раз мы закинули невод с твердым намерением завершить проект во что бы то ни стало! Учтя все недостатки предыдущего дизайна (так считали), мы вновь составили техническое задание для Владимира, надеясь на богов верхней реки, что на этот-то раз у нас все получится.

Дмитрий: Ah shit, here we go again

Нельзя сказать, что с приобретением последнего макета разработка пошла легче, просто нами уже двигали как желание сделать наперекор всему, так и уж больно понравился дизайн. В особенности после того, что выдавали мы.

Текущая версии SophiApp и то, как она работает

Полномочия в команде мы разделили следующим образом: на мои плечи возложено было написание всех логически верных проверок на PowerShell, чтобы Дмитрий мог в дальнейшем понять, как выставлять чекбоксы в интерфейсе, то бишь, когда ложь, а когда истина. Кроме того, перевод интерфейса на английский язык, тестирование сборок, PR, написание начальных проверок и проверок для определения работоспособности Microsoft Defender (чтобы отсеять пользователей, у которых он сломан или сломан кэш WMI), логика работы каждой функции. Дмитрию же достались написание кода и отладка.

Стоит заострить внимание, почему мы так трепетно относимся к проверке версии сборки Windows и работоспособности Microsoft Defender. На самом деле все достаточно банально: как показала практика поддержки моего скрипта Sophia Script for Windows (более 500 000 скачиваний за 2,5 года, а также более 5 000 звезд на GitHub), есть достаточная прослойка пользователей, которые целенаправленно ломают Windows, используя сомнительные программы, с целью выключения встроенного антивируса или его полного вырезания из системы. Все опять же растет из YouTube, где нечистые на руку блогеры специально ведут риторику о том, что все беды в ОС от наличия в ней Microsoft Defender.

Другая крайность, с которой мы столкнулись, — это отказ некоторых пользователей вообще обновлять их Windows, так как они уверены (опять же с подачи блоггеров), что обновления к ОС выходят каждый день, и их надо срочно отключить. Эти два фактора напрямую влияют, какой фидбек мы получаем от пользователя после использования SophiApp. И чем сильнее укрепляется в вере пользователь, что Defender нужно сломать, а ОС никогда не обновлять, тем больше ошибок возникает в работе нашей программы.

С учетом этих вводных, волевым усилием было принято решение где-то раз в полгода повышать требование к минорной версии билда Windows. На текущий момент это 19044. 1706+ для Windows 10 и 22000.739+, Windows 11 (Windows 11 Insider Preview) соответственно.

То же касается и проверки через интернет, последняя ли версия программы запущена: в каждом билде исправляются ошибки (и, конечно, добавляются новые), потому мы хотим предоставить лучший опыт использования пользователям. Для этого программа не только уведомляет об обнаружении новой версии, но и блокирует текущую, если ее версия ниже, чем уже имеется. А проверка осуществляется достаточно примитивно. Как у многих софтверных компаний в облаке хранится JSON-файл, где прописываются последние версии для стабильной ветки и для бета-версии.

Итак, мы плавно подходим к описанию того, как работает под капотом SophiApp, но сначала напомню особенности программы:

  • Динамически отрисовывающийся UI: все элементы НЕ захардкожены;
  • 25 000+ строк кода (не считая JSON-конфигов);
  • Более 130 твиков;
  • Копировать описания функций через ПКМ;
  • Переведена носителями на английский, украинский, немецкий, итальянский, французский, чешский и турецкий языки;
  • SophiApp использует паттерн MVVM;
  • Поддержка многопоточности;
  • SophiApp проверяется статическим анализатором, лицензию на который любезно предоставили в PVS-Studio;
  • Все билды компилируются в облаке с использованием GitHub Actions (конфиг). Вы можете сравнить хэш-сумму архива на странице релиза с хэш-суммой в облачной консоли на шаге «Compress Files», чтобы быть уверенным, что архив не подменялся после релиза (для открытия облачных логов вы должны войти в вашу учетную запись GitHub);
  • Описание к функциям при наведении курсора на функцию;
  • Имеет встроенный движок поиска по заголовкам и описанию;
  • Программа поддерживает темную и светлую темы. Может менять тему мгновенно в зависимости от выставленного режима приложений в Windows;
  • Настроить конфиденциальность и телеметрию;
  • Выключить задания диагностического характера в Планировщике заданий;
  • Настроить UI и персонализацию;
  • Правильно и до конца удалить OneDrive, не нарушив целостность ОС;
  • Удалить UWP-приложения, отображая локализованные имена пакетов. Список приложений рендерится динамически, используя локальные иконки самих приложений. Ничего не захардкожено;
  • Скачать и установить расширение HEVC Video Extensions from Device Manufacturer, чтобы появилась возможность открывать файлы формата. heic и. heif;
  • Создать задание «Windows Cleanup» по очистке неиспользуемых файлов и обновлений Windows в Планировщике заданий. Перед началом очистки всплывет нативный тост, где вы сможете выбрать: отложить, отменить или запустить задание;
  • Создать задание «SoftwareDistribution» по очистке папок% SystemRoot% \SoftwareDistribution\Download и% TEMP% в Планировщике заданий;
  • Настроить безопасность Windows;
  • Программа полностью портативная: в реестре не сохраняются никакие специальные ключи, а после закрытия чистится лог .NET Framework от программы;
  • Огромное количество твиков по кастомизации проводника и контекстного меню;
  • Все настройки проводятся задокументированными возможностями ОС, что исключает шанс навредить работоспособности системы.

Системные требования

  • Windows 10 2004/20H2/21H1/21H2/22H2 x64;Билд 1904x. 1706+.
  • Windows 11 21H2/22H2/23H2;22000.739+, 22509+.
  • Чтобы запустить SophiApp, вы должны быть единственным вошедшим пользователем с правами администратора на ПК;
  • Правильная работоспособность программы гарантируется лишь при использовании оригинального образа ОС. SophiApp может не работать на сломанных сборках Windows;
  • Некоторые функции зависят от доступа в интернет. При отсутствии последнего соответствующие функции будут скрыты в UI до тех пор, пока не появится доступ;
  • Вы можете включить скрытые функции в UI, включив «Расширенные настройки» в Настройках программы. Скрытые функции будут помечены соответствующей шестеренкой;
  • После закрытия SophiApp будет автоматически создан лог-файл, который можно прикрепить, если возникла проблема, чтобы помочь нам понять, что пошло не так.

Сторонние библиотеки

Передаю слово Дмитрию, который и написал SophiApp.

Здравствуйте. У меня нет какого-то опыта в написании статей, но постараюсь описать, как мы пришли к идее принципа работы SophiApp в том виде, в котором она сейчас и работает. Жаль, что нельзя, как математики в древней Индии, просто дать ссылку на папку с кодом на GitHub и подписать: "Смотри!". В общем по мере возможности я буду приводить примеры кода, того, как реализован тот или иной аспект работы программы.

В сумме SophiApp мной переписывалась 5 раз с нуля, и каждый раз я был готов бросить сию затею, так как нервы дороже. Но, как видите, мы живы, и программа работает. :)

Мое перманентное состояние в начале разработки

В начале этой истории мне почему-то казалось, что максимально правильно будет не использовать сторонние библиотеки UI, а делать все средствами WPF. К тому же дизайн программы не подразумевал стандартных элементов UI — все должно быть нестандартно, стильно и вызывать wow-эффект. И вообще в программе не используются какие-то особые хаки или ноу-хау. Все достаточно приземленно выглядит.

Пришлось с нуля изучать стилизацию и анимации в WPF. Простой переключатель (switch) я делал неделю, перечитав множество тем на StackOverflow десятилетней давности и испортив тонны кода. А когда он заработал — Дмитрий «обрадовал« меня тем, что все элементы должны поддерживать две темы: тёмную и светлую, и темы должны иметь возможность переключаться »на лету».

Премного благодарен
Премного благодарен

Количество идей и функций, которые нам хотелось сделать, всё нарастало, код становился сложнее: одни методы конфликтовали с другими, что-то работало стабильно, что-то все время ломалось — давал о себе знать человеческий фактор. С каждым днём этот хаос нарастал как лавина и этому не было конца. Конечно, мы быстро достигли того момента, когда любое изменение ломало приложение уже на этапе компиляции.

Думаю, если бы я не пришел к пониманию, что нужна поддержка шаблона MVVM, то еще бы долго и упорно копал не туда. Сначала я очень скептически отнесся к этой идее: "Зачем усложнять и без того сложный код?". Но после тестового приложения все встало на свои места, и я снова переписал SophiApp, добавив поддержку MVVM и заодно RelayCommand для элементов интерфейса. Самое лучшее в коде — это то, что его всегда можно переписать!

This is fine.

Для того чтобы проверить, как будет выглядеть программа «вживую», я сделал специальный дебаг-билд, который считывал из главного JSON-файла названия, описания и тип элементов, и отрисовывал их в интерфейсе. Этот файл послужил основой для текущей версии SophiApp. В папке рядом с исполняемым файлом лежал JSON-файл, и при редактировании последнего без перекомпиляции исполняемого файла программа отрисовывала новые элементы интерфейса, их тип и описание к заголовкам и кнопкам. Это надолго заняло Дмитрия созданием и редактированием текста на русском и английском языках, заодно обогатив его жизненный опыт тонкостями различий между флажком (checkbox) и радиокнопкой (radiobutton) и т. п. Как оказалось, емко и грамотно сформулировать описание ко всем функциям достаточно тяжело, не скатываясь в популизм и не опускаясь до уровня фраз и словечек вроде «выпилить телеметрию«, «бессовестное поведение Microsoft«, »шпионский модуль», »назойливый Центр безопасности» и прочего, не относящегося к работе Windows. Кстати, все фразы — выдержки из реальных программ.

Так как в приложении много текста, его нужно было как-то хранить. Выбор ожидаемо пал на JSON. JSON — это модно и молодежно, думали мы. Так оно и есть, когда его используют рационально, у нас получился огромный файл, где хранятся все локализации, описания и заголовки ко всем функциям. Огромное преимущество в использовании такого подхода было в том, что его удобно парсить — хоть тем же PowerShell, но про минусы мы узнали, когда нам стали предлагать переводы интерфейса SophiApp. И тут стало понятно, что никого палкой не заставишь вписывать сотни новых строк в этот огромный файл! Выход виделся лишь один: искусственно разбить единый файл на множество маленьких файлов под каждую локализацию, чтобы человек мог перевести файл только с английской локализацией или улучшить уже существующий. Сказано — сделано.

Выбор пал на Json. NET, так как это де-факто стандарт парсинга JSON. Благодаря библиотеке, парсим и превращаем JSON в объекты:

private async Task DeserializeTextedElementsAsync() { await Task.Run(() => { var deserializedElements = JsonConvert.DeserializeObject<IEnumerable<TextedElementDto>>Encoding.UTF8.GetString(Properties.Resources.UIData)) .Where(dto => IsWindows11 ? dto.Windows11Supported : dto.Windows10Supported) .Select(dto => FabricHelper.CreateTextedElement(dto: dto, errorHandler: OnTextedElementErrorAsync, statusHandler: OnTextedElementStatusChanged, language: Localization.Language)) .OrderByDescending(element => element.ViewId); TextedElements = new ConcurrentBag<TextedElement>(deserializedElements); }); }

Все было безветренно, пока не встал вопрос: «как добавить новый перевод в главный файл». На этот раз на помощь пришел PowerShell. В данном примере показывается, как можно интегрировать турецкую локализацию в основной JSON-файл.

# Compare 2 JSONs and merge them into one Remove-TypeData System.Array -ErrorAction Ignore $Parameters = @{ Uri = "https://raw.githubusercontent.com/Sophia-Community/SophiApp/master/SophiApp/SophiApp/Resources/UIData.json" UseBasicParsing = $true } $Full = Invoke-RestMethod @Parameters $Parameters = @{ Uri = "https://raw.githubusercontent.com/Sophia-Community/SophiApp/master/SophiApp/SophiApp/Localizations/UIData_TR.json" UseBasicParsing = $true } $Translation = Invoke-RestMethod @Parameters # In this case we add Turkish translation $ID = "TR" $Full | ForEach-Object -Process { $UiData = $_ $Data = $Translation | Where-Object -FilterScript {$_.Id -eq $UiData.Id} $UiData.Header | Add-Member -Name $ID -MemberType NoteProperty -Value $Data.Header.$ID -Force $UiData.Description | Add-Member -Name $ID -MemberType NoteProperty -Value $Data.Description.$ID -Force if ($UiData.ChildElements) { $UiData.ChildElements | ForEach-Object -Process { $UiChild = $_ $Child = $Data.ChildElements | Where-Object -FilterScript {$_.Id -eq $UiChild.Id} $UiChild.ChildHeader | Add-Member -Name $ID -MemberType NoteProperty -Value $Child.ChildHeader.$ID -Force $UiChild.ChildDescription | Add-Member -Name $ID -MemberType NoteProperty -Value $Child.ChildDescription.$ID -Force } } } ConvertTo-Json -InputObject $Full -Depth 4 | ForEach-Object -Process {$_.Replace("\u0027", "'")} | Set-Content -Path "D:\3.json" -Encoding UTF8 -Force # Re-save in the UTF-8 without BOM encoding due to JSON must not has the BOM: https://datatracker.ietf.org/doc/html/rfc8259#section-8.1 Set-Content -Value (New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false).GetBytes($(Get-Content -Path "D:\3.json" -Raw)) -Encoding Byte -Path "D:\3.json" -Force

Хранение всех данных в таком виде позволяет на лету менять язык программы без ее перезапуска — это, конечно, победа, но оборотная сторона такого подхода — крайнее усложнение добавления новых локализаций.

После этих изменений в разработке образовалась — так любимая всеми нами — стабильность. Добавлялись новые фичи, фиксились и вносились баги. Появились люди, которые стали скачивать и запускать тестовые билды, делясь своими предложениями и впечатлениями. После нескольких сеансов общения далеко за полночь по Москве с кем-то из другого полушария стало понятно, что нужно фиксировать, как и с какими данными работает приложение. Другими словами, нужен был лог работы программы.

Пример лога (сайт не дает полотно вставить)

Без создания логов человеческих ресурсов не хватало для вычленения багов. А после добавления логирования можно было просто сказать: «Пришлите лог работы программы», — и идти спокойно спать (нет лога — нет фикса), предвкушая завтрашний дебаг. С логом жить стало лучше, жить стало веселее!

Нельзя обойти стороной и PVS-Studio, который помогает мне найти ошибки в логике работы. Да, их было немного (ведь и проект в сухих цифрах не самый большой), но то ли я стал писать со временем лучше, то ли что-то еще повлияло. Первый раз, когда я натравил PVS-Studio на проект, она нашла в сумме около 30 предупреждений, которые и были исправлены. Многие из этих предупреждений были совсем не очевидны для меня.

После того, как основные функции приложения были готовы и более-менее заработали, оказалось, что найти даже в одной категории нужный твик не так просто, если их много. Захотелось функции поиска. :) Были идеи сделать файлы индекса для каждой локализации и искать в них, динамически подгружая, но пока мы ограничились обычным перебором названий и описаний элементов, использовав функцию Contains.

private async void SearchClickedAsync(object arg) { await Task.Run(() => { var stopwatch = Stopwatch.StartNew(); var searchString = arg as string; FoundTextedElement.Clear(); Search = SearchState.Running; SetVisibleViewTag(Tags.ViewSearch); FoundTextedElement = TextedElements.Where(element => element.Status != ElementStatus.DISABLED && element.ContainsText(searchString)) .ToList(); Search = SearchState.Stopped; stopwatch.Stop(); DebugHelper.StopSearch(searchString, stopwatch.Elapsed.TotalSeconds, foundTextedElement.Count); }); }

С ростом функционала я стал понимать, что очень много действий строится на одной парадигме. Потому было решено написать парочку так называемых хелперов по автоматизации однотипных действий. Так появилась папка Helpers, да и не особо уже парочка хелперов там. Они записывают данные, начиная от реестра и заканчивая проверкой на здоровье Microsoft Defender.

Так как в программе достаточно часто исправляются ошибки, то было принято решение при запуске делать проверку через интернет на наличие новой версии и блокировать интерфейс при наличии нового релиза. Это избавит в теории пользователей от возможности наткнуться на баг, который уже был исправлен.

В облаке хранится JSON-файл, который и парсится. Есть две ветки: стабильная и бета-версия. При компиляции через GitHub Actions считывается тип релиза (release или pre-release) и через скрипт в файл AppHelper. cs записывается свойство билда «private const bool IS_release» $true или $false. В зависимости от этого при запуске программа читает то или иное свойство файла в облаке, чтобы определить наличие новой версии.

internal class NewVersionCondition : IStartupCondition { public bool HasProblem { get; set; } public ConditionsTag Tag { get; set; } = ConditionsTag.NewVersion; public bool Invoke() { DebugHelper.IsOnline(); try { if (HttpHelper.IsOnline) { HttpWebRequest request = WebRequest.CreateHttp(AppHelper.SophiAppVersionsJson); request.UserAgent = AppHelper.UserAgent; var response = request.GetResponse(); using (Stream dataStream = response.GetResponseStream()) { StreamReader reader = new StreamReader(dataStream); var serverResponse = reader.ReadToEnd(); var release = JsonConvert.DeserializeObject<ReleaseDto>(serverResponse); DebugHelper.HasUpdateRelease(release); var releasedVersion = new Version(AppHelper.IsRelease ? release.SophiApp_release : release.SophiApp_pre_release); var hasNewVersion = releasedVersion > AppHelper.Version; if (hasNewVersion) { DebugHelper.IsNewRelease(); ToastHelper.ShowUpdateToast(currentVersion: $"{AppHelper.Version}", newVersion: $"{releasedVersion}"); } else { DebugHelper.UpdateNotNecessary(); } return HasProblem = hasNewVersion; } } return HasProblem; } catch (WebException e) { DebugHelper.HasException("An error occurred while checking for an update", e); return HasProblem; } catch (Exception e) { throw new Exception(e.Message.Replace(":", null)); } } }

А при нахождении всплывет вот такой милый тост с уведомлением:

SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11

SophiApp поддерживает динамически-генерируемый список установленных UWP-приложений, подгружая их локализованные имена и беря соответствующую иконку приложения, — ничего не захардкожено. Такое умеют лишь SophiApp и O&O AppBuster от O&O Software. Но эту функцию надо будет тоже как-нибудь переписать, так как она далеко не быстро работает.

SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11
SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11

В интерфейсе есть тумблер для переключения, в какой области удалять пакеты: в области пользователя или для всей ОС. Тогда все новые создаваемые пользователи не получат удаленные пакеты.

Сайт не дает вставить полотно.

При генерации списка, чтобы случайно не сломать Windows, кроме системных пакетов, в исключение попадают следующие пакеты:

Microsoft.DesktopAppInstaller, Microsoft.StorePurchaseApp, Microsoft.WindowsNotepad, Microsoft.WindowsStore, Microsoft.WindowsTerminal, Microsoft.WindowsTerminalPreview, Microsoft.WebMediaExtensions, Microsoft.AV1VideoExtension, Microsoft.HEVCVideoExtension

У каждого элемента в приложении есть состояние: включен или выключен. При запуске все элементы проверяют свое состояние и отрисовывают его в UI, показывая реальное состояние каждой функции в Windows. Да, для этого пришлось написать и отладить 130 функций для проверки состояния каждого элемента. И это не всегда быстро, так как WMI еще живее всех живых.

После применения какой-либо функции идет повторное считывание всех функций (и проставление галочек с радиокнопками соответственно), мягкий перезапуск переменных, панели задач, меню «Пуск» и отправка команды по обновлению Рабочего стола (F5). Кто в комментариях напишет, для какой функции это надо, получит зачет автоматом.: D

private const int WM_SETTINGCHANGE = 0x1a; private const int SMTO_ABORTIFHUNG = 0x0002; private const string TRAY_SETTINGS = "TraySettings"; private static readonly IntPtr hWnd = new IntPtr(65535); private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); // Virtual key ID of the F5 in File Explorer private static readonly UIntPtr UIntPtr = new UIntPtr(41504); public static void PostMessage() => PostMessageW(hWnd, Msg, UIntPtr, IntPtr.Zero); public static void RefreshEnvironment() { // Update Desktop Icons SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero); // Update Environment Variables SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, null, SMTO_ABORTIFHUNG, 100, IntPtr.Zero); // Update Taskbar SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, TRAY_SETTINGS); // Update Start Menu ProcessHelper.Stop(START_MENU_PROCESS); }

Код логики элемента интерфейса (сайт не дает вставить плотно):

SophiApp умеет создавать специализированные задания в Планировщике заданий, чтобы помочь пользователю автоматизировать рутину запуска очистки диска с предзаготовленным набором флагов.

internal const string _700_CLEANUP_TASK_ARGS = @"-WindowStyle Hidden -Command Get-Process -Name cleanmgr | Stop-Process -Force Get-Process -Name Dism | Stop-Process -Force Get-Process -Name DismHost | Stop-Process -Force $ProcessInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo $ProcessInfo.FileName = """"""$env:SystemRoot\system32\cleanmgr.exe"""""" $ProcessInfo.Arguments = """"""/sagerun:1337"""""" $ProcessInfo.UseShellExecute = $true $ProcessInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Minimized $Process = New-Object -TypeName System.Diagnostics.Process $Process.StartInfo = $ProcessInfo $Process.Start() | Out-Null Start-Sleep -Seconds 3 [int]$SourceMainWindowHandle = (Get-Process -Name cleanmgr | Where-Object -FilterScript {$_.PriorityClass -eq """"""BelowNormal""""""}).MainWindowHandle function MinimizeWindow { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $Process ) $ShowWindowAsync = @{ Namespace = """"""WinAPI"""""" Name = """"""Win32ShowWindowAsync"""""" Language = """"""CSharp"""""" MemberDefinition = @' [DllImport(""""""user32.dll"""""")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow) '@ } if (-not(""""""WinAPI.Win32ShowWindowAsync"""""" -as [type])) { Add-Type @ShowWindowAsync } $MainWindowHandle = (Get-Process -Name $Process | Where-Object -FilterScript {$_.PriorityClass -eq """"""BelowNormal""""""}).MainWindowHandle [WinAPI.Win32ShowWindowAsync]::ShowWindowAsync($MainWindowHandle, 2) } while ($true) { [int]$CurrentMainWindowHandle = (Get-Process -Name cleanmgr | Where-Object -FilterScript {$_.PriorityClass -eq """"""BelowNormal""""""}).MainWindowHandle if ($SourceMainWindowHandle -ne $CurrentMainWindowHandle) { MinimizeWindow -Process cleanmgr break } Start-Sleep -Milliseconds 5 } $ProcessInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo $ProcessInfo.FileName = """"""$env:SystemRoot\system32\dism.exe"""""" $ProcessInfo.Arguments = """"""/Online /English /Cleanup-Image /StartComponentCleanup /NoRestart"""""" $ProcessInfo.UseShellExecute = $true $ProcessInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Minimized $Process = New-Object -TypeName System.Diagnostics.Process $Process.StartInfo = $ProcessInfo $Process.Start() | Out-Null"; internal const string _700_CLEANUP_TOAST_TASK_ARGS = @"-WindowStyle Hidden -Command [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null [xml]$ToastTemplate = @"""""" <toast duration=""""""Long"""""" scenario=""""""reminder""""""> <visual> <binding template = """"""ToastGeneric"""""" > <text>*</text> <group> <subgroup> <text hint-style=""""""title"""""" hint-wrap=""""""true"""""">*</text> </subgroup> </group> <group> <subgroup> <text hint-style=""""""body"""""" hint-wrap=""""""true"""""">*</text> </subgroup> </group> </binding> </visual> <audio src=""""""ms-winsoundevent:notification.default""""""/> <actions> <input id=""""""SnoozeTimer"""""" type=""""""selection"""""" title=""""""*"""""" defaultInput=""""""1""""""> <selection id=""""""1"""""" content=""""""*"""""" /> <selection id=""""""30"""""" content=""""""*"""""" /> <selection id=""""""240"""""" content=""""""*"""""" /> </input> <action activationType=""""""system"""""" arguments=""""""snooze"""""" hint-inputId=""""""SnoozeTimer"""""" content="""""""""""" id=""""""test-snooze""""""/> <action arguments=""""""WindowsCleanup:"""""" content=""""""*"""""" activationType=""""""protocol""""""/> <action arguments=""""""dismiss"""""" content="""""""""""" activationType=""""""system""""""/> </actions> </toast> """"""@ $ToastXml = [Windows.Data.Xml.Dom.XmlDocument]::New() $ToastXml.LoadXml($ToastTemplate.OuterXml) $ToastMessage = [Windows.UI.Notifications.ToastNotification]::New($ToastXML) [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(""""""windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel"""""").Show($ToastMessage)";

При триггере, например, задания по очистке временных файлов пользователь увидит, как откроется специально написанное окно с предложением запустить очистку Windows, а также удалить неиспользованные обновления (замененные). В интерактивном тосте пользователь может отложить запуск на разные интервалы времени, отменить вовсе или запустить. При запуске начнет работать cleanmgr. exe, а затем dism. exe /Online /English /Cleanup-Image /StartComponentCleanup /NoRestart, что позволяет автоматизировать столь рутинное занятие. Аналогичные тосты всплывают при очистке папки временных файлов и папки %SystemRoot% \SoftwareDistribution\Download с той лишь разницей, что задание для очистки последней папки будет ждать, когда остановится служба Центра обновлений Windows, чтобы случайно не удалить уже скачанные пакеты обновления, предназначенные для установки.

SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11
SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11
SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11

Некоторые функции рассчитаны на настоящих любителей экзотики, потому было принято решение скрыть их по умолчанию. К таким, например, относится функция по переносу папки% TEMP% официальным образом в корень диска C:\.

Вишенкой креатива Дмитрия стала идея сделать несколько функций, но не показывать их в интерфейсе. Мол, это потенциально опасные для пользователя функции, и ему не нужно про них знать. На мой резонный вопрос: «Зачем?", — он парировал: »Программировай!". К счастью, WPF очень гибкий, да и я к тому времени уже не впадал в шок от его идей, хотя часто не представлял, как это сделать.

SophiApp, или Как мы делали опенсорс программу для настройки Windows 10 & 11
public bool AdvancedSettingsVisibility { get => advancedSettingsVisibility; set { advancedSettingsVisibility = value; DebugHelper.AdvancedSettinsVisibility(value); OnPropertyChanged(AdvancedSettingsVisibilityPropertyName); } } private void AdvancedSettingsClicked(object args) => AdvancedSettingsVisibility = AdvancedSettingsVisibility.Invert();

В 2020 году встал вопрос об автоматизации компиляции. Легче сказать, чем сделать. В это же время GitHub аккурат выкатил свой Actions, и после множества сломанных копий при релизе триггерится Action, беря версию тэга релиза и записывает перед компиляцией в файл AssemblyInfo. cs, чтобы в заголовке программы была видна версия. Дальше идет непосредственно компиляция с выкачиванием последних версий зависимостей из своих репозиториев, архивация в ZIP-архив и автоматическая загрузка готового архива на страницу релиза. На опенсорсе, конечно, свет не сошелся, но для успокоения пользователей мы решили добавить в сборку шаг получения хэш-суммы собранного архива.

cloc творит чудеса!
cloc творит чудеса!

Также через Actions идет подсчет количества строк кода в репозитории. При пуше или пулл реквесте триггерится Action, который выкачивает cloc. Дальше через GitHub-секрет в gist Дмитрия записывается JSON-файл с данными по количеству строк кода в репозитории. В данном случае записалось «message":"25.1k». В дальнейшем этот JSON отдается бэйджику от shields. io, который и рендерит уже красивую зеленую плашку.

Ну, и пару слов о логотипе. Перед новогодним релизом, когда было уже не так стыдно показать работающий билд SophiApp, Владимир посоветовал Наталью как фриланс-дизайнера, которая и нарисовала текущий логотип, за что ей и спасибо.

Такую разработку не дай бог каждому!

Выводы и планы на будущее

Выводы напрашиваются, собственно, сами собой:

  • Надо здраво рассчитывать свои силы, если берешься за такой проект;
  • Четко прорабатывать техническое задание, чтобы знать объем работ;
  • Уметь терпеть и не бросать все на полпути;
  • И вообще счастье — в преодолении.: -)

Отдельное спасибо Дмитрию за то, что он прошел этот путь со мной от начала и до конца. Ну, и еще кое-что: мечтайте осторожно — мечты сбываются, ведь Дмитрий после 20 лет работы системным администратором решился сменить профессию, подавшись в разработчики. Сейчас он работает в крупной российской девелоперской компании. Если бы не мой комментарий, ничто бы в нашей жизни не изменилось.

Если вам интересны новости ИТ и технологий из первоисточников на английском, можете подписаться на мой новостной канал Sophia News, обсудить их в чате Sophia Chat, где можно задать вопросы по SophiApp, Sophia Script, ПК, ОС и прочим темам про ИТ.

Все баги и пожелания можете оставлять здесь, в Discord, в чате Telegram-группы или создайте Issue на GitHub.

Спасибо, что пережили с нами еще раз разработку SophiApp!

P. S. Спасибо за правки и редактуру текста DoubleSharp и Инне Пристягиной из PVS-Studio.

Жизнь с SophiApp
141141
58 комментариев

Комментарий недоступен

19

Можно, конечно. Пройтись по тем же кнопкам и выключить. Безоткатных вещей нет. Это не твикер, который ломает ОС.

8

Комментарий недоступен

18

коммент по дистрибутивам епта

4