【问题标题】:Linux display command works in terminal, but not in systemd serviceLinux display 命令在终端中有效,但在 systemd 服务中无效
【发布时间】:2015-12-13 23:00:16
【问题描述】:

我制作了一个网络应用程序来关闭我的电脑屏幕,有几种不同的技术,但很简单:

我有一个 html/js 前端,它检测到按钮单击(屏幕打开/屏幕关闭),它通过 ajax 将选项发送到 PHP 后端

php 然后通过 tcp 端口连接,将选项发送到用 golang 编写的程序

然后我的 golang 程序执行关闭/打开屏幕的命令。 它运行的命令是 ("xset -display :0 dpms force off")

我遇到的问题是该命令仅在我在终端中运行 golang 程序时才有效,但是当我将其设置为服务时,该命令将不起作用。

这是 golang 代码:

package main

import (
    "os/exec"
    "net"
    "fmt"
    "bufio"
)

func main() {
    fmt.Println("Launching server")

    ln, _ := net.Listen("tcp", ":7777")
    fmt.Println("Listening...\n")

    for {
        // accept connection on port
        conn, _ := ln.Accept()
        fmt.Println("New connection")

        // listen for message ending in \n
        message, _ := bufio.NewReader(conn).ReadString('\n')
        rec := string(message)

        // remove trailing \n
        rec = rec[:len(rec)-1]

        fmt.Println("Message Received: ", "\""+rec+"\"")

        returnMessage := "fail"

        if (rec == "screensOff") {
            fmt.Println("Turning off screens...")

            //execute screens off command
            cmd := exec.Command("xset", "-display", ":0", "dpms", "force", "off")
            stdout, err := cmd.Output()

            if err != nil {
                fmt.Println(err.Error())
            } else {
                fmt.Println(string(stdout))
                returnMessage = "done"
            }
        } else if (rec == "screensOn") {
            fmt.Println("Turning on screens...");

            //execute screens on command
            cmd := exec.Command("xset", "-display", ":0", "dpms", "force", "on")

            stdout, err := cmd.Output()
            if err != nil {
                fmt.Println(err.Error())
            } else {
                fmt.Println(string(stdout))
                returnMessage = "done"
            }
            returnMessage = "done"
        } 

        conn.Write([]byte(returnMessage + "\n"))

        conn.Close()
        fmt.Println("Connection closed\n")
    }
}

以及相关的PHP代码:

<?php
function sendServiceMessage($message) {
    $host = "localhost";
    $port = 7777;
    $timeout = 30;

    // connect to service
    $socket = fsockopen($host, $port, $errnum, $errstr, $timeout);
    if (!is_resource($socket)) {
        exit("connection fail: ".$errnum." ".$errstr);
    }
    else {
        // send message
        fputs($socket, $message."\n");

        // receive return message
        $recieved = "";
        while (!feof($socket)) {
            $recieved .= fgets ($socket, 1024);
        }
    }

    // close connection
    fclose($socket);
    if ($recieved == "done") {
        return true;
    }
    return false;   
}

sendServiceMessage("screensOff");

我使用systemd设置服务,所以在构建程序并将其放置在/usr/bin/之后

...$ go build screenControl.go
...$ sudo cp screenControl /usr/bin/screenControl

我可以在终端中运行 screenControl 程序,然后在 Web 应用程序中选择“屏幕关闭”,一切正常:

...$ screenControl
Launching server
Listening...

New Connection
Message Received:  "screensOff"
Turning off screens...

Connection closed

然后我创建了一个 systemd 单元文件(/etc/systemd/system/screenControl.service):

[Unit]
Description=Screen control service

[Service]
ExecStart=/usr/bin/screenControl
Restart=on-abort

[Install]
WantedBy=multi-user.target

我启动了服务并检查了它:

...$ systemctl start screenControl
...$ systemctl status screenControl
● screenControl.service - Screen control service
   Loaded: loaded (/etc/systemd/system/screenControl.service; disabled; vendor preset: enabled)
   Active: active (running) since Sun 2015-12-13 22:31:54 GMT; 6s ago
 Main PID: 19871 (screenControl)
   CGroup: /system.slice/screenControl.service
           └─19871 /usr/bin/screenControl

Dec 13 22:31:54 User systemd[1]: Started Screen control service.
Dec 13 22:31:54 User screenControl[19871]: Launching server
Dec 13 22:31:54 User screenControl[19871]: Listening...

所以它正在运行,但是当我现在在网络应用程序中选择屏幕关闭时,什么也没有发生...我再次检查了服务状态,它正在接收关闭屏幕的消息,但命令正在退出并出现错误:

...
Dec 13 22:31:54 User screenControlTest[19871]: Launching server
Dec 13 22:31:54 User screenControlTest[19871]: Listening...
Dec 13 22:32:25 User screenControlTest[19871]: New connection
Dec 13 22:32:25 User screenControlTest[19871]: Message Received:  "screensOff"
Dec 13 22:32:25 User screenControlTest[19871]: Turning off screens...
Dec 13 22:32:25 User screenControlTest[19871]: exit status 1
Dec 13 22:32:25 User screenControlTest[19871]: Connection closed

这里有什么问题,我怎样才能让该命令作为服务工作?一旦这个工作正常,我希望在机器打开时自动启动服务,尽管使用 systemd 我认为这很简单:

...$ systemctl enable screenControl

任何帮助都会很棒,谢谢:)

编辑

在 golang 程序显示 xset 命令的标准错误后,我现在也收到错误消息:

