远程控制您的 Linux 家庭服务器。
使用简单的托盘图标即可将其打开和关闭。
引言
在家中,我最近使用最新版本的 Ubuntu 设置了一个 Ubuntu 家庭服务器,但不想让它一直运行。但是,每次需要它时,我都不想亲自去访问它。因此,我结合了多种不同的技术来构建我自己的托盘图标来控制应用程序。
本文总结了我控制家庭服务器所需的三种不同方法
- 网络唤醒
- FreeNX 和 NoMachine NX 客户端
- 通过 C# 进行 SSH 连接
您可以在下方看到托盘图标的上下文菜单。该应用程序仅能执行这些操作。其中没有 Windows 窗体。这一切都是关于控制家庭服务器(名为 Majestix):打开它、关闭它、重启它以及打开远程会话。
除了这些基本功能之外,我还配置了机器每天凌晨 2 点自动关机。这就是子菜单的作用:您可以打开和关闭自动挂起功能。
托盘图标本身显示服务器的当前状态
- 红色:不可用
- 绿色:可用
- 蓝色:尚未初始化
- 黄色:正在工作,或等待服务器状态更改
您可以在此处找到所有源代码:源代码 - 133 KB。
现在让我们深入了解细节。
应用程序结构
只使用了两个结构组件
- program.cs:负责控制托盘图标。所有用户操作都由此类处理。通常,它会调用 BusinessLogic.cs 中的方法。
- BusinessLogic.cs:包含执行实际工作的方法,并使用一些辅助方法来完成工作。
关于基本结构,您只需要了解这些。
检查服务器状态
服务器状态的检查在 Pinger.cs 中完成。此类部署一个工作线程,每 10 秒 ping 一次服务器。一旦服务器状态发生变化,“HostStatusChanged
”事件就会被触发。然后 program.cs 会使用此事件来更新托盘图标的颜色。它还包含一个名为“GetNervous
”的方法,该方法将 ping 间隔限制为 1 秒。它用于在用户操作(如“唤醒”或“挂起”)后更快地检测服务器更改。
您可以在下面找到工作线程的核心(“Work
”方法用作 ThreadStart
)
private void Work()
{
while (RunWorker)
{
Ping ping = new Ping();
PingReply reply = ping.Send(Address);
HostStatus newHostStatus = reply.Status == IPStatus.Success
? HostStatus.Online
: HostStatus.Offline;
if (newHostStatus != CurrentHostStatus)
{
CurrentHostStatus = newHostStatus;
if (HostStatusChanged != null)
{
HostStatusChanged(CurrentHostStatus);
}
}
try
{
if (DateTime.Now < NervousUntil)
{
Thread.Sleep(NERVOUS_SLEEP);
}
else
{
Thread.Sleep(SLEEP_BETWEEN_PINGS);
}
}
catch (ThreadInterruptedException)
{
//Ignore.
}
}
}
此循环将一直运行,直到“RunWorker
”为 false
。在循环开始时,会发出一个 Ping
。根据结果,HostStatus
会发生更改并触发相应的事件。
之后,代码会决定适当的休眠时间。
Pinger
类还包含一些用于控制线程的方法:StartWorker
和 StopWorker
。
public void StartWoker()
{
lock (this)
{
Assert.IsNull(Worker, "Worker is already running");
RunWorker = true;
Worker = new Thread(Work);
Worker.Start();
}
}
StartWorker
会部署线程并让它开始工作。
public void StopWorker()
{
lock (this)
{
Assert.NotNull(Worker, "The Worker is currently not running.");
RunWorker = false;
Worker.Interrupt();
Worker.Join();
Worker = null;
}
}
在 StopWorker
中,线程会被重新连接:StopWorker
仅在工作线程退出后返回。此接口提供了对 pinger 线程的非常简单的控制:调用 StartWorker
来启动它,并在要退出应用程序时调用 StopWorker
。
在 program.cs 中使用 HostStatusChanged
事件时,会执行两个操作:让 pinger 变慢以停止频繁 ping,并更新菜单和图标状态。
static void Pinger_HostStatusChanged(Pinger.HostStatus newHostStatus)
{
Pinger.CalmDown();
UpdateMenuStatus();
}
唤醒他
服务器唤醒是通过网络唤醒 (WakeOnLan) 完成的。现代主板通常支持此功能。网络组件会一直监听并等待魔术包。您需要将数据包发送到网络的广播地址,以确保它能到达您的服务器。
用于传输数据包的方法是 BusinessLogic.WakeOnLan()
。您可以在附件中找到源代码。我从未尝试理解该方法,因为我从某处复制了它(在“随意使用和重新分发”许可下)。
挂起或重启服务器
这有点棘手,因为您需要连接到服务器并告诉它挂起。
为了实现这一点,我正在使用 SSH 连接。C# 有两种支持 SSH 连接的解决方案
SharpSSH 易于使用,但不幸的是,当连接丢失时会冻结 - 当您将机器置于待机状态时,这种情况确实会发生。因此,我无法使用此库,而不得不使用另一个库
Granados 没有这个缺点,但使用起来非常复杂。没有简单的连接和发送命令的方法,只有一个非常基本的接口。我创建了一个小的包装类,以便将所有复杂的东西隐藏在一个易于使用的接口后面。
这个简单接口的一个重要方法是“Connect
”方法。它使用 Granados 的基本类来建立链接
public void Connect(string hostname, string username, string password)
{
SSHConnectionParameter parameters = new SSHConnectionParameter();
parameters.UserName = username;
parameters.Password = password;
parameters.Protocol = SSHProtocol.SSH2;
parameters.AuthenticationType = AuthenticationType.Password;
parameters.WindowSize = 0x1000;
IPAddress ip;
try
{
ip = Dns.GetHostEntry(hostname).AddressList[0];
}
catch (SocketException e)
{
throw new HostnameResolutionException(
"Could not resolve hostname", e, hostname);
}
Socket s = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
try
{
s.Connect(new IPEndPoint(ip, 22));
}
catch (SocketException e)
{
throw new ConnectionFailedException(
"Could not connect to host.", e, hostname);
}
EventReceiver eventReceiver = new EventReceiver(this);
Debug.WriteLine("Connection starts.");
eventReceiver.Notify += InitializationListener;
BaseConnection = SSHConnection.Connect(parameters, eventReceiver, s);
Channel = BaseConnection.OpenShell(eventReceiver);
ConnectionInfo = BaseConnection.ConnectionInfo;
lock (this)
{
while (!eventReceiver.Ready && !eventReceiver.Error)
{
Monitor.Wait(this);
}
if (eventReceiver.Error)
{
throw new Exception("Unexpected " +
"exception during initialization.");
}
}
lock (this)
{
while (!Ready && !eventReceiver.Error)
{
Monitor.Wait(this);
}
if (eventReceiver.Error)
{
throw new Exception("Unexpected exception while waiting for prompt.");
}
}
eventReceiver.Notify -= InitializationListener;
MyEventReceiver = eventReceiver;
}
首先,它用参数填充一个类(也可以稍后完成)。然后,它打开用于连接的套接字。然后开始繁琐的工作。以下行需要一些解释
EventReceiver eventReceiver = new EventReceiver(this);
Debug.WriteLine("Connection starts.");
eventReceiver.Notify += InitializationListener;
BaseConnection = SSHConnection.Connect(parameters, eventReceiver, s);
Channel = BaseConnection.OpenShell(eventReceiver);
ConnectionInfo = BaseConnection.ConnectionInfo;
首先,我正在创建一个自实现的 EventReceiver
。此接收器由一组方法组成,Granados 使用它来通信连接上发生的任何有趣的事情。大多数方法仍然只包含“NotImplementedException
”,因为它们从未被调用过。
然后,我向 EventReceiver
附加了一个 InitializationListener
,当有数据到达时,它会收到通知
public void InitializationListener(string input)
{
if (input.EndsWith(":~$ "))
{
lock (this)
{
this.Ready = true;
Monitor.PulseAll(this);
}
}
}
如您所见,我只是等待从服务器传输提示符“:~ $ ”,并将“this.Ready
”设置为 true
。这不是最干净的解决方案,但它运行良好,我无法想到其他方法。
在这些步骤之后,就是创建连接本身的时间了:SSHconnection.connect
。这些行会立即返回,并且主线会进入 while 循环。它一直停留在这里,直到 InitializationListener
将“this.Ready
”设置为 true
并唤醒主线。
然后,主线中会进行一些清理工作,并且该方法以可用的有效 SSH 连接退出。
传输命令时,使用类似的模式
public string WriteCommand(string command)
{
//possible issue: Deadlock when the connection gets lost.
command += "\n";
MyEventReceiver.Notify += ReceiveCommandResult;
CommandFinished = false;
CommandResult = new StringBuilder();
Channel.Transmit(Encoding.ASCII.GetBytes(command), 0, command.Length);
lock (this)
{
while (!CommandFinished && !MyEventReceiver.Error)
{
Monitor.Wait(this);
}
}
MyEventReceiver.Notify -= ReceiveCommandResult;
string result = CommandResult.ToString();
//Remove the first and last line.
//The first line is the command itself
//The last line is the new prompt.
result = result.Substring(result.IndexOf("\r\n")+2);
result = result.Substring(0, Math.Max(result.LastIndexOf("\r\n"), 0));
return result;
}
首先,我添加了一个可以接收命令结果的监听方法。然后,我传输命令,该命令会立即返回。之后,我等待“ReceiveCommandResult
”将“CommandFinished
”设置为 true
private void ReceiveCommandResult(string data)
{
CommandResult.Append(data);
if (data.EndsWith(":~$ "))
{
lock (this)
{
CommandFinished = true;
Monitor.PulseAll(this);
}
}
}
此方法还将结果收集在一个 StringBuilder
中,然后用于将命令结果返回给“WriteCommand
”的调用者。
关于死锁问题,您可能在“WriteCommand
”的第一个注释中看到了:我的局域网从未丢失过连接,因此我还没有费心去修复这个问题。
为了传输挂起和待机命令,还有另一个方法,它只是发送命令并在发送命令后立即返回。
为了将服务器置于待机状态,SSH 连接会打开到服务器,然后会传输命令“pmi action suspend”或“sudo shutdown -r now”来挂起或重启服务器。不要忘记将您的 SSH 用户添加到 sudoers 文件中。
打开远程会话
这非常简单:在我的本地环境中,托盘图标随 NoMachineNX 客户端一起安装。然后启动此客户端 - 就是这样。我已从本文附件中删除了 NoMashine 的可执行文件,因为我无权重新分发它。
控制自动挂起
一旦设置了 SSH 连接,控制自动挂起就不再是什么难事了。想法是服务器每天凌晨 2 点自动关机。这样,我可以在需要时打开它,而不用担心再次关闭它。
对于那些需要更长时间运行的任务,我想能够禁用自动关机。
棘手的部分在 Linux 端。我创建了一个作业,每天使用 GNOME-schedule 在凌晨 2 点运行以下脚本
#!/bin/sh
date
exec < /etc/automaticShutdown.conf
read line
echo "Found the value $line in configuration"
if [ "true" = "$line" ]; then
echo "Going to suspend"
/usr/sbin/pmi action suspend
fi
如您所见,如果它在“automaticShutdown.conf”文件中找到“true”行,它就会挂起机器。因此,所有远程控制要做的事情就是通过 SSH 更改该文件中的值。它只需提交以下两个命令之一
echo "true" > /etc/automaticShutdown.conf
echo "false" > /etc/automaticShutdown.conf
就是这样。
下一步
- 目前没有从家庭网络外部控制服务器的机制。如果您在这方面有任何工作,请告知我。
- 我仍然需要一个选项,仅在某个晚上禁用自动关机。一种想法是将一个整数写入关机文件,并且每次执行关机脚本时,该数字都会递减,直到达到 0。我还没有时间来实现它。
结论
为了创建这个小应用程序,我在网上搜了很多地方,读了很多代码。希望我能让您不必花那么多时间去研究。
历史
- 0.1:最初创建。
- 0.2:在对文章的第一个评论之后,我向文章添加了一些代码,对我的工作进行了一些注释,并添加了在第一个版本中因错误而未提供的源代码文件。