开源游戏机java模拟器_开源一个Flutter编写的完整终端模拟器
上次開源了一個簡易的終端模擬器,我也知道并不是標準的,但自己也一直在用,然后就發現了一些棘手的問題,就又跑去研究了一些完整終端的源碼,termux,Android Terminal,最后成功的將他們的原理在Flutter實現
其實這個源也可能會是你學習使用dart:ffi的一個例子,其中用到的char **,也就是二級指針的傳遞在也很少能在官方的example中也很難找到直接的例子,也是我處理這種類型遇見的比較麻煩的坑,主要就是沒有案例。我將termux的C語言部分完全重構以供Flutter使用,由于UI框架使用的Flutter經過測試可以在Macos上跑起來!!!
Process類的stdout是哪里來的?
自己在使用中遇見了這個棘手的問題,還是由于經驗不夠,還去知乎上提了我遇見的問題,
知乎傳送
經過與同學的探討后(死皮賴臉問人家),可以知道Process中的stdout是來自于pipe(管道),也可以看到stdout也有pipe這個方法,而管道是存在緩沖的,舉個🌰
使用
cp -rv sourceDir targetDir
命令,由于開啟了-v參數,所以在標準終端中,cp命令會一行一行打印出正在復制的文件,而當用dart的Process去執行這樣的操作,你在對stdout的監聽中并不會收到一次一行的回調,而是一次一堆的回調,那就是由于管道是存在緩沖機制的,達到緩沖上限后才能拿到一次,或者程序結束后,緩沖區未滿也能拿到。
我們再切換到標準終端模擬器
cp -rv sourceDir targetDir | xargs echo
我們在終端中也使用管道,通過xargs將其打印出來,這個時候會發現,打印的東西跟次數,跟dart中stdout的回調是一樣的,不止dart,包括java中runtime拿到的輸入流,也無法拿到無緩沖的輸出.
終端與管道的緩沖差別
終端也具有緩沖,終端為行緩沖,管道為全緩沖,行緩沖中,遇見換行符\n即可向終端中輸出一次,或者主動在C語言中調用fflush()方法,會將已經在緩沖區的內容輸出一次,如果沒有以上兩個條件,就只能等到緩沖區滿1024個字節,才能輸出一次
標準終端又是怎么做到拿到行緩沖的輸出的?
我能想到的最快的方法就是去看一些標準終端的開源庫,現在比較優秀有termux,跟Android Terminal,termux可以說是目前安卓上最強大的終端了,有大量的可擴展資源,我就直接clone下來,從manifest中找到主類,從Activity中oncreate中一點一點看,還是花了挺多時間,畢竟termux還是比較大型的儲存庫,也有注釋,但始終找不到關鍵的地方,能夠在Flutter實現的地方,最后定位到了UI中獲取輸入,包括將輸出同步到屏幕,這一系列都指向了JNI,也就是一個java到c/c++的一個通道,我也是從這才開始知道項目中的那個C語言是什么時候用的了。
標準終端實現原理
這種終端稱偽終端(pty)
必須先看一波來自互聯網的科普
偽終端(pseudo terminal,有時也被稱為 pty)是指偽終端 master 和偽終端 slave 這一對字符設備。其中的 slave 對應 /dev/pts/ 目錄下的一個文件,而 master 則在內存中標識為一個文件描述符(fd)。偽終端由終端模擬器提供,終端模擬器是一個運行在用戶態的應用程序。
Master 端是更接近用戶顯示器、鍵盤的一端,slave 端是在虛擬終端上運行的 CLI(Command Line Interface,命令行接口)程序。Linux 的偽終端驅動程序,會把 master 端(如鍵盤)寫入的數據轉發給 slave 端供程序輸入,把程序寫入 slave 端的數據轉發給 master 端供(顯示器驅動等)讀取。請參考下面的示意圖(此圖來自互聯網):
image
我們打開的終端桌面程序,比如 GNOME Terminal,其實是一種終端模擬軟件。當終端模擬軟件運行時,它通過打開 /dev/ptmx 文件創建了一個偽終端的 master 和 slave 對,并讓 shell 運行在 slave 端。當用戶在終端模擬軟件中按下鍵盤按鍵時,它產生字節流并寫入 master 中,shell 進程便可從 slave 中讀取輸入;shell 和它的子程序,將輸出內容寫入 slave 中,由終端模擬軟件負責將字符打印到窗口中。
文本描述符又是啥!?
來自百度:
Linux 中一切皆文件,比如 C++ 源文件、視頻文件、Shell腳本、可執行文件等,就連鍵盤、顯示器、鼠標等硬件設備也都是文件。
一個 Linux 進程可以打開成百上千個文件,為了表示和區分已經打開的文件,Linux 會給每個文件分配一個編號(一個 ID),這個編號就是一個整數,被稱為文件描述符(File Descriptor)。
以下操作僅在Unix系統上
大致知道這個文本描述符就是一個int值,通過這個值就能進行讀寫,C語言中write(fd, str, length),就能直接寫入文本描述符,java中也有一個FileDescriptor類,用來讀寫文本描述符,Dart沒有,不過可以解決。
簡述一下終端原理,在C語言中調用open("/dev/ptmx")會得到一個文本描述符,然后同時會在/dev/pts/下獲得一個文件的產生,文件名是0,1,2,3,系統會依次往上給你分配。
/dev/ptmx 是一個字符設備文件,當進程打開 /dev/ptmx 文件時,進程會同時獲得一個指向 pseudoterminal master(ptm)的文件描述符和一個在 /dev/pts 目錄中創建的 pseudoterminal slave(pts) 設備。通過打開 /dev/ptmx 文件獲得的每個文件描述符都是一個獨立的 ptm,它有自己關聯的 pts
直接看我更改后的實現
int get_ptm_int(
int rows,
int columns)
{
//調用open這個路徑會隨機獲得一個大于0的整形值
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
//這個值會從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;
}
這個函數主要就用來得到ptm的文本描述符,中間還有一些對終端,由于時間緣故,我暫時注釋了對java的回調報錯,之后用對dart的回調代替。拿到這個ptm描述符后,我們就可以對這個ptm描述符讀寫,往里面寫的內容都能再讀出來,感覺有點對此一舉?并不是,任何的二進制程序往里面進行寫操作,而你的終端UI,只需要一直讀就可以了,看一下termux在java部分的實現
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();
兩個死循環,一個負責讀ptm,將讀出的內容同步到UI
而另一個負責將輸入隊列的類容寫進ptm
在看termux中比較關鍵的一個函數(經過我更改后的)
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
//創建一個進程,返回是它的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);
//下面三個大概將stdin,stdout,stderr復制到了這個pts里面
//ptmx,pts pseudo terminal master and slave
dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);
//Linux的api,打開一個文件夾
DIR *self_dir = opendir("/proc/self/fd");
if (self_dir != NULL)
{
//dirfd沒查到,好像把文件夾轉換為文件描述符
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);
} //清除環境變量
// 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);
}
//執行程序
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);
}
}
實際上我為了配合Dart的部分,將termux原有的create_subprocess拆分成了兩塊,具體邏輯并未做修改,增加了中文注釋,留意其中調用了一次fork(),這個函數調用后,就會再分叉一個進程,之后的代碼都會被執行兩次,函數中通過pid的值來判斷父進程與子進程分別應該干啥,pid大于0即為父進程,可以看到父進程更改了pProcessId這個指針指向的值,子進程去執行了調用函數時的命令,包括設置當前環境,執行參數等,通過ptsname_r函數拿到了ptm對應的pts,然后通過dup2函數將改程序的0,1,2復制到了pts(/dev/pts/*),也就是stdin,stdout,stderr,最后調用exec,所以此時exec調用的二進制的輸出全會寫進pts,而寫進pts就能從ptm出來,也就實現了偽終端
Dart不能讀寫文本描述符怎么辦?
通過dart:ff對接,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); //獲取當前flag
flag |= O_NONBLOCK; //設置新falg
fcntl(fd, F_SETFL, flag); //更新flag
//動態申請空間
char *str = (char *)malloc((4097) * sizeof(char));
//read函數返回從fd中讀取到字符的長度
//讀取的內容存進str,4096表示此次讀取4096個字節,如果只讀到10個則length為10
int length = read(fd, str, 4096);
if (length == -1)
{
free(str);
return NULL;
}
else
{
str[length] = '\0';
return str;
}
}
Flutter的部分實現也比較復雜,因為要重寫一套完整的終端序列不是簡單的事,termux作為安卓原生項目,有大量的社區資源跟第三方開發者的支持,現在才已經比較完善,關于Dart調用ffi也可以參考我之前的帖子
效果!!!
Python的使用:
Python
光標移動:
在這里插入圖片描述
ls等命令顏色的輸出:
在這里插入圖片描述
開源地址
目前這個新的終端模擬器已經完全的引進了自己的項目,作者的維護能力非常有限,更新速度也比較慢,如果對這個項目有興趣有問題都可以在下面留言,感謝各位前輩!!!
參考帖子
總結
以上是生活随笔為你收集整理的开源游戏机java模拟器_开源一个Flutter编写的完整终端模拟器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jmap java opts_jmap
- 下一篇: cmd上写的java简单代码_用cmd编