xset:  unable to open display ""

【问题讨论】:

  • xwindows 不允许当前会话所有者以外的用户向其发送命令。在调用您的服务之前,从 shell(如您)尝试“xhost +”。可能是root被阻止关闭屏幕。但是当您从 shell 运行命令时它可以工作,因为您拥有会话
  • 我停止了服务,尝试了“xhost +”,它说“访问控制已禁用,客户端可以从任何主机连接”然后再次启动服务,但它仍然从 xset 命令获得“退出状态 1” ://
  • 您是否生成了与退出代码 1 一起使用的实际错误消息?
  • 我玩过 go 并让它向我显示 stderr 输出:(退出状态 1:xset:无法打开显示“”)我想知道它是否像你说的那样,那root 被阻止了,虽然我可以从 root 终端运行 golang 程序并且它可以工作.. 但这是因为我从桌面用户会话启动了 root 会话(通过“sudo -i”)吗?这比我预期的要深,有趣的东西:)
  • 当它说显示“”时,这意味着它不知道哪个显示。当您从 go 执行命令行应用程序时,您可以传递环境,尝试使用 DISPLAY=":0"

标签: linux ubuntu service go systemd


【解决方案1】:

xset 命令只是 X 服务器的客户端。它通过检查 DISPLAY 环境变量来确定要与哪个 X 服务器通信,当您将命令作为系统服务运行时不会设置该变量。

即使您确保在运行守护程序时设置了DISPLAY,它也可能会以不同的用户帐户运行,并且默认情况下拒绝访问显示器。

更好的选择是将守护进程作为用户会话的一部分运行。这将解决身份验证问题(它将像您一样运行)以及定位显示器的能力(环境变量应该是可见的)。当您未登录时,该守护程序不会运行,但这对于此特定用例可能无关紧要。

您已使用“Ubuntu”标记您的问题,会话仍由Upstart 管理。您可以通过在~/.config/upstart 中创建文件来创建新的用户会话作业。文件格式的详细信息可以在init(5) man page找到。

【讨论】:

  • 我以为 Upstart 在 15.04 被 systemd 取代了? (我在 15.10 上)但是,除非我从用户会话中运行“xhost +”,否则它确实不会工作......是否可以授权 root 用户也可以访问显示?所以无论谁登录,它仍然可以工作。这样做会有什么问题吗?
  • 我注意到我可以运行xhost +SI:localuser:root 来授权root用户,所以我把它放在/etc/bash.bashrc 文件的底部为所有用户运行,希望没有什么错了:)
  • 现在它只有在登录到桌面后才能工作(这很好,如果它在登录屏幕上工作会很好,但是嗯)然后必须打开一个终端窗口...... :s,但关闭终端后它仍然有效......我什至不知道用谷歌来修复那个:s 离工作很近!
  • 通过将xhost +SI:localuser:root 放在/etc/profile 文件而不是bash.bashrc 文件的顶部来解决。感谢您的帮助:)
  • 在 15.04 之前,您将运行两个 Upstart 副本:系统一个作为 pid 1,另一个作为您的用户帐户运行以管理您的桌面登录会话。系统实例确实已被 systemd 替换,但用户会话之一仍然存在。它可能也会在某个时候被 systemd 取代,但这还没有发生。
【解决方案2】:

根据 David Budworth 的评论,修复非常简单;由于该服务在 root 下运行,它没有设置 DISPLAY 环境变量。

在 go 中,您可以在使用 exec 时设置环境变量,如下所示:

//execute screens off command
cmd := exec.Command("xset", "-display", ":0", "dpms", "force", "off")
cmd.Env = []string{"DISPLAY=:0"} // set the display before executing
stdout, stderr := cmd.CombinedOutput() //execute and return all output

从 James Henstridge 的回答中,我发现我还需要运行 xhost +SI:localuser:root 以允许 root 用户访问 X 服务器。

您可以在用户登录后将此行添加到/etc/profile 文件的顶部

xhost +SI:localuser:root > /dev/null 2>&1

即使没有用户登录(显示登录屏幕时),您也可以让它工作

首先我创建了目录/opt/scripts 然后创建文件/opt/scripts/xhost.sh 并使用chmod +x /opt/scripts/xhost.sh 赋予它可执行权限

在这个文件中只有一行:

xhost +SI:localuser:root > /dev/null 2>&1

然后编辑文件/etc/lightdm/lightdm.conf(我必须创建它,但如果它存在则编辑它)并添加该行 display-setup-script=/opt/scripts/xhost.sh

所以我的lightdm.conf 文件看起来像这样:

[SeatDefaults]
greeter-session=unity-greeter
user-session=ubuntu
display-setup-script=/opt/scripts/xhost.sh

这告诉 LightDM(在 Ubuntu 中运行的显示管理器)在 X 服务器启动之后但在其他任何事情之前运行脚本 /opt/scripts/xhost.sh,因此 root 会立即获得 xhost 授权!

注意:

display-setup-script 在 X 服务器启动之后但在用户会话/欢迎程序运行之前运行。如果您需要在 X 服务器中配置任何特殊内容,请设置此项。它以 root 身份运行。如果此命令返回错误代码,则 X 服务器将停止。 来源:https://wiki.ubuntu.com/LightDM

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-07-18
    • 2019-05-07
    • 2021-10-11
    • 1970-01-01
    • 1970-01-01
    • 2017-01-15
    • 1970-01-01
    相关资源
    最近更新 更多