diff --git a/package-debian.sh b/package-debian.sh index 0d459e84..c8e2448c 100644 --- a/package-debian.sh +++ b/package-debian.sh @@ -676,6 +676,15 @@ EOF find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} + [[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true + # Core binaries must be executable before installation: ordinary users cannot + # chmod the root-owned files installed by dpkg. + local core + for core in xray/xray sing_box/sing-box mihomo/mihomo; do + if [[ -f "$stage/opt/v2rayN/bin/$core" ]]; then + chmod 0755 "$stage/opt/v2rayN/bin/$core" + fi + done + deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb" dpkg-deb --root-owner-group --build "$stage" "$deb_out" diff --git a/v2rayN/ServiceLib.Tests/Common/UnixFileModeTests.cs b/v2rayN/ServiceLib.Tests/Common/UnixFileModeTests.cs new file mode 100644 index 00000000..ac268078 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Common/UnixFileModeTests.cs @@ -0,0 +1,42 @@ +namespace ServiceLib.Tests.Common; + +public class UnixFileModeTests +{ + [Test] + public async Task AlreadyExecutableSystemBinary_ShouldNotRequireOwnership() + { + if (OperatingSystem.IsWindows()) + { + return; + } + + // /bin/sh is executable but owned by root on the Unix CI runners. + // A regular user must not need chmod permission just to launch it. + var mode = File.GetUnixFileMode("/bin/sh"); + await Utils.SetUnixFileMode("/bin/sh").Should().BeTrue(); + await File.GetUnixFileMode("/bin/sh").Should().BeEqualTo(mode); + } + + [Test] + public async Task NonExecutableOwnedFile_ShouldGainExecuteBitsWithoutLosingOtherPermissions() + { + if (OperatingSystem.IsWindows()) + { + return; + } + + var path = Path.GetTempFileName(); + try + { + var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead; + File.SetUnixFileMode(path, mode); + + await Utils.SetUnixFileMode(path).Should().BeTrue(); + await File.GetUnixFileMode(path).Should().BeEqualTo(mode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute); + } + finally + { + File.Delete(path); + } + } +} diff --git a/v2rayN/ServiceLib.Tests/Services/SpeedtestServiceBatchTests.cs b/v2rayN/ServiceLib.Tests/Services/SpeedtestServiceBatchTests.cs new file mode 100644 index 00000000..b5cdef19 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Services/SpeedtestServiceBatchTests.cs @@ -0,0 +1,59 @@ +namespace ServiceLib.Tests.Services; + +public class SpeedtestServiceBatchTests +{ + [Test] + [Arguments(0)] + [Arguments(1000)] + public async Task EmptySelection_ShouldReturnNoBatches(int pageSize) + { + await GetBatches([], pageSize).Count.Should().BeEqualTo(0); + } + + [Test] + [Arguments(0)] + [Arguments(-1)] + public async Task NonPositivePageSize_ShouldKeepEveryEligibleProfile(int pageSize) + { + var profiles = CreateProfiles(); + var batches = GetBatches(profiles, pageSize); + + await batches.Count.Should().BeEqualTo(profiles.Count); + await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count); + foreach (var profile in profiles) + { + await batches.SelectMany(batch => batch).Count(item => ReferenceEquals(item, profile)).Should().BeEqualTo(1); + } + } + + [Test] + public async Task NormalBatches_ShouldRespectPageSizeAndKeepCoreTypesSeparate() + { + var profiles = CreateProfiles(); + var batches = GetBatches(profiles, 2); + + await batches.Count.Should().BeEqualTo(3); + await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count); + foreach (var batch in batches) + { + await (batch.Count > 0 && batch.Count <= 2).Should().BeTrue(); + await batch.Select(item => item.CoreType).Distinct().Count().Should().BeEqualTo(1); + } + } + + private static List CreateProfiles() => + [ + new() { CoreType = ECoreType.Xray }, + new() { CoreType = ECoreType.Xray }, + new() { CoreType = ECoreType.Xray }, + new() { CoreType = ECoreType.sing_box }, + new() { CoreType = ECoreType.sing_box } + ]; + + private static List> GetBatches(List profiles, int pageSize) + { + var service = new SpeedtestService(new Config { SpeedTestItem = new() }, _ => Task.CompletedTask); + var method = typeof(SpeedtestService).GetMethod("GetTestBatchItem", BindingFlags.Instance | BindingFlags.NonPublic)!; + return (List>)method.Invoke(service, [profiles, pageSize])!; + } +} diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index f1613782..393c4fe7 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -1441,7 +1441,11 @@ public class Utils if (File.Exists(fileName)) { var currentMode = File.GetUnixFileMode(fileName); - File.SetUnixFileMode(fileName, currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute); + var executableMode = currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; + if (currentMode != executableMode) + { + File.SetUnixFileMode(fileName, executableMode); + } return true; } } diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index c1a7de38..a5155589 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -587,6 +587,12 @@ public class SpeedtestService(Config config, Func updateF private List> GetTestBatchItem(List lstSelected, int pageSize) { List> lstTest = []; + if (lstSelected.Count == 0) + { + return lstTest; + } + + pageSize = Math.Max(pageSize, 1); var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList(); var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList(); diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index 5cfe417f..91895e3f 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -219,7 +219,8 @@ public partial class StatusBarViewModel : MyReactiveObject await ConfigHandler.InitBuiltinRouting(_config); await RefreshRoutingsMenu(); await InboundDisplayStatus(); - await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true); + // The view refreshes its icon on initialization; interaction handlers are not registered yet. + await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, false); BlRouting = true; }