Keep learning, keep living...

0%

CentOS7配置Console GUI/TUI程序

以虚拟机镜像或者自带操作系统形式交付的产品往往需要对系统进行各种各样的配置,如IP设置、DNS设置等等。为了提升用户体验,提供更简化的使用方式,往往会在console界面提供GUI/TUI的交互方式, 如XenServerconsole界面:

本文介绍在CentOS7系统上增加GUI界面的方法。

Linux系统的终端输入/输出设备一般称为TTY,是TeleTypewriter的缩写, 它的历史可以参考这两篇文章:

getty是管理物理或者虚拟终端(ttys)程序的通用名称,表示get tty,用于防止非授权访问系统。当getty检测到终端连接时,它获取用户输入用户名, 然后执行login程序获取用户输入密码来进行登录校验。

从终端登录的一般过程是:

  • 系统内核启动完成后,会启动init进程,它是一号进程。
  • init进程会在特定的tty启动getty进程获取用户名。
  • 调用exec执行login程序处理登录过程
  • login登录校验成功后,执行相应用户指定的shell程序

整体的流程图如下:

CentOS7上的init程序已经从SysV init换为systemd,安装的getty程序为agetty。默认情况下,CentOS76个可用的tty,可以使用Ctrl + Alt + F[1-6]进行切换访问。

根据上述的终端登录流程,想要在终端显示GUI程序可以有几种方法:

  • GUI程序作为getty程序执行
  • GUI程序替换login程序执行,这种方式需要跳过getty程序读取用户名的步骤
  • GUI程序作为shell程序执行,这种方式需要完成自动登录,跳过密码校验之前的步骤

CentOSinit程序为systemd。它通过service模板来定义getty服务,从而支持多个getty程序启动:

1
2
3
4
[root@default ~]# systemctl list-units --type=service |grep tty
getty@tty1.service loaded active running Getty on tty1
[root@default ~]# systemctl list-unit-files --type=service |grep '^getty@'
getty@.service enabled

下面分别来简单实验一下这几种方式。

GUI程序作为getty程序执行

我们写一个简单的GUI程序来替换getty, getty程序需要将标准输入/输出指向/dev/ttyN(N为数字)驱动文件。可以参考agetty的实现,它位于包:

1
2
[root@default ~]# rpm -qf /sbin/agetty
util-linux-2.23.2-63.el7.x86_64

之前的文章<<TUI库newt和snack简要介绍>>介绍过RedhatTUInewt, 这里基于newt实现一个简单的登录界面,tuilogin.c:

1
2
3
4
5
6
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <unistd.h>
#include <newt.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>


static void show_login_failed_window(void) {
newtComponent label, button, form;

newtCls();
newtCenteredWindow(60, 6, "NOTE");
label = newtLabel(10, 2, "Login failed!");

button = newtCompactButton(30, 4, "Exit");

form = newtForm(NULL, NULL, 0);
newtFormAddComponents(form, label, button, NULL);
newtRunForm(form);

newtFormDestroy(form);
newtPopWindow();
}

static void open_tty(char *tty) {
int fd;
char buf[PATH_MAX+1];

snprintf(buf, sizeof(buf), "/dev/%s", tty);

fd = open(buf, O_RDWR|O_NOCTTY|O_NONBLOCK, 0);
if (fd < -1) {
fprintf(stderr, "open %s failed: %d\n", fd);
exit(-1);
}

close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

assert(dup2(fd, 0) == 0);
assert(dup2(fd, 1) == 1);
assert(dup2(fd, 2) == 2);

close(fd);

setvbuf(stdout, NULL, _IONBF, 0);
setenv("TERM", "linux", 1);
}


int main(int argc, char **argv) {
int cols, rows;
char *bash_args[1];

if (argc != 2) {
fprintf(stderr, "usage: menu <tty>\n");
exit(-1);
}

open_tty(argv[1]);

newtInit();

do {
newtCls();
newtPushHelpLine(NULL);

newtGetScreenSize(&cols, &rows);
newtOpenWindow(1, 1, cols - 2, rows - 4, "LOGIN");

newtComponent form, btn_ok, label_username, label_password,
entry_username, entry_password, result;
const char *username, *password;

label_username = newtLabel(10, 3, "Username:");
entry_username = newtEntry(20, 3, "", 20, &username,
NEWT_FLAG_SCROLL);
label_password = newtLabel(10, 5, "Password:");
entry_password = newtEntry(20, 5, "", 20, &password,
NEWT_FLAG_PASSWORD | NEWT_FLAG_SCROLL);

result = newtLabel(10, 10, "");

btn_ok = newtButton(20, 7, " OK ");

form = newtForm(NULL, NULL, 0);

newtFormAddComponents(form, label_username, entry_username,
label_password, entry_password,
btn_ok, result,
NULL);
struct newtExitStruct exit_status;
newtFormRun(form, &exit_status);

if (exit_status.reason == NEWT_EXIT_COMPONENT) {
if (exit_status.u.co == btn_ok) {
if ((strcmp(username, "root") == 0)
&& (strcmp(password, "123456") == 0))
{
/* login success */
break;
} else {
show_login_failed_window();
}
}
}

newtRefresh();
newtFormDestroy(form);
newtPopWindow();
newtPopHelpLine();

} while (1);

newtFinished();

bash_args[0] = "bash";

execv("/bin/bash", bash_args);

return 0;
}

编译:

1
gcc -o tuilogin tuilogin.c -lnewt

程序功能为,当用户名输入root,密码输入123456时,执行bash进程。

将编译后的tuilogin放到/sbin/目录。

