日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

开源游戏机java模拟器_开源一个Flutter编写的完整终端模拟器

發(fā)布時(shí)間:2024/4/20 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 开源游戏机java模拟器_开源一个Flutter编写的完整终端模拟器 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

上次開源了一個(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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。