开源游戏机java模拟器_开源一个Flutter编写的完整终端模拟器
上次開源了一個(gè)簡(jiǎn)易的終端模擬器,我也知道并不是標(biāo)準(zhǔn)的,但自己也一直在用,然后就發(fā)現(xiàn)了一些棘手的問題,就又跑去研究了一些完整終端的源碼,termux,Android Terminal,最后成功的將他們的原理在Flutter實(shí)現(xiàn)
其實(shí)這個(gè)源也可能會(huì)是你學(xué)習(xí)使用dart:ffi的一個(gè)例子,其中用到的char **,也就是二級(jí)指針的傳遞在也很少能在官方的example中也很難找到直接的例子,也是我處理這種類型遇見的比較麻煩的坑,主要就是沒有案例。我將termux的C語言部分完全重構(gòu)以供Flutter使用,由于UI框架使用的Flutter經(jīng)過測(cè)試可以在Macos上跑起來!!!
Process類的stdout是哪里來的?
自己在使用中遇見了這個(gè)棘手的問題,還是由于經(jīng)驗(yàn)不夠,還去知乎上提了我遇見的問題,
知乎傳送
經(jīng)過與同學(xué)的探討后(死皮賴臉問人家),可以知道Process中的stdout是來自于pipe(管道),也可以看到stdout也有pipe這個(gè)方法,而管道是存在緩沖的,舉個(gè)🌰
使用
cp -rv sourceDir targetDir
命令,由于開啟了-v參數(shù),所以在標(biāo)準(zhǔn)終端中,cp命令會(huì)一行一行打印出正在復(fù)制的文件,而當(dāng)用dart的Process去執(zhí)行這樣的操作,你在對(duì)stdout的監(jiān)聽中并不會(huì)收到一次一行的回調(diào),而是一次一堆的回調(diào),那就是由于管道是存在緩沖機(jī)制的,達(dá)到緩沖上限后才能拿到一次,或者程序結(jié)束后,緩沖區(qū)未滿也能拿到。
我們?cè)偾袚Q到標(biāo)準(zhǔn)終端模擬器
cp -rv sourceDir targetDir | xargs echo
我們?cè)诮K端中也使用管道,通過xargs將其打印出來,這個(gè)時(shí)候會(huì)發(fā)現(xiàn),打印的東西跟次數(shù),跟dart中stdout的回調(diào)是一樣的,不止dart,包括java中runtime拿到的輸入流,也無法拿到無緩沖的輸出.
終端與管道的緩沖差別
終端也具有緩沖,終端為行緩沖,管道為全緩沖,行緩沖中,遇見換行符\n即可向終端中輸出一次,或者主動(dòng)在C語言中調(diào)用fflush()方法,會(huì)將已經(jīng)在緩沖區(qū)的內(nèi)容輸出一次,如果沒有以上兩個(gè)條件,就只能等到緩沖區(qū)滿1024個(gè)字節(jié),才能輸出一次
標(biāo)準(zhǔn)終端又是怎么做到拿到行緩沖的輸出的?
我能想到的最快的方法就是去看一些標(biāo)準(zhǔn)終端的開源庫,現(xiàn)在比較優(yōu)秀有termux,跟Android Terminal,termux可以說是目前安卓上最強(qiáng)大的終端了,有大量的可擴(kuò)展資源,我就直接clone下來,從manifest中找到主類,從Activity中oncreate中一點(diǎn)一點(diǎn)看,還是花了挺多時(shí)間,畢竟termux還是比較大型的儲(chǔ)存庫,也有注釋,但始終找不到關(guān)鍵的地方,能夠在Flutter實(shí)現(xiàn)的地方,最后定位到了UI中獲取輸入,包括將輸出同步到屏幕,這一系列都指向了JNI,也就是一個(gè)java到c/c++的一個(gè)通道,我也是從這才開始知道項(xiàng)目中的那個(gè)C語言是什么時(shí)候用的了。
標(biāo)準(zhǔn)終端實(shí)現(xiàn)原理
這種終端稱偽終端(pty)
必須先看一波來自互聯(lián)網(wǎng)的科普
偽終端(pseudo terminal,有時(shí)也被稱為 pty)是指?jìng)谓K端 master 和偽終端 slave 這一對(duì)字符設(shè)備。其中的 slave 對(duì)應(yīng) /dev/pts/ 目錄下的一個(gè)文件,而 master 則在內(nèi)存中標(biāo)識(shí)為一個(gè)文件描述符(fd)。偽終端由終端模擬器提供,終端模擬器是一個(gè)運(yùn)行在用戶態(tài)的應(yīng)用程序。
Master 端是更接近用戶顯示器、鍵盤的一端,slave 端是在虛擬終端上運(yùn)行的 CLI(Command Line Interface,命令行接口)程序。Linux 的偽終端驅(qū)動(dòng)程序,會(huì)把 master 端(如鍵盤)寫入的數(shù)據(jù)轉(zhuǎn)發(fā)給 slave 端供程序輸入,把程序?qū)懭?slave 端的數(shù)據(jù)轉(zhuǎn)發(fā)給 master 端供(顯示器驅(qū)動(dòng)等)讀取。請(qǐng)參考下面的示意圖(此圖來自互聯(lián)網(wǎng)):
image
我們打開的終端桌面程序,比如 GNOME Terminal,其實(shí)是一種終端模擬軟件。當(dāng)終端模擬軟件運(yùn)行時(shí),它通過打開 /dev/ptmx 文件創(chuàng)建了一個(gè)偽終端的 master 和 slave 對(duì),并讓 shell 運(yùn)行在 slave 端。當(dāng)用戶在終端模擬軟件中按下鍵盤按鍵時(shí),它產(chǎn)生字節(jié)流并寫入 master 中,shell 進(jìn)程便可從 slave 中讀取輸入;shell 和它的子程序,將輸出內(nèi)容寫入 slave 中,由終端模擬軟件負(fù)責(zé)將字符打印到窗口中。
文本描述符又是啥!?
來自百度:
Linux 中一切皆文件,比如 C++ 源文件、視頻文件、Shell腳本、可執(zhí)行文件等,就連鍵盤、顯示器、鼠標(biāo)等硬件設(shè)備也都是文件。
一個(gè) Linux 進(jìn)程可以打開成百上千個(gè)文件,為了表示和區(qū)分已經(jīng)打開的文件,Linux 會(huì)給每個(gè)文件分配一個(gè)編號(hào)(一個(gè) ID),這個(gè)編號(hào)就是一個(gè)整數(shù),被稱為文件描述符(File Descriptor)。
以下操作僅在Unix系統(tǒng)上
大致知道這個(gè)文本描述符就是一個(gè)int值,通過這個(gè)值就能進(jìn)行讀寫,C語言中write(fd, str, length),就能直接寫入文本描述符,java中也有一個(gè)FileDescriptor類,用來讀寫文本描述符,Dart沒有,不過可以解決。
簡(jiǎn)述一下終端原理,在C語言中調(diào)用open("/dev/ptmx")會(huì)得到一個(gè)文本描述符,然后同時(shí)會(huì)在/dev/pts/下獲得一個(gè)文件的產(chǎn)生,文件名是0,1,2,3,系統(tǒng)會(huì)依次往上給你分配。
/dev/ptmx 是一個(gè)字符設(shè)備文件,當(dāng)進(jìn)程打開 /dev/ptmx 文件時(shí),進(jìn)程會(huì)同時(shí)獲得一個(gè)指向 pseudoterminal master(ptm)的文件描述符和一個(gè)在 /dev/pts 目錄中創(chuàng)建的 pseudoterminal slave(pts) 設(shè)備。通過打開 /dev/ptmx 文件獲得的每個(gè)文件描述符都是一個(gè)獨(dú)立的 ptm,它有自己關(guān)聯(lián)的 pts
直接看我更改后的實(shí)現(xiàn)
int get_ptm_int(
int rows,
int columns)
{
//調(diào)用open這個(gè)路徑會(huì)隨機(jī)獲得一個(gè)大于0的整形值
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
//這個(gè)值會(huì)從0依次上增
// if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
#ifdef LACKS_PTSNAME_R
char *devname;
#else
char devname[64];
#endif
if (grantpt(ptm) || unlockpt(ptm) ||
#ifdef LACKS_PTSNAME_R
(devname = ptsname(ptm)) == NULL
#else
ptsname_r(ptm, devname, sizeof(devname))
#endif
)
{
// return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
}
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
struct termios tios;
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */
struct winsize sz = {.ws_row = (unsigned short)rows, .ws_col = (unsigned short)columns};
ioctl(ptm, TIOCSWINSZ, &sz);
return ptm;
}
這個(gè)函數(shù)主要就用來得到ptm的文本描述符,中間還有一些對(duì)終端,由于時(shí)間緣故,我暫時(shí)注釋了對(duì)java的回調(diào)報(bào)錯(cuò),之后用對(duì)dart的回調(diào)代替。拿到這個(gè)ptm描述符后,我們就可以對(duì)這個(gè)ptm描述符讀寫,往里面寫的內(nèi)容都能再讀出來,感覺有點(diǎn)對(duì)此一舉?并不是,任何的二進(jìn)制程序往里面進(jìn)行寫操作,而你的終端UI,只需要一直讀就可以了,看一下termux在java部分的實(shí)現(xiàn)
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@Override
public void run() {
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
final byte[] buffer = new byte[4096];
while (true) {
int read = termIn.read(buffer);
if (read == -1) return;
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
}
} catch (Exception e) {
// Ignore, just shutting down.
}
}
}.start();
new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
@Override
public void run() {
final byte[] buffer = new byte[4096];
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
while (true) {
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
if (bytesToWrite == -1) return;
termOut.write(buffer, 0, bytesToWrite);
}
} catch (IOException e) {
// Ignore.
}
}
}.start();
兩個(gè)死循環(huán),一個(gè)負(fù)責(zé)讀ptm,將讀出的內(nèi)容同步到UI
而另一個(gè)負(fù)責(zé)將輸入隊(duì)列的類容寫進(jìn)ptm
在看termux中比較關(guān)鍵的一個(gè)函數(shù)(經(jīng)過我更改后的)
void create_subprocess(char *env,
char const *cmd,
char const *cwd,
char *const argv[],
char **envp,
int *pProcessId,
int ptmfd)
{
#ifdef LACKS_PTSNAME_R
char *devname;
#else
char devname[64];
#endif
#ifdef LACKS_PTSNAME_R
devname = ptsname(ptmfd);
#else
ptsname_r(ptmfd, devname, sizeof(devname));
#endif
//創(chuàng)建一個(gè)進(jìn)程,返回是它的pid
pid_t pid = fork();
if (pid < 0)
{
// return throw_runtime_exception(env, "Fork failed");
}
else if (pid > 0)
{
*pProcessId = (int)pid;
}
else
{
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
close(ptmfd);
setsid();
//O_RDWR讀寫,devname為/dev/pts/0,1,2,3...
int pts = open(devname, O_RDWR);
if (pts < 0)
exit(-1);
//下面三個(gè)大概將stdin,stdout,stderr復(fù)制到了這個(gè)pts里面
//ptmx,pts pseudo terminal master and slave
dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);
//Linux的api,打開一個(gè)文件夾
DIR *self_dir = opendir("/proc/self/fd");
if (self_dir != NULL)
{
//dirfd沒查到,好像把文件夾轉(zhuǎn)換為文件描述符
int self_dir_fd = dirfd(self_dir);
struct dirent *entry;
while ((entry = readdir(self_dir)) != NULL)
{
int fd = atoi(entry->d_name);
if (fd > 2 && fd != self_dir_fd)
close(fd);
}
closedir(self_dir);
} //清除環(huán)境變量
// clearenv();
if (envp)
for (; *envp; ++envp)
putenv(*envp);
if (chdir(cwd) != 0)
{
char *error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1)
error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
//執(zhí)行程序
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char *error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1)
error_message = "exec()";
perror(error_message);
_exit(1);
}
}
實(shí)際上我為了配合Dart的部分,將termux原有的create_subprocess拆分成了兩塊,具體邏輯并未做修改,增加了中文注釋,留意其中調(diào)用了一次fork(),這個(gè)函數(shù)調(diào)用后,就會(huì)再分叉一個(gè)進(jìn)程,之后的代碼都會(huì)被執(zhí)行兩次,函數(shù)中通過pid的值來判斷父進(jìn)程與子進(jìn)程分別應(yīng)該干啥,pid大于0即為父進(jìn)程,可以看到父進(jìn)程更改了pProcessId這個(gè)指針指向的值,子進(jìn)程去執(zhí)行了調(diào)用函數(shù)時(shí)的命令,包括設(shè)置當(dāng)前環(huán)境,執(zhí)行參數(shù)等,通過ptsname_r函數(shù)拿到了ptm對(duì)應(yīng)的pts,然后通過dup2函數(shù)將改程序的0,1,2復(fù)制到了pts(/dev/pts/*),也就是stdin,stdout,stderr,最后調(diào)用exec,所以此時(shí)exec調(diào)用的二進(jìn)制的輸出全會(huì)寫進(jìn)pts,而寫進(jìn)pts就能從ptm出來,也就實(shí)現(xiàn)了偽終端
Dart不能讀寫文本描述符怎么辦?
通過dart:ff對(duì)接,C語言可以讀就不存在
void write_to_fd(int fd, char *str)
{
write(fd, str, strlen(str));
}
char *get_output_from_fd(int fd)
{
int flag = -1;
flag = fcntl(fd, F_GETFL); //獲取當(dāng)前flag
flag |= O_NONBLOCK; //設(shè)置新falg
fcntl(fd, F_SETFL, flag); //更新flag
//動(dòng)態(tài)申請(qǐng)空間
char *str = (char *)malloc((4097) * sizeof(char));
//read函數(shù)返回從fd中讀取到字符的長(zhǎng)度
//讀取的內(nèi)容存進(jìn)str,4096表示此次讀取4096個(gè)字節(jié),如果只讀到10個(gè)則length為10
int length = read(fd, str, 4096);
if (length == -1)
{
free(str);
return NULL;
}
else
{
str[length] = '\0';
return str;
}
}
Flutter的部分實(shí)現(xiàn)也比較復(fù)雜,因?yàn)橐貙懸惶淄暾慕K端序列不是簡(jiǎn)單的事,termux作為安卓原生項(xiàng)目,有大量的社區(qū)資源跟第三方開發(fā)者的支持,現(xiàn)在才已經(jīng)比較完善,關(guān)于Dart調(diào)用ffi也可以參考我之前的帖子
效果!!!
Python的使用:
Python
光標(biāo)移動(dòng):
在這里插入圖片描述
ls等命令顏色的輸出:
在這里插入圖片描述
開源地址
目前這個(gè)新的終端模擬器已經(jīng)完全的引進(jìn)了自己的項(xiàng)目,作者的維護(hù)能力非常有限,更新速度也比較慢,如果對(duì)這個(gè)項(xiàng)目有興趣有問題都可以在下面留言,感謝各位前輩!!!
參考帖子
總結(jié)
以上是生活随笔為你收集整理的开源游戏机java模拟器_开源一个Flutter编写的完整终端模拟器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jmap java opts_jmap
- 下一篇: cmd上写的java简单代码_用cmd编