65.9K
CodeProject 正在变化。 阅读更多。
Home

远程控制您的 Linux 家庭服务器。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (5投票s)

2010 年 2 月 17 日

CPOL

7分钟阅读

viewsIcon

25561

downloadIcon

579

使用简单的托盘图标即可将其打开和关闭。

引言

在家中,我最近使用最新版本的 Ubuntu 设置了一个 Ubuntu 家庭服务器,但不想让它一直运行。但是,每次需要它时,我都不想亲自去访问它。因此,我结合了多种不同的技术来构建我自己的托盘图标来控制应用程序。

本文总结了我控制家庭服务器所需的三种不同方法

  • 网络唤醒
  • FreeNX 和 NoMachine NX 客户端
  • 通过 C# 进行 SSH 连接

您可以在下方看到托盘图标的上下文菜单。该应用程序仅能执行这些操作。其中没有 Windows 窗体。这一切都是关于控制家庭服务器(名为 Majestix):打开它、关闭它、重启它以及打开远程会话。

Menu2.gif

除了这些基本功能之外,我还配置了机器每天凌晨 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 类还包含一些用于控制线程的方法:StartWorkerStopWorker

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:在对文章的第一个评论之后,我向文章添加了一些代码,对我的工作进行了一些注释,并添加了在第一个版本中因错误而未提供的源代码文件。
© . All rights reserved.