├── UPS-AutoHelper
├── Assets
│ ├── tray.ico
│ └── tray.png
├── _mac_publish
│ ├── tray.icns
│ ├── dotnet_publish_cmd.txt
│ ├── macapp.sh
│ └── Info.plist
├── _win_publish
│ ├── inno.iss
│ └── dotnet_publish_cmd.txt
├── MainWindow.axaml
├── app.manifest
├── Program.cs
├── TickWindow.axaml
├── App.axaml
├── Common
│ ├── ExecuteCommandBase.cs
│ ├── TcpConnect.cs
│ └── Common.cs
├── UPS-AutoHelper.csproj
├── Model
│ └── SettingModel.cs
├── SettingWindow.axaml
├── App.axaml.cs
├── TickWindow.axaml.cs
├── SettingWindow.axaml.cs
└── MainWindow.axaml.cs
├── .gitignore
├── README.md
└── NAS-AutoHelper.sln.DotSettings.user
/UPS-AutoHelper/Assets/tray.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlueSkyCaps/UPS-AutoHelper/HEAD/UPS-AutoHelper/Assets/tray.ico
--------------------------------------------------------------------------------
/UPS-AutoHelper/Assets/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlueSkyCaps/UPS-AutoHelper/HEAD/UPS-AutoHelper/Assets/tray.png
--------------------------------------------------------------------------------
/UPS-AutoHelper/_mac_publish/tray.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlueSkyCaps/UPS-AutoHelper/HEAD/UPS-AutoHelper/_mac_publish/tray.icns
--------------------------------------------------------------------------------
/UPS-AutoHelper/_win_publish/inno.iss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BlueSkyCaps/UPS-AutoHelper/HEAD/UPS-AutoHelper/_win_publish/inno.iss
--------------------------------------------------------------------------------
/UPS-AutoHelper/_win_publish/dotnet_publish_cmd.txt:
--------------------------------------------------------------------------------
1 | dotnet publish -r win-x64 --configuration Release -p:UseAppHost=true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true --self-contained true -p:AssemblyName=UPS自动关机
2 | dotnet publish -r win-arm64 --configuration Release -p:UseAppHost=true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true --self-contained true -p:AssemblyName=UPS自动关机
3 |
4 | # dotnet编译自包含运行时的可执行文件,然后可以用anno setup脚本打包exe安装包
5 |
6 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/_mac_publish/dotnet_publish_cmd.txt:
--------------------------------------------------------------------------------
1 | dotnet publish -r osx-arm64 --configuration Release -p:UseAppHost=true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true --self-contained true -p:AssemblyName=UPS自动关机
2 | dotnet publish -r osx-x64 --configuration Release -p:UseAppHost=true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true --self-contained true -p:AssemblyName=UPS自动关机
3 |
4 | # dotnet编译自包含运行时的可执行文件,用sh脚本生成app包
5 | # 还可用packages进行安装包打包,执行安装后脚本postinstall进行登录项添加等逻辑
6 |
7 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ################################################################################
2 | # 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
3 | ################################################################################
4 |
5 | /.vs
6 | /NAS-AutoHelper/bin/Debug/net8.0
7 | /NAS-AutoHelper/obj
8 | /NAS-AutoHelper/.vs/NAS-AutoHelper/FileContentIndex/ae6563ae-fd59-46e8-ae8f-36649e4d5b37.vsidx
9 | /NAS-AutoHelper/.vs/NAS-AutoHelper/v17/.suo
10 | /NAS-AutoHelper/.vs/NAS-AutoHelper
11 | /UPS-AutoHelper/obj
12 | /UPS-AutoHelper/.vs
13 | /UPS-AutoHelper/bin/Debug/net8.0
14 | /UPS-AutoHelper/bin/Release/net8.0
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Power by [Avalonia](https://github.com/avaloniaui/avalonia)
2 | #### 一款自动关机程序。常驻后台检测网络状态。若你的电脑断网,则会自动关机,防止意外断电造成设备损害。若你的UPS没有提供Windows或macOS的自动关机软件,这个是简单的代替品。
3 | ##### 本项目代码不多,主要涉及Avalonia而已。但是全套都有,学了你就会掌握:
4 | - Mac/Windows跨平台桌面UI,一套代码运行三端(Linux没编译过)
5 | - 以窗体的方式隐藏,并且在后台一直运行
6 | - 能够在Mac上显示菜单栏图标/Windows上显示托盘 进行操作
7 | - Mac/Windows开机自启动
8 | - 单例运行(不允许同时运行多个此程序)
9 | - 教你打包安装包:Mac打包成可执行文件app + pkg格式安装包;Windows打包成exe格式安装包
10 | #### An automatic shutdown program. Permanently detect the network status in the background. If your computer disconnects the network, it will automatically shut down to prevent accidental power outage causing damage to the equipment. If your UPS does not provide automatic shutdown software for Windows or macOS, this is a simple substitute.
11 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/_mac_publish/macapp.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | APP_NAME="./UPS自动关机.app"
3 | # 替换成dotnet编译好的目录
4 | PUBLISH_OUTPUT_DIRECTORY="../bin/Release/net8.0/osx-arm64/publish/."
5 | # PUBLISH_OUTPUT_DIRECTORY should point to the output directory of your dotnet publish command.
6 | # One example is /path/to/your/csproj/bin/Release/netcoreapp3.1/osx-x64/publish/.
7 | # If you want to change output directories, add `--output /my/directory/path` to your `dotnet publish` command.
8 | INFO_PLIST="./Info.plist"
9 | ICON_FILE="./tray.icns"
10 | ICON_FILE_NAME="tray.icns"
11 |
12 | if [ -d "$APP_NAME" ]
13 | then
14 | rm -rf "$APP_NAME"
15 | fi
16 |
17 | mkdir "$APP_NAME"
18 |
19 | mkdir "$APP_NAME/Contents"
20 | mkdir "$APP_NAME/Contents/MacOS"
21 | mkdir "$APP_NAME/Contents/Resources"
22 |
23 | cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
24 | cp "$ICON_FILE" "$APP_NAME/Contents/Resources/$ICON_FILE_NAME"
25 | cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS"
--------------------------------------------------------------------------------
/UPS-AutoHelper/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Avalonia.Threading;
7 | using MsBox.Avalonia;
8 | using MsBox.Avalonia.Enums;
9 |
10 | namespace UPS_AutoHelper;
11 | class Program
12 | {
13 | // Initialization code. Don't use any Avalonia, third-party APIs or any
14 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
15 | // yet and stuff might break.
16 | [STAThread]
17 | public static void Main(string[] args)
18 | {
19 | BuildAvaloniaApp()
20 | .StartWithClassicDesktopLifetime(args);
21 | }
22 |
23 | // Avalonia configuration, don't remove; also used by visual designer.
24 | public static AppBuilder BuildAvaloniaApp()
25 | {
26 | return AppBuilder.Configure()
27 | .UsePlatformDetect()
28 | .WithInterFont()
29 | .With(new MacOSPlatformOptions() { ShowInDock = false})
30 | .LogToTrace();
31 | }
32 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/_mac_publish/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | CFBundleName
7 | UPS自动关机
8 | CFBundleDisplayName
9 | UPS自动关机
10 | CFBundleIdentifier
11 | top.reminisce.autohelper-ups
12 | CFBundleVersion
13 | 1.0.0
14 | CFBundleShortVersionString
15 | 版本1.0.0
16 | NSHighResolutionCapable
17 |
18 | CFBundleExecutable
19 | UPS自动关机
20 |
21 | CFBundleIconFile
22 | tray.icns
23 |
24 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/TickWindow.axaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/App.axaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/Common/ExecuteCommandBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Runtime.InteropServices;
4 |
5 | namespace UPS_AutoHelper;
6 |
7 | public class ExecuteCommandBase
8 | {
9 | private ExecuteCommandBase()
10 | {
11 |
12 | }
13 |
14 | public static ExecuteCommandBase Instance { get; } = new();
15 |
16 | ///
17 | /// 无头模式打开shell执行命令 兼容各个操作系统
18 | ///
19 | ///
20 | ///
21 | public (string output, string error) ExecuteCommand(string command)
22 | {
23 | // 创建一个新的进程对象
24 | using Process process = new Process();
25 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
26 | {
27 | process.StartInfo.FileName = "cmd.exe";
28 | process.StartInfo.Arguments =$"/C {command}";
29 | }
30 |
31 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
32 | {
33 | process.StartInfo.FileName = "/bin/zsh"; // 使用 zsh 来执行命令
34 | process.StartInfo.Arguments = $"-c \"{command}\""; // 使用 -c "" 参数传递命令
35 | }
36 | process.StartInfo.RedirectStandardOutput = true; // 重定向标准输出
37 | process.StartInfo.RedirectStandardError = true; // 重定向错误输出
38 | process.StartInfo.UseShellExecute = false; // 禁用外壳程序执行
39 | process.StartInfo.CreateNoWindow = true; // 不创建新窗口
40 |
41 | // 启动进程并等待完成
42 | process.Start();
43 | process.WaitForExit(); // 等待进程结束
44 | string output = process.StandardOutput.ReadToEnd(); // 读取标准输出
45 | string error = process.StandardError.ReadToEnd(); // 读取标准错误
46 | return (output, error);
47 | }
48 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/UPS-AutoHelper.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net8.0
5 | enable
6 | true
7 | true
8 | app.manifest
9 | true
10 | osx-arm64;osx-x64;linux-x64;win-x64;win-arm64
11 |
12 |
13 |
14 |
15 |
16 | Always
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | None
26 | All
27 |
28 |
29 |
30 |
31 |
32 | Always
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/Model/SettingModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel;
3 | using System.Runtime.CompilerServices;
4 |
5 | namespace UPS_AutoHelper.Model;
6 |
7 | public class SettingModel:INotifyPropertyChanged
8 | {
9 | private string _pingHost;
10 | private string _macPassword;
11 | private bool _already = false;
12 | private uint _pingTickSeconds = 20;
13 | private uint _countdownSeconds = 60;
14 |
15 | public string PingHost
16 | {
17 | get => _pingHost;
18 | set
19 | {
20 | if (value == _pingHost) return;
21 | _pingHost = value;
22 | OnPropertyChanged();
23 | }
24 | }
25 |
26 | ///
27 | /// Mac系统需要征得获取密码
28 | ///
29 | public string MacPassword
30 | {
31 | get => _macPassword;
32 | set
33 | {
34 | if (value == _macPassword) return;
35 | _macPassword = value;
36 | OnPropertyChanged();
37 | }
38 | }
39 |
40 | ///
41 | /// 是否第一次打开填写过设置
42 | ///
43 | public bool Already
44 | {
45 | get => _already;
46 | set
47 | {
48 | if (value == _already) return;
49 | _already = value;
50 | OnPropertyChanged();
51 | }
52 | }
53 |
54 | ///
55 | /// 设置检测网络的时间间隔
56 | ///
57 | public uint PingTickSeconds
58 | {
59 | get => _pingTickSeconds;
60 | set
61 | {
62 | if (value == _pingTickSeconds) return;
63 | _pingTickSeconds = value;
64 | OnPropertyChanged();
65 | }
66 | }
67 |
68 | ///
69 | /// 设置倒计时关机的秒数
70 | ///
71 | public uint CountdownSeconds
72 | {
73 | get => _countdownSeconds;
74 | set
75 | {
76 | if (value == _countdownSeconds) return;
77 | _countdownSeconds = value;
78 | OnPropertyChanged();
79 | }
80 | }
81 |
82 | public event PropertyChangedEventHandler? PropertyChanged;
83 |
84 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
85 | {
86 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/SettingWindow.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/UPS-AutoHelper/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Controls.ApplicationLifetimes;
8 | using Avalonia.Markup.Xaml;
9 | using Avalonia.Threading;
10 | using MsBox.Avalonia;
11 |
12 | namespace UPS_AutoHelper;
13 |
14 | public partial class App : Application
15 | {
16 | public override void Initialize()
17 | {
18 | AvaloniaXamlLoader.Load(this);
19 | }
20 |
21 | public override void OnFrameworkInitializationCompleted()
22 | {
23 |
24 | InitSetting();
25 | }
26 |
27 | ///
28 | /// 初始化加载配置等
29 | ///
30 | ///
31 | private void InitSetting()
32 | {
33 | // 加载配置文件
34 | if (!Path.Exists(Common.UserDataPath))
35 | {
36 | // 无配置文件,则是首次打开:弹出设置窗口!
37 | SettingWindow settingWindow = new SettingWindow();
38 | settingWindow.Show();
39 | }
40 | else
41 | {
42 | var settingText = File.ReadAllText(Common.UserDataPath);
43 | MainWindow.Setting = System.Text.Json.JsonSerializer.Deserialize(settingText);
44 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
45 | {
46 | desktop.MainWindow = new MainWindow();
47 | }
48 | base.OnFrameworkInitializationCompleted();
49 | }
50 | }
51 |
52 | private void ExitItem_OnClick(object? sender, EventArgs e)
53 | {
54 | Environment.Exit(0);
55 | }
56 |
57 | private void SettingItem_OnClick(object? sender, EventArgs e)
58 | {
59 | new SettingWindow().Show();
60 | }
61 |
62 | private void AboutItem_OnClick(object? sender, EventArgs e)
63 | {
64 | Dispatcher.UIThread.InvokeAsync( () =>
65 | {
66 | string msg = "此应用常驻后台,若你的电脑断网,则会自动关机,防止意外断电造成设备损害。\n" +
67 | "若你的UPS没有提供Windows或macOS的自动关机程序,这是简单的代替品。\n" +
68 | "可在桌面 [菜单栏/托盘] 中点击应用图标进行操作。\n" +
69 | "如果你无法找到应用图标,请在 [系统设置] 里让它重新显示,然后重启电脑。\n" +
70 | "小红书/抖音/B站关注@芝士虾米。";
71 | var box = MessageBoxManager
72 | .GetMessageBoxStandard("提示", msg);
73 | box.ShowAsync();
74 | });
75 | }
76 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/TickWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Avalonia;
4 | using Avalonia.Controls;
5 | using Avalonia.Interactivity;
6 | using Avalonia.Markup.Xaml;
7 | using Avalonia.Threading;
8 |
9 | namespace UPS_AutoHelper;
10 |
11 | public partial class TickWindow : Window
12 | {
13 | private Timer? _timer;
14 | public StyledProperty SecondsProperty =
15 | AvaloniaProperty.Register(nameof(Seconds), defaultValue: MainWindow.Setting.CountdownSeconds);
16 | public uint Seconds
17 | {
18 | get => GetValue(SecondsProperty);
19 | set => SetValue(SecondsProperty, value);
20 | }
21 |
22 | public TickWindow()
23 | {
24 | InitializeComponent();
25 | // 初始化倒计时的UI计时器⏱️
26 | InitTimer();
27 | }
28 |
29 | private void InitTimer()
30 | {
31 | _timer = new Timer(Tick, null, 1000, 1000); // 每秒钟触发一次Tick方法
32 | }
33 |
34 | private void Tick(object? state)
35 | {
36 | Dispatcher.UIThread.InvokeAsync(() =>
37 | {
38 | if (Seconds > 0)
39 | {
40 | Seconds -= 1;
41 | }
42 | else
43 | {
44 | QuitUi();
45 | }
46 | });
47 |
48 |
49 | }
50 |
51 | private void QuitUi()
52 | {
53 | _timer?.Dispose(); // 停止计时器
54 | Console.WriteLine("QuitUi");
55 | CancelBtn.IsEnabled = false;
56 | // System.Environment.Exit(0);
57 | }
58 |
59 | protected override void OnOpened(EventArgs e)
60 | {
61 | Console.WriteLine();
62 | base.OnOpened(e);
63 | }
64 |
65 | ///
66 | /// 点击了关机撤销按钮
67 | ///
68 | ///
69 | ///
70 | private void CancelBtn_OnClick(object? sender, RoutedEventArgs e)
71 | {
72 | _timer?.Dispose(); // 停止计时器
73 | CancelBtn.IsEnabled = false;
74 | Dispatcher.UIThread.InvokeAsync(() =>
75 | {
76 | CancelBtn.Content = "已阻止关机。";
77 | });
78 |
79 | // 访问主窗口,停止倒计时关机的Timer、重启网络监测
80 | MainWindow.InitNetLogic();
81 | }
82 |
83 | ///
84 | /// 当窗口关闭时,阻止
85 | ///
86 | ///
87 | ///
88 | private void Window_OnClosing(object? sender, WindowClosingEventArgs e)
89 | {
90 | // 没有点击撤销按钮,阻止关闭。只有点击了撤销按钮,才可以关闭
91 | if (CancelBtn.IsEnabled)
92 | {
93 | e.Cancel = true;
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/Common/TcpConnect.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Sockets;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace UPS_AutoHelper;
8 |
9 | public class TcpConnect
10 | {
11 | private void InitTcpConnect()
12 | {
13 | BuildTcpServer();
14 | SendTcpServerMessage();
15 | }
16 |
17 | ///
18 | /// 发送数据到本地TCP服务
19 | ///
20 | private static void SendTcpServerMessage()
21 | {
22 | string token = "@UPS!ShutdownCanceled";
23 | string ipAddress = "127.0.0.1";
24 | int port = 12345; // 服务端的端口号
25 |
26 | try
27 | {
28 | using var tcpClient = new TcpClient(ipAddress, port);
29 | // 获取网络流
30 | NetworkStream networkStream = tcpClient.GetStream();
31 | // 将消息转换为字节数组
32 | var data = Encoding.UTF8.GetBytes(token);
33 |
34 | // 发送数据
35 | networkStream.Write(data, 0, data.Length);
36 | // 关闭 客户端
37 | networkStream.Close();
38 | tcpClient.Close();
39 | }
40 | catch (Exception e)
41 | {
42 | Console.WriteLine("send不过 "+ e);
43 | throw;
44 | }
45 | }
46 |
47 | ///
48 | /// 构建本地TCP连接服务端 监听后续请求
49 | ///
50 | private static void BuildTcpServer()
51 | {
52 | Task.Run(() =>
53 | {
54 | try
55 | {
56 | int serverPort = 12345; // 监听的端口号
57 | // 创建 TcpListener 实例并开始监听
58 | var tcpListener = new TcpListener(IPAddress.Loopback, serverPort);
59 | tcpListener.Start();
60 | Console.WriteLine("Server is listening...");
61 |
62 | // 接受客户端连接
63 | TcpClient tcpClient = tcpListener.AcceptTcpClient();
64 | Console.WriteLine("Client connected.");
65 | // 获取网络流
66 | NetworkStream networkStream = tcpClient.GetStream();
67 |
68 | // 读取数据
69 | var buffer = new byte[1024]; // 创建一个缓冲区来存放接收到的数据
70 | var bytesRead = networkStream.Read(buffer, 0, buffer.Length);
71 | var receivedMessage = Encoding.UTF8.GetString(buffer);
72 | Console.WriteLine($"Received message: {receivedMessage}");
73 |
74 | // 关闭连接
75 | networkStream.Close();
76 | tcpClient.Close();
77 | tcpListener.Stop();
78 | }
79 | catch (Exception e)
80 | {
81 | Console.WriteLine("的bug++ "+e);
82 | }
83 |
84 | });
85 | }
86 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/SettingWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.InteropServices;
4 | using System.Threading.Tasks;
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Interactivity;
8 | using Avalonia.Markup.Xaml;
9 | using Avalonia.Threading;
10 | using MsBox.Avalonia;
11 | using UPS_AutoHelper.Model;
12 |
13 | namespace UPS_AutoHelper;
14 |
15 | public partial class SettingWindow : Window
16 | {
17 | public bool MacPanelVis{get; set; }=RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
18 | public SettingWindow()
19 | {
20 | this.DataContext = MainWindow.Setting;
21 | InitializeComponent();
22 | }
23 |
24 | private void CancelBtn_OnClick(object? sender, RoutedEventArgs e)
25 | {
26 | this.Close();
27 | }
28 |
29 | private async void OkBtn_OnClick(object? sender, RoutedEventArgs e)
30 | {
31 | var sTbPingHost = TbPingHost.Text?.Trim();
32 | var sTbPingTickSeconds = TbPingTickSeconds.Value;
33 | var sTbCountdownSeconds = TbCountdownSeconds.Value;
34 | var sTbMacPassword = TbMacPassword.Text;
35 | if (!Common.IsValidIPv4(sTbPingHost))
36 | {
37 | string msg = "无效的IPv4地址";
38 | await Dispatcher.UIThread.InvokeAsync(async () =>
39 | {
40 | var box = MessageBoxManager
41 | .GetMessageBoxStandard("提示", msg);
42 | await box.ShowWindowDialogAsync(this);
43 | });
44 | return;
45 | }
46 | if ( sTbPingTickSeconds < 10)
47 | {
48 | string msg = "检测间隔(秒):不少于10秒";
49 | await Dispatcher.UIThread.InvokeAsync(async () =>
50 | {
51 | var box = MessageBoxManager
52 | .GetMessageBoxStandard("提示", msg);
53 | await box.ShowWindowDialogAsync(this);
54 | });
55 | return;
56 | }
57 | if (sTbCountdownSeconds<0)
58 | {
59 | string msg = "关机倒计时(秒):请输入数字";
60 | await Dispatcher.UIThread.InvokeAsync(async () =>
61 | {
62 | var box = MessageBoxManager
63 | .GetMessageBoxStandard("提示", msg);
64 | await box.ShowWindowDialogAsync(this);
65 | });
66 | return;
67 | }
68 | // Mac需要密码
69 | if (MacPanelVis)
70 | {
71 | if (string.IsNullOrEmpty(TbMacPassword.Text))
72 | {
73 | string msg = "您确定你的锁屏密码是空吗?!\n" +
74 | "由于macOS的安全机制,执行关机权限需要获得您的登录密码(非AppleID)。\n" +
75 | "您必须去[系统设置]里设置有效的登录密码才能使用此应用!";
76 | await Dispatcher.UIThread.InvokeAsync(async () =>
77 | {
78 | var box = MessageBoxManager
79 | .GetMessageBoxStandard("提示", msg);
80 | await box.ShowWindowDialogAsync(this);
81 | });
82 | return;
83 | }
84 | }
85 | MainWindow.Setting.PingHost = sTbPingHost;
86 | MainWindow.Setting.PingTickSeconds = Convert.ToUInt32(sTbPingTickSeconds);
87 | MainWindow.Setting.CountdownSeconds = Convert.ToUInt32(sTbCountdownSeconds);
88 | MainWindow.Setting.MacPassword = sTbMacPassword;
89 |
90 | // 更新配置文件
91 | try
92 | {
93 | var serialize = System.Text.Json.JsonSerializer.Serialize(MainWindow.Setting);
94 | if (!File.Exists(Common.UserDataPath))
95 | {
96 | // 不存在则创建,表示首次打开应用!并且初始化主窗口
97 | Directory.CreateDirectory(Common.UserDataDir);
98 | var toWrite = File.CreateText(Common.UserDataPath);
99 | await toWrite.WriteAsync(serialize);
100 | toWrite.Close();
101 | new MainWindow().Show();
102 | }
103 | else
104 | {
105 | // 覆盖更新,表示是通过菜单点击的设置
106 | await File.WriteAllTextAsync(Common.UserDataPath, serialize);
107 | // 重新初始化主窗口逻辑 更新时间间隔
108 | MainWindow.InitNetLogic();
109 | }
110 | }
111 | catch(Exception ex)
112 | {
113 | await Dispatcher.UIThread.InvokeAsync(async () =>
114 | {
115 | var box = MessageBoxManager
116 | .GetMessageBoxStandard("致命错误", ""+ex.Message);
117 | await box.ShowWindowDialogAsync(this);
118 | Environment.Exit(1);
119 | });
120 | }
121 | this.Close();
122 | }
123 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/Common/Common.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Net.NetworkInformation;
5 | using System.Runtime.InteropServices;
6 | using Microsoft.VisualBasic;
7 |
8 | namespace UPS_AutoHelper;
9 |
10 | public static class Common
11 | {
12 | public static readonly string UserDataDir = Path.Combine(
13 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".autoHelper-ups");
14 | public static readonly string UserDataPath = Path.Combine(UserDataDir, "Settings.json");
15 |
16 | public static bool IsValidIPv4(string ipAddress)
17 | {
18 | if (string.IsNullOrWhiteSpace(ipAddress))
19 | return false;
20 |
21 | var segments = ipAddress.Split('.');
22 | if (segments.Length != 4)
23 | return false;
24 |
25 | foreach (var segment in segments)
26 | {
27 | if (!int.TryParse(segment, out int value) || value < 0 || value > 255)
28 | return false;
29 | }
30 |
31 | return true;
32 | }
33 | ///
34 | /// 执行一个ping命令
35 | ///
36 | ///
37 | ///
38 | ///
39 | public static Status ExecutePing(string? command=null)
40 | {
41 | if (command == null)
42 | {
43 | command = $"ping -n 1 -w 3000 {MainWindow.Setting.PingHost}";
44 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
45 | {
46 | command = $"ping -c1 -t3 {MainWindow.Setting.PingHost}";
47 | }
48 | }
49 | Status status = new();
50 | try
51 | {
52 | var (output, error) = ExecuteCommandBase.Instance.ExecuteCommand(command);
53 |
54 | // 如果错误流有数据,则断开:No route to host
55 |
56 | if (!string.IsNullOrWhiteSpace(error))
57 | {
58 | throw new Exception("路由器/网线/光猫/宽带与本设备断开连接 - "+error);
59 | }
60 | /*
61 | * macOS:
62 | * 但是存在这种情况:路由器仍在正常连接,但无法接入互联网:光猫/宽带/路由器无网络源
63 | * 这种情况是不会输出到错误流的,需要判断字符串是否包含:0 packets received
64 | */
65 | if (!string.IsNullOrWhiteSpace(output))
66 | {
67 | if (output.ToLower().Contains("0 packets received"))
68 | {
69 | throw new Exception("路由器/网线/光猫/宽带无网络源 - 0 packets received");
70 | }
71 | // windows还存在这种情况:已断网无法访问目标主机。但是错误流没有数据
72 | if (output.ToLower().Contains("无法访问")|| output.ToLower().Contains("传输失败"))
73 | {
74 | throw new Exception("路由器/网线/光猫/宽带无网络源 - 无法访问目标主机");
75 | }
76 | }
77 |
78 | status.Message = output;
79 | status.Success = true;
80 | return status; // 返回标准输出
81 | }
82 | catch (Exception e)
83 | {
84 | status.Message = e.Message;
85 | status.Success = false;
86 | return status;
87 | }
88 | }
89 |
90 | ///
91 | /// 执行关机命令
92 | ///
93 | ///
94 | public static Status ExecuteShutdown(string? command=null)
95 | {
96 | if (command == null)
97 | {
98 | command = "shutdown /s /f /t 0";
99 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
100 | {
101 | command = $"echo \"{MainWindow.Setting.MacPassword}\" | sudo -S shutdown -h now";
102 | }
103 | }
104 | Status status = new();
105 | try
106 | {
107 | (string output, string error) = ExecuteCommandBase.Instance
108 | .ExecuteCommand(command);
109 | // 如果错误流有数据
110 | if (!string.IsNullOrWhiteSpace(error))
111 | {
112 | /*
113 | 但是存在这种情况:mac执行echo sudo命令进行密码的自动填充,即使密码正确
114 | 但是仍会把“Password:”截断输出到错误流的,因此需要进一步判断
115 | */
116 | if (! (error.ToLower().TrimStart().StartsWith("password") && ! error.ToLower().Contains("try again")))
117 | {
118 | throw new Exception("密码有误,请尝试同步你的密码。"+error);
119 | }
120 | }
121 | // 此处正常早已关机 程序早已终止
122 | status.Message = output;
123 | status.Success = true;
124 | return status;
125 | }
126 | catch (Exception e)
127 | {
128 | status.Message = e.Message;
129 | status.Success = false;
130 | return status;
131 | }
132 | }
133 | }
134 |
135 | public class Status
136 | {
137 | public bool Success { get; set; }
138 | public int Code { get; set; }
139 | public string? Message { get; set; }
140 | }
--------------------------------------------------------------------------------
/UPS-AutoHelper/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Threading;
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Threading;
8 | using MsBox.Avalonia;
9 | using MsBox.Avalonia.Enums;
10 | using UPS_AutoHelper.Model;
11 |
12 | namespace UPS_AutoHelper;
13 |
14 | ///
15 | /// 在后台一直隐藏运行 无法正常关闭的主窗口
16 | ///
17 | public partial class MainWindow : Window
18 | {
19 | private static Timer? _mainPingTimer;
20 | private static Timer? _shutdownTimer;
21 | public static SettingModel Setting { get; set; } = new();
22 | public MainWindow()
23 | {
24 | // 确保当前只有一个此应用实例在运行
25 | var processName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;
26 | if (System.Diagnostics.Process.GetProcessesByName(processName).Length>1)
27 | {
28 | Console.WriteLine(processName);
29 | Dispatcher.UIThread.InvokeAsync(async () =>
30 | {
31 | string msg = "已有一个UPS自动关机应用正在运行,无法重复启动。\n" +
32 | "可在桌面 [菜单栏/托盘] 中点击应用图标进行操作。\n" +
33 | "如果你无法找到应用图标,请在 [系统设置] 里让它重新显示,然后重启电脑。";
34 | var box = MessageBoxManager
35 | .GetMessageBoxStandard("提示", msg);
36 | var y = await box.ShowAsync();
37 | // 到这一步 即使点击了是,程序也不会被退出。因为主窗口守护已经创建了。只能等待提示框被点击后主动退出当前程序
38 | Environment.Exit(0);
39 | });
40 | // 直接return 不让他执行后续的初始化逻辑
41 | return;
42 | }
43 | // 显示托盘
44 | TrayIcon.GetIcons(Application.Current).FirstOrDefault().IsVisible = true;
45 | InitializeComponent();
46 | // 开始网络监测
47 | InitNetLogic();
48 |
49 | }
50 |
51 | ///
52 | /// 启动网络监测: 当检测到断网时停止、当点击撤销关机按钮后可被再次调用启动
53 | ///
54 | ///
55 | public static void InitNetLogic()
56 | {
57 | Console.WriteLine("InitNetLogic");
58 | // 无论如何,先关闭关机的Timer。即使第一次它还没被创建。用于后续点击撤销关机按钮时统一关闭
59 | _shutdownTimer?.Dispose();
60 | _mainPingTimer?.Dispose();
61 | // 启动:每倒计时秒钟触发一次Tick方法 ping路由
62 | _mainPingTimer = new Timer(PingTickAction, null, TimeSpan.FromSeconds(Setting.PingTickSeconds), TimeSpan.FromSeconds(Setting.PingTickSeconds));
63 | }
64 |
65 | private static void PingTickAction(object? state)
66 | {
67 | // 先暂停网络监测 避免执行重复 等到当前的ping操作执行完毕,根据结果再判断是否重启监测
68 | _mainPingTimer?.Dispose();
69 | // ping 光猫IP是最直接有效的
70 | var status = Common.ExecutePing();
71 | Console.WriteLine(status.Message);
72 | if (! status.Success)
73 | {
74 | // 断网 执行倒计时关机
75 | Console.WriteLine("网络未连接");
76 | // 启动UI倒计时提示界面
77 | Dispatcher.UIThread.InvokeAsync(() =>
78 | {
79 | new TickWindow().Show();
80 | });
81 | // X秒后执行的关机timer,无限期间隔等待意味它只会执行一次
82 | _shutdownTimer = new Timer(ShutdownOnceTickAction,null, TimeSpan.FromSeconds(Setting.CountdownSeconds), Timeout.InfiniteTimeSpan);
83 | }
84 | else
85 | {
86 | // 正常,立即重启网络监测Timer
87 | InitNetLogic();
88 | }
89 | }
90 |
91 | private static void ShutdownOnceTickAction(object? state)
92 | {
93 |
94 | Console.WriteLine("开始执行关机 ShutdownOnceTickAction");
95 | // Mac 使用 sudo 执行关机命令,需要管理员权限。windows无需
96 | var status = Common.ExecuteShutdown();
97 | if (status.Success)
98 | {
99 | Console.WriteLine("关机执行成功,返回数据:"+status.Message);
100 | }
101 | else
102 | {
103 | Console.WriteLine("执行关机失败 ShutdownOnceTickAction"+status.Message);
104 | _mainPingTimer?.Dispose();
105 | Dispatcher.UIThread.InvokeAsync(async () =>
106 | {
107 | // 执行关机命令失败 弹出提示
108 | string msg = "当您看到此信息,意味着您的设备已经过断电断网,但是我们无法正常关机。\n" +
109 | "请查看您的电源是否电量不足,若是,请立即手动关机以防止断电造成的设备损害。\n"+
110 | $"(错误提示:{status.Message})";
111 | var box = MessageBoxManager
112 | .GetMessageBoxStandard("UPS奔溃", msg);
113 | await box.ShowAsync();
114 | Environment.Exit(1);
115 | });
116 | }
117 | }
118 |
119 | protected override void OnOpened(EventArgs e)
120 | {
121 | this.ShowActivated = false;
122 | this.ShowInTaskbar = false;
123 | // 当打开窗口时立即隐藏主窗口 IsVisible在Mac无效
124 | this.Hide();
125 | base.OnOpened(e);
126 | }
127 |
128 | ///
129 | /// 当窗口关闭时,阻止
130 | ///
131 | ///
132 | ///
133 | private void Window_OnClosing(object? sender, WindowClosingEventArgs e)
134 | {
135 | e.Cancel = true;
136 | }
137 | }
--------------------------------------------------------------------------------
/NAS-AutoHelper.sln.DotSettings.user:
--------------------------------------------------------------------------------
1 |
2 | ForceIncluded
3 | ForceIncluded
4 | ForceIncluded
5 | ForceIncluded
6 | ForceIncluded
7 | ForceIncluded
8 | ForceIncluded
9 | ForceIncluded
10 | ForceIncluded
11 | ForceIncluded
12 | ForceIncluded
13 | ForceIncluded
14 | ForceIncluded
15 | ForceIncluded
16 | ForceIncluded
17 | ForceIncluded
18 | ForceIncluded
19 | ForceIncluded
20 | ForceIncluded
21 | ForceIncluded
22 | ForceIncluded
23 | ForceIncluded
24 | ForceIncluded
--------------------------------------------------------------------------------