然后使用systemddrop-in files机制来修改tty1的启动逻辑:

1
systemctl edit getty@tty1

输入内容:

1
2
3
[Service]
ExecStart=
ExecStart=-/sbin/tuilogin %I

这会创建文件/etc/systemd/system/getty@tty1.service.d/override.conf

重启系统:

可以看到TUI程序在机器启动时执行,当输入root123456之后,进入bash界面。

GUI程序替换login程序执行

因为agetty已经处理了/dev/ttyN文件的处理,我们去掉上述代码中tty的处理逻辑:

1
2
3
4
5
6
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <newt.h>

void show_login_failed_window(void) {
newtComponent label, button, form;

newtCls();
newtCenteredWindow(60, 6, "NOTE");
label = newtLabel(10, 2, "Login failed!");

button = newtCompactButton(30, 4, "Exit");

form = newtForm(NULL, NULL, 0);
newtFormAddComponents(form, label, button, NULL);
newtRunForm(form);

newtFormDestroy(form);
newtPopWindow();
}

int main(void) {
int cols, rows;
char *bash_args[1];

newtInit();

do {
newtCls();
newtPushHelpLine(NULL);

newtGetScreenSize(&cols, &rows);
newtOpenWindow(1, 1, cols - 2, rows - 4, "LOGIN NOTTY");

newtComponent form, btn_ok, label_username, label_password,
entry_username, entry_password, result;
const char *username, *password;

label_username = newtLabel(10, 3, "Username:");
entry_username = newtEntry(20, 3, "", 20, &username,
NEWT_FLAG_SCROLL);
label_password = newtLabel(10, 5, "Password:");
entry_password = newtEntry(20, 5, "", 20, &password,
NEWT_FLAG_PASSWORD | NEWT_FLAG_SCROLL);

result = newtLabel(10, 10, "");

btn_ok = newtButton(20, 7, " OK ");

form = newtForm(NULL, NULL, 0);

newtFormAddComponents(form, label_username, entry_username,
label_password, entry_password,
btn_ok, result,
NULL);
struct newtExitStruct exit_status;
newtFormRun(form, &exit_status);

if (exit_status.reason == NEWT_EXIT_COMPONENT) {
if (exit_status.u.co == btn_ok) {
if ((strcmp(username, "root") == 0)
&& (strcmp(password, "123456") == 0))
{
/* login success */
break;
} else {
show_login_failed_window();
}
}
}

newtRefresh();
newtFormDestroy(form);
newtPopWindow();
newtPopHelpLine();

} while (1);

newtFinished();

bash_args[0] = "bash";

execv("/bin/bash", bash_args);

return 0;
}

agetty程序支持通过命令行指令被调用的login程序:

1
2
3
4
5
-l, --login-program login_program
Invoke the specified login_program instead of /bin/login.
This allows the use of a non-standard login program. Such a
program could, for example, ask for a dial-up password or use
a different password file. See --login-options.
1
2
3
4
5
6
7
8
9
10
11
-n, --skip-login
Do not prompt the user for a login name. This can be used in
connection with the --login-program option to invoke a
non-standard login process such as a BBS system. Note that
with the --skip-login option, agetty gets no input from the
user who logs in and therefore will not be able to figure out
parity, character size, and newline processing of the
connection. It defaults to space parity, 7 bit characters,
and ASCII CR (13) end-of-line character. Beware that the
program that agetty starts (usually /bin/login) is run as
root.

再次修改getty@tty1drop-in file:

1
systemctl edit getty@tty1

内容为:

1
2
3
[Service]
ExecStart=
ExecStart=-/usr/sbin/agetty --login-program=/sbin/tuilogin_notty -n %I $TERM

重启系统, 可以看到新的TUI程序正常执行:

GUI程序作为shell程序执行

这种方式需要完成自动登录,跳过密码校验之前的步骤,可以使用agetty-a选项:

1
2
3
4
5
6
7
8
9
10
11
-a, --autologin username
Automatically log in the specified user without asking for a
username or password. Using this option causes an -f username
option and argument to be added to the /bin/login command
line. See --login-options, which can be used to modify this
option’s behavior.

Note that --autologin may affect the way in which getty
initializes the serial line, because on auto-login agetty
does not read from the line and it has no opportunity
optimize the line setting.

再次修改getty@tty1drop-in file:

1
systemctl edit getty@tty1

内容为:

1
2
3
[Service]
ExecStart=
ExecStart=-/usr/sbin/agetty --autologin root --noclear %I $TERM

这次我们使用dialog程序来创建一个TUI程序用来和之前的窗口区分。
安装dialog:

1
yum install -y dialog

创建/sbin/tuilogin_shell文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
while true
do
USERNAME=$(dialog --backtitle "Welcome" --title "login" --colors --no-shadow --stdout --nocancel --inputbox "Username:" 10 60)
if [ x"$USERNAME" == x"" ]; then
continue
fi

PASSWORD=$(dialog --backtitle "Welcome" --title "login" --colors --no-shadow --stdout --insecure --passwordbox "Password:" 10 60)

if [ x"$PASSWORD" == x"" ]; then
continue
fi

if [ x"$USERNAME" == x"root" ] && [ x"$PASSWORD" == x"123456" ]; then
clear
exec /bin/bash
else
dialog --backtitle "Welcome" --title "login" --colors --no-shadow --msgbox "Invalid username or password!" 10 60
fi
done

修改/etc/passwdroot帐号的shell部分为/sbin/tuilogin_shell:

1
root:x:0:0:root:/root:/sbin/tuilogin_shell

重启系统, 可以看到执行成功:

参考: