Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ jobs:
- name: Package ${{ matrix.platform }}
run: python .\utils\post-build.py .\bin\${{ matrix.platform }}\Release\output . ${{ env.VERSION }}

- name: Verify packaged translations from different launch directories
shell: pwsh
run: |
dotnet build .\utils\SatelliteResourceProbe\SatelliteResourceProbe.csproj -c Release
.\utils\SatelliteResourceProbe\Run-Probe.ps1 -SourceBuild ".\bin\${{ matrix.platform }}\Release\DS4Windows"

- name: Build standard installer
shell: pwsh
run: |
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ jobs:
if ($dispatch -and -not $unsignedRc) {
throw 'Draft dispatch is limited to named RC prereleases; other releases retain the signed published-event path.'
}
if ($dispatch) {
# Saving a new draft does not create its Git tag. Materialize only
# a missing tag, from the explicitly selected dispatch branch.
$refs = @(gh api "repos/$env:GITHUB_REPOSITORY/git/matching-refs/tags/$tag" | ConvertFrom-Json)
$exact = @($refs | Where-Object { $_.ref -ceq "refs/tags/$tag" })
if ($exact.Count -eq 0) {
$target = gh api "repos/$env:GITHUB_REPOSITORY/commits/$($release.target_commitish)" | ConvertFrom-Json
if ($target.sha -cne $env:GITHUB_SHA) {
throw 'A new draft tag must target the same commit as the selected workflow branch.'
}
gh api --method POST "repos/$env:GITHUB_REPOSITORY/git/refs" `
-f "ref=refs/tags/$tag" -f "sha=$($target.sha)"
}
}
"tag=$tag" >> $env:GITHUB_OUTPUT
"release_id=$($release.id)" >> $env:GITHUB_OUTPUT
"unsigned_rc=$($unsignedRc.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
Expand Down Expand Up @@ -174,6 +188,11 @@ jobs:
- name: Post-Build script X64
shell: pwsh
run: python .\utils\post-build.py .\bin\x64\Release\output . $env:VERSION
- name: Verify packaged translations from different launch directories
shell: pwsh
run: |
dotnet build .\utils\SatelliteResourceProbe\SatelliteResourceProbe.csproj -c Release
.\utils\SatelliteResourceProbe\Run-Probe.ps1 -SourceBuild ".\bin\x64\Release\DS4Windows"
- name: Build standard installer X64
shell: pwsh
env:
Expand Down
7 changes: 3 additions & 4 deletions DS4Windows/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
DS4Windows
Copyright (C) 2023 Travis Nickles

Expand Down Expand Up @@ -416,9 +416,8 @@ private void ApplicationStartupCore(object sender, StartupEventArgs e)
logger.Info($"DS4Windows version {version}");
logger.Info($"DS4Windows exe file: {DS4Windows.Global.exeFileName}");
logger.Info($"DS4Windows Assembly Architecture: {(Environment.Is64BitProcess ? "x64" : "x86")}");
logger.Info($"OS Version: {Environment.OSVersion}");
logger.Info($"OS Product Name: {DS4Windows.Util.GetOSProductName()}");
logger.Info($"OS Release ID: {DS4Windows.Util.GetOSReleaseId()}");
foreach (string line in DS4Windows.WindowsVersionInfo.Read().LogLines)
logger.Info(line);
logger.Info($"System Architecture: {(Environment.Is64BitOperatingSystem ? "x64" : "x86")}");
logger.Info("Logger created");
if (DS4Windows.PortableLabContext.Current is { } lab)
Expand Down
48 changes: 23 additions & 25 deletions DS4Windows/DS4Control/Viiper/ViiperSetupManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2611,7 +2611,7 @@ private static (bool Safe, string Message)
}

return (true,
"The loaded usbip-win2 0.9.7.7 driver files match the " +
"The registered usbip-win2 0.9.7.7 driver files match the " +
"verified signed package.");
}
catch (Exception ex)
Expand All @@ -2622,41 +2622,39 @@ private static (bool Safe, string Message)
}
}

private static bool TryGetSystemDriverSha256(string serviceName,
out string sha256, out string error)
private static string ReadSystemDriverImagePath(string serviceName)
{
// Win32_SystemDriver depends on WMI provider startup. At logon its
// two-second enumeration timeout can expire even for intact drivers.
// Read the same registered ImagePath directly; do not fall back to a
// guessed filename, cached hash, or the bundled copy.
using RegistryKey service = Registry.LocalMachine.OpenSubKey(
@"SYSTEM\CurrentControlSet\Services\" + serviceName);
if (service == null) return null;
return service.GetValue("ImagePath", string.Empty,
RegistryValueOptions.DoNotExpandEnvironmentNames) as string
?? string.Empty;
}

internal static bool TryGetSystemDriverSha256(string serviceName,
out string sha256, out string error,
Func<string, string> readImagePath = null)
{
sha256 = null;
error = null;
string pathName = null;
int matches = 0;

using ManagementObjectSearcher searcher = new(
"SELECT PathName FROM Win32_SystemDriver " +
$"WHERE Name='{serviceName}'");
searcher.Options = CreateDependencyQueryOptions();
using ManagementObjectCollection drivers = searcher.Get();
foreach (ManagementObject driver in drivers)
{
using (driver)
{
matches++;
pathName = driver["PathName"] as string;
}
}
string pathName = (readImagePath ?? ReadSystemDriverImagePath)(serviceName);

if (matches != 1)
if (pathName == null)
{
error = matches == 0
? $"the {serviceName} service is missing"
: $"multiple {serviceName} services were returned";
error = $"the {serviceName} service is missing";
return false;
}

string driverPath = ResolveSystemDriverPath(pathName);
if (string.IsNullOrWhiteSpace(driverPath) ||
!File.Exists(driverPath))
{
error = $"the active {serviceName} driver file is missing";
error = $"the registered {serviceName} driver file is missing";
return false;
}

Expand All @@ -2666,7 +2664,7 @@ private static bool TryGetSystemDriverSha256(string serviceName,
return true;
}

private static string ResolveSystemDriverPath(string pathName)
internal static string ResolveSystemDriverPath(string pathName)
{
if (string.IsNullOrWhiteSpace(pathName))
{
Expand Down
58 changes: 58 additions & 0 deletions DS4Windows/DS4Control/WindowsVersionInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Globalization;
using Microsoft.Win32;

namespace DS4Windows;

internal sealed record WindowsVersionInfo(string ProductName, string DisplayVersion,
string Build, string KernelVersion)
{
internal string[] LogLines => new[]
{
$"OS Version: {ProductName} {DisplayVersion} (Build {Build})",
$"OS Kernel Version: {KernelVersion}",
};

internal static WindowsVersionInfo Read()
{
Version kernel = Environment.OSVersion.Version;
try
{
using RegistryKey machine = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
using RegistryKey key = machine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
int build = int.TryParse(key?.GetValue("CurrentBuildNumber") as string,
NumberStyles.None, CultureInfo.InvariantCulture, out int parsed) ? parsed : kernel.Build;
int? revision = key?.GetValue("UBR") is int ubr && ubr >= 0 ? ubr : null;
return Create(key?.GetValue("ProductName") as string,
key?.GetValue("DisplayVersion") as string, key?.GetValue("ReleaseId") as string,
key?.GetValue("InstallationType") as string, build, revision, kernel);
}
catch
{
// Diagnostics must not prevent startup when registry access fails.
return Create(null, null, null, null, kernel.Build, null, kernel);
}
}

internal static WindowsVersionInfo Create(string productName, string displayVersion,
string releaseId, string installationType, int build, int? revision, Version kernel)
{
string product = string.IsNullOrWhiteSpace(productName) ? "Windows" : productName;
// Windows 11 retains the legacy Windows 10 product string on many PCs.
// Restrict normalization to client editions; server build numbers overlap.
if (build >= 22000 && product.StartsWith("Windows 10", StringComparison.OrdinalIgnoreCase) &&
(string.IsNullOrEmpty(installationType) ||
string.Equals(installationType, "Client", StringComparison.OrdinalIgnoreCase)))
product = "Windows 11" + product.Substring("Windows 10".Length);

string display = !string.IsNullOrWhiteSpace(displayVersion) ? displayVersion :
!string.IsNullOrWhiteSpace(releaseId) ? releaseId : "version unavailable";
string buildText = build.ToString(CultureInfo.InvariantCulture) +
(revision.HasValue ? "." + revision.Value.ToString(CultureInfo.InvariantCulture) :
" (revision unavailable)");
string kernelText = $"{kernel.Major}.{kernel.Minor}.{build}" +
(revision.HasValue ? "." + revision.Value.ToString(CultureInfo.InvariantCulture) : "");
return new(product, display, buildText, kernelText);
}
}
88 changes: 88 additions & 0 deletions DS4Windows/DS4Forms/Converters/TrayIconConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Data;

namespace DS4WinWPF.DS4Forms.Converters;

public sealed class TrayIconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not string resource || string.IsNullOrEmpty(resource)) return null;
using Stream stream = Application.GetResourceStream(new Uri(resource, UriKind.RelativeOrAbsolute)).Stream;
return TrayIconLoader.Load(stream, TrayIconLoader.GetPixelSize());
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}

internal static class TrayIconLoader
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindow(string className, string windowName);
[DllImport("user32.dll")]
private static extern uint GetDpiForWindow(IntPtr window);
[DllImport("user32.dll")]
private static extern uint GetDpiForSystem();
[DllImport("user32.dll")]
private static extern int GetSystemMetricsForDpi(int index, uint dpi);

internal static int GetPixelSize()
{
// The tray belongs to Explorer's taskbar, which may be on another monitor.
uint dpi = GetDpiForWindow(FindWindow("Shell_TrayWnd", null));
if (dpi == 0) dpi = GetDpiForSystem();
return Math.Max(16, GetSystemMetricsForDpi(49 /* SM_CXSMICON */, dpi));
}

internal static Icon Load(Stream source, int pixelSize)
{
if (pixelSize <= 0) throw new ArgumentOutOfRangeException(nameof(pixelSize));
using var bytes = new MemoryStream();
source.CopyTo(bytes);
byte[] data = bytes.ToArray();
using var reader = new BinaryReader(new MemoryStream(data));
if (reader.ReadUInt16() != 0 || reader.ReadUInt16() != 1)
throw new InvalidDataException("Invalid tray icon header.");
int count = reader.ReadUInt16();
int selected = -1, selectedSize = 0, selectedDepth = 0;
for (int index = 0; index < count; index++)
{
int offset = 6 + index * 16;
if (offset + 16 > data.Length) throw new InvalidDataException("Truncated tray icon directory.");
int width = data[offset] == 0 ? 256 : data[offset];
int height = data[offset + 1] == 0 ? 256 : data[offset + 1];
if (width != height) continue;
int depth = BitConverter.ToUInt16(data, offset + 6);
// Prefer an exact size, otherwise downsample the next larger frame.
// A smaller frame is used only when no sufficiently large one exists.
bool better = selected < 0 ||
(width >= pixelSize && (selectedSize < pixelSize || width < selectedSize)) ||
(width < pixelSize && selectedSize < pixelSize && width > selectedSize) ||
(width == selectedSize && depth > selectedDepth);
if (better) { selected = offset; selectedSize = width; selectedDepth = depth; }
}
if (selected < 0) throw new InvalidDataException("Tray icon has no square frames.");
uint length = BitConverter.ToUInt32(data, selected + 8);
uint start = BitConverter.ToUInt32(data, selected + 12);
if (length == 0 || start < 6 + count * 16 || (ulong)start + length > (ulong)data.Length)
throw new InvalidDataException("Invalid tray icon frame.");
using var frame = new MemoryStream();
using (var writer = new BinaryWriter(frame, System.Text.Encoding.UTF8, leaveOpen: true))
{
writer.Write((ushort)0);
writer.Write((ushort)1);
writer.Write((ushort)1);
writer.Write(data, selected, 12);
writer.Write((uint)22);
writer.Write(data, checked((int)start), checked((int)length));
}
frame.Position = 0;
return new Icon(frame, selectedSize, selectedSize);
}
}
3 changes: 2 additions & 1 deletion DS4Windows/DS4Forms/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<local:ImageLocationPaths x:Key="ImgLocPaths" />
<localConverters:TrayIconConverter x:Key="TrayIconConverter" />
</Window.Resources>
<DockPanel>
<tb:TaskbarIcon x:Name="notifyIcon" IconSource="{Binding IconSource,Mode=OneWay}" ToolTipText="{Binding TooltipText,Mode=OneWay}" MenuActivation="RightClick" TrayRightMouseUp="NotifyIcon_TrayRightMouseUp" TrayMiddleMouseDown="NotifyIcon_TrayMiddleMouseDown" TrayMouseDoubleClick="NotifyIcon_TrayMouseDoubleClick">
<tb:TaskbarIcon x:Name="notifyIcon" Icon="{Binding IconSource,Mode=OneWay,Converter={StaticResource TrayIconConverter}}" ToolTipText="{Binding TooltipText,Mode=OneWay}" MenuActivation="RightClick" TrayRightMouseUp="NotifyIcon_TrayRightMouseUp" TrayMiddleMouseDown="NotifyIcon_TrayMiddleMouseDown" TrayMouseDoubleClick="NotifyIcon_TrayMouseDoubleClick">
<!--<tb:TaskbarIcon.ContextMenu>
<ContextMenu>
<MenuItem Header="Open" />
Expand Down
19 changes: 19 additions & 0 deletions DS4Windows/DS4Forms/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ public MainWindow(ArgumentParser parser)

// Need to define before calling TaskbarIcon.ForceCreate
notifyIcon.DataContext = trayIconVM;
Microsoft.Win32.SystemEvents.DisplaySettingsChanged += TrayDisplaySettingsChanged;
notifyIcon.CustomName = Global.exelocation;
notifyIcon.ContextMenu = trayIconVM.ContextMenu;

Expand Down Expand Up @@ -1925,6 +1926,7 @@ private void MainDS4Window_Closing(object sender, System.ComponentModel.CancelEv

private void MainDS4Window_Closed(object sender, EventArgs e)
{
Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= TrayDisplaySettingsChanged;
logAutoScroller?.Dispose();
DisposePowerLifecycle();
CancelBoundedHotplugRecovery();
Expand All @@ -1939,13 +1941,30 @@ private void MainDS4Window_Closed(object sender, EventArgs e)
if (notifyIcon != null)
{
trayIconVM?.Dispose();
notifyIcon.Icon = null;
notifyIcon.Dispose();
notifyIcon = null;
}

Application.Current.Shutdown();
}

private void TrayDisplaySettingsChanged(object sender, EventArgs e)
{
if (Dispatcher.HasShutdownStarted) return;
Dispatcher.BeginInvoke(new Action(() =>
{
if (notifyIcon == null || Volatile.Read(ref shutdownRequested) != 0) return;
notifyIcon.GetBindingExpression(H.NotifyIcon.TaskbarIcon.IconProperty)?.UpdateTarget();
}));
}

protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
{
base.OnDpiChanged(oldDpi, newDpi);
TrayDisplaySettingsChanged(this, EventArgs.Empty);
}

protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
Expand Down
Loading