分類
Linux 软件

Turmux X11 以及 VSCode

本篇文件介紹如何在安卓手機上運行 VSCode 來實現在旅途中繼續編碼。我使用的手機是 2018 年的 Xperia XZ2,內存只有 4GB,CPU 型號是 Qualcomm SDM845,屏幕是 5.7 in (140 mm) 1080p (2160 × 1080) IPS LCD, ~424 pixel density。最終效果如下:

VSCode On Debian With Termux

藉助 Termux 我們得以在安卓上運行多種 Linux,之所以選擇 Debian 是因爲它相對輕量且不含 systemd (安卓不支持)。本文得以形成主要是參考了 Termux X11:手機的X伺服器使用教學Android手機安裝Linux發行版:Termux proot-distro使用教學 以及 VSCode installation tutorial with Termux-x11,在此一併感謝!

如果你的手機性能良好,推薦按照 Ivon 的文章安裝 Xfce4 桌面環境,這樣東西就一應俱全了(除了不能運行 Docker,安卓內核不支持)。

安裝 Termux

雖然可以直接去 Github 下載安裝包,但是還是建議去 F-Droid 安裝Termux。當前的最新版本是 0.118.0。安裝完成後執行下列命令:

pkg update
pkg upgrade
pkg install nano proot-distro x11-repo termux-x11-nightly
termux-setup-storage

安裝 X11

這裏需要到 Github 下載 X11 的安裝包。如果沒有 Github 賬戶,可以從這裏下載我安裝的這個版本。

安裝 Debian

手機性能好的話,可以嘗試其他口味的 Linux 以及桌面環境。我的手機連 Xfce4 也運行不起來(X11 裏可以,但是 Debian 裏就會自動退出),所以我使用輕量的 openbox。

proot-distro install debian
#進入 Debian
proot-distro login debian
apt update
apt upgrade
apt install nano sudo wget git
#添加普通用戶
passwd
groupadd storage
groupadd wheel
groupadd video
#替換 [USER] 爲你想要的名字
useradd -m -g users -G wheel,audio,video,storage -s /bin/bash [USER]
passwd [USER]
visudo
#在 root ALL=(ALL:ALL) ALL 這行的後面添加
[USER] root ALL=(ALL:ALL) ALL
#保存後使用 exit 退出 Debian

創建兩個腳本來啓動和關閉 X11:

##以下是startDebian.sh
#!/bin/bash
export DISPLAY=:0
killall -9 termux-x11 termux-wake-lock
# 啟動Termux X11
am start --user 0 -n com.termux.x11/com.termux.x11.MainActivity
XDG_RUNTIME_DIR=${TMPDIR}
termux-x11 :0 &
sleep 3

proot-distro login debian --user [USER] --shared-tmp
##以下是stopDebian.sh
#!/bin/bash
killall -9 termux-x11

然後給予可執行權限:

chmod +x startDebian.sh
chmod +x stopDebian.sh
#然後執行 startDebian.sh 就能就如 Debian 了
./startDebian.sh
#若出現報錯 ERROR: openbox-xdg-autostart requires PyXDG to be installed 可以按回車鍵忽略
#安裝窗口管理器
sudo apt install xorg openbox
sudo apt install firefox-esr fonts-noto-cjk proxychains4 curl tint2 xclip pcmanfm
#火狐:你需要一個瀏覽器
#fonts-noto-cjk:字體
#proxychains4:沒有代理不科學
#curl:安裝 curl 可以修復使用 proxychains4 時 library "libdl.so.2" not found 的錯誤
#tint2:任務欄讓桌面更美好
#pcmanfm:桌面環境怎能少得了文件管理器
#xclip:把剪切版的內容從安卓傳遞到 Debian

# .bashrc 中添加下面文本

alias pp='termux-clipboard-get | xclip -sel clipboard'
alias p='proxychains4'

export DISPLAY=:0
if ! pgrep -x "openbox" > /dev/null; then
    openbox-session &
    tint2 &
fi

安裝 VSCode

前往 VSCode 下載頁,下載 Arm64 版本的 .deb 安裝包。可以通過瀏覽器下載,然後下載頁會有下載的直鏈,複製後用 wget 下載就好。

#安裝 VSCode
sudo dpkg -i /sdcard/Downloads/code-[VERSION].deb
#如果出現報錯嘗試
sudo apt --fix-broken install
#然後再試試上面的 dpkg 安裝指令

最在啓動欄創建兩個常用的快捷啓動方式:先修改下桌面快捷方式

#把 /usr/share/applications/code.desktop 中的
Exec=/usr/share/code/code --unity-launch %F
#改爲
Exec=/usr/share/code/code --no-sandbox %F

#把 /usr/share/applications/firefox-esr.desktop 中的
Exec=/usr/lib/firefox-esr/firefox-esr %u
#改爲
Exec=/usr/lib/firefox-esr/firefox-esr --no-sandbox %u

兩個手機點一下桌面(黑屏)呼出右鍵菜單,選擇 Applications > Settings > Tint2 Settings 。點擊左上角編輯,Panel 推薦設置在左邊,length 150%,size 60 pixels。Panel items 留下 Launcher 和 Taskbar 即可。Lanucher 裏選擇 VS Code 和 Firefox ESR 保存即可。

既然提到了命令行啓動火狐,順便記錄下火狐的多用戶。在命令行加上 -P 可以啓動 Profile 管理器。在管理器裏可以新增、重命名以及設置默認的用戶。想以用戶 42 啓動,則把上面快捷方式裏的運行項改爲 /usr/lib/firefox-esr/firefox-esr --no-sandbox -P 42 %u 即可。

使用場景

開始使用

  1. 啓動 Termux
  2. ./startDebian.sh

退出

  1. 切換到 Termux,執行 exit
  2. ./stopDebian.sh

其他小撇步

字體非常小:在 Debian 家目錄新增文件 nano ~/.Xdefaults 內容爲 Xft.dpi: 192 即指定 dpi 分辨率爲 192。如果個別應用字體還是很小,可以嘗試通過應用自己的配置來調整顯示大小。顏色深度也可以在這裏配置,例如:Xft.depth: 16

剪切版:從 Debian 到安卓默認是沒問題的,除非你在 X11 的配置項裏禁用了。但是從安卓到 Debian 卻不輕鬆。目前一個可行的方案就是上面使用 xclip 的方式:首先切換到 Termux,運行 alias pp,然後就可以在 Debian 裏粘貼了。

中文輸入:本篇沒有安裝中文輸入法,以節省更多資源予 VSCode。

掛載外部 SD Card 到 Debian:啓動時加入 --bind /data/data/com.termux/files/home/storage/external-1:/sdcard1 就會將外部儲存卡的可用目錄掛載到 proot 系統裏的 /sdcard1。手機裏的內部儲存默認是掛載在 /sdcard 的。

分類
Linux

使用 xinput 映射鼠標按鍵

鼠標左鍵故障無法使用了,可以使用 xinput 將其他不常用的按鍵映射爲左鍵。Fedora 的 Xfce 桌面環境沒有默認安裝 xinput,需要手動安裝一下。

sudo dnf install xinput
#查看設備列表
xinput list
#比如我的鼠標會找到
Bluetooth Mouse M336/M337/M535 Mouse    	id=20	[slave  pointer  (2)]
#後續操作使用 20 和 Bluetooth Mouse M336/M337/M535 Mouse 都可以
#查看此鼠標當前的按鍵映射配置
xinput --get-button-map 'Bluetooth Mouse M336/M337/M535 Mouse'
1 2 3 4 5 6 7 8 9 10 11 12
#查看當前鼠標按鍵
xinput test 'Bluetooth Mouse M336/M337/M535 Mouse'
#當你操作鼠標時機會顯示哪個按鍵有動作
#通過映射拯救左鍵
xinput set-button-map 'Bluetooth Mouse M336/M337/M535 Mouse' 1 1 3 4 5 1 1 1 1 1 1 1
#我把不常用的鍵都指定成左鍵了
#測試沒有問題後可以將映射的指令添加到開機啓動裏
#Applications > Settings > Session and Startup > Application Autostart
/usr/bin/xinput set-button-map 'Bluetooth Mouse M336/M337/M535 Mouse' 1 1 3 4 5 1 1 1 1 1 1 1

用了一小段時間後發現,似乎鼠標滾輪也不是很好,所以最終在屋裏找到另一隻鼠標後,放棄了這隻。建議大家旅行時攜帶鼠標最好有個盒子,免得擠壓導致鼠標死亡。

分類
软件

nginx 默認配置下載文件 403

裝了個 nginx 臨時分享一下文件,但是文件複製到 /usr/share/nginx/html 中後訪問卻提示 403。排除防火牆、文件權限和所有者的問題後,發現是 SElinux 的原因。

#查看 SELinux 上下文
ls -lZ
#大概是這樣的
-rw-r--r--. 1 root root system_u:object_r:httpd_sys_content_t:s0     3086 Jan 28 13:39 index.html
-rwxr-xr-x. root root unconfined_u:object_r:user_home_t:s0 ToBeShared.zip
#只要 user_home_t 改成 httpd_sys_content_t 就可以了
sudo chcon -t httpd_sys_content_t ToBeShared.zip

更多參考:Nginx 403 error for single file while others workSELinux documentation from RedHat

分類
讀書

我成了故鄉的臥底

田園將蕪

——後鄉村時代記事

作者:江子

ISBN:978-7-224-10540-7-

江西人民出版社


從外表看,我已經與一名城裡人無異。

我也算得上是衣冠楚楚。我的皮膚也還白皙。我的普通話還算流利。我保持著城市的許多生活習慣:出門頭髮梳得整齊,喜歡喝點好茶和咖啡。我走起路來和城裡人一樣快慢有致。我還喜歡看碟,聽音樂,對足球也說得上愛好,床頭上堆著一些與精神有關的書。必要的時候,我還能說上幾句這座城市的方言,短時間內一般不會露出漏洞,對我不熟悉的人,完全可能把我當作土生土長的本市人。

可我是農民的後裔,是一個生活在城裡的鄉下人。在許多表格關於籍貫的一欄裡,我寫的是與我所在的城市不一樣的一個地址﹁吉水﹂——距離我居住的城市二百公裡之遙的南方小縣。而若干年前,我在吉水工作時,寫的是﹁楓江鎮下隴洲村﹂。

那是吉水贛江之濱的一個村子。除了求學,我的童年和少年都在那裡度過。那裡至今住著我的父輩和兄弟。從這個村莊出發,我的親友遍布故鄉的山山水水。得益於國家早年沒有來得及實施計劃生育政策,祖輩強大的生殖力衍生了故鄉龐大的親系。

他們是卑微的、底層的一群,是大地上匍匐的一群。他們多麼渴望在天空中飛翔——城市就是他們常常窺視、仰望的天空。從農村包圍城市,是我的故鄉世世代代不死的心。城市擁有任何時代都是最好的物質和精神資源:高層的行政機關,先進的醫療、教育、物流、文化體系。因為求學、患病、購物或者厄運,他們暫時離開了鄉村,坐火車或汽車,沿著血脈的通道,秘密潛人城市,與我會合。鄉音或者他們口中的我在鄉村粗鄙難聽的乳名,就是接頭的暗語。

他們是我的另一個組織,掌握了我的血脈、出身甚至更多的秘密檔案。我其實是他們安排在這座城市的臥底,是潛伏在城市內部的、為故鄉工作的地下工作者。我的衣冠楚楚人模狗樣從另一個角度上來說,不過是為了便於開展工作的一種化妝術。一名潛入城市的臥底,就是我在故鄉的組織掌握的檔案上的真實身份。


那個人站在那裡,手足無措。那個人無論頭髮臉龐和衣著,都與那座很歐化的拱形的大門外觀和來來往往的高檔車輛極不相稱。那個人頭髮蓬亂,皮膚粗糙黝黑,面色愁苦,鬍子拉碴,皺巴巴的襯衫有一塊明顯的印記——顯然那是不習慣出遠門暈車嘔吐的痕跡。那個人的手裡提著一個髒兮兮的蛇皮袋,背上背著一床被子,被面的花色是嶄新而豔俗的那種。那個人的樣子就像一個難民。他的旁邊,是他的兒子,兩手空空,卻因為和他的難民父親站在一起感到尷尬萬分。

他是我的姨父。站在他身邊的是我的表弟。姨父看見我,臉上露出了沒有被人察覺的笑意。他的神情看起來有些激動,但是他並沒有像在故鄉的路上遇見時那樣大叫大嚷,而是竭力保持著克制。待我走近,他顯得訓練有素地慢慢伸出了手,輕輕地用鄉音喚了我一聲——就像電影裡組織派來的人與地下黨員在敵佔區秘密接頭一個樣。

姨父生了兩個兒子。他靠著種幾畝地和做點小生意供兩個兒子上學。大兒子今年高考落榜,可他還想上學。前些日子,姨父從家鄉打電話給我,問我有沒有什麼辦法,讓表弟在省城上一所民辦大學。我說乾脆讓他出去打工算啦,去民辦大學上學能讀到什麼東西?再說那該要老大一筆錢呢,您現在負擔那麼重。可姨父說兒子想讀,做父母的,就成全他麼。從電話裡聽得出,姨父有一種為了兒子豁出去的悲壯意味。

我知道這是我的組織交給我的任務。我必須充分利用我的城裡人身份來完成它。接到姨父的電話以後,我費勁了心機搞到了這座城市幾乎所有民校的情報,內容從建校歷史、學校規模、師資力量、專業結構、收費就業情況等無所不包。最後,我把目標鎖定了市郊一所據說就業率 98% 以上的大學。其實所有的民辦高校只要給錢就能上,但我不能讓姨父把辛辛苦苦積攢的錢往水裡扔。

在單位隔壁的飯館裡,我叫了兩瓶啤酒,姨父說路上車暈得厲害,什麼也吃不下。他幾乎沒動什麼筷子,只是一根接一根地抽菸。倒是表弟一個人自斟自飲把兩瓶啤酒幹了。我想,即使不暈車,這檔兒他也不會有什麼胃口—這座於他缺乏把握能力的城市危機四伏,對表弟前途命運的擔心顯而易見。再說了,每年上萬塊錢的學費,對他也不會是一個小數目。

我領著父子倆打車來到了那所大學報名處。正是報名的高峰時期,校園人山人海。姨父躲到偏僻處,費力地從縫合在褲子上的口袋裡掏出一疊皺巴巴的錢來。辦完手續,手中的錢變成了幾張輕飄飄的收據。我看見姨父的臉虛弱蒼白,手還有些抖。正是炎熱的九月天氣,他不停地用衣服的下擺擦著汗。很多衣著光鮮的人從他面前走過,露出了鄙夷和警惕的表情,仿佛他是一個被繳了械的俘虜。姨父告訴我,學費中的很大一部分,是借來的,要三分的利息。我說那該還到猴年馬月?他咬咬牙說,什麼時候還得清先不管,兒子讀書要緊。

報完名,姨父就要回去,農活不能耽誤,好幾丘田等著灌水呢。他反覆囑咐兒子,囉裡囉唆,表弟都有點煩了。——我盯著表弟不耐煩的臉色,心想,如果這傢伙做了一名城裡人,早晚會成為故鄉的叛徒。

姨父當天來當天回。他坐上回家的班車的時候,天已經全黑了。


我不認識眼前的這個人,雖然他有著和我的故鄉契合的表情和裝束,不同之處是他手裡拿著一張 X 光片。這張 X 光片此次變成了我和故鄉在城裡接頭的暗號。他是我舅舅的﹁特派員﹂。這個在牛皮紙信封裡半露半藏的膠片上記錄了舅舅的腰椎。舅舅是個泥瓦匠,在縣城的某個工地上打工。春節去給他拜年時得知他在縣城幹得不錯,比在家種幾畝地時強很多。可就在前幾天,他不小心從腳手架上摔下來,腰椎斷了。眼前的這個人是他工地上的工友。舅舅讓他帶話給我,要我去問省城的醫生能否不做手術,因為做手術會花光他全家僅有的存款,兩個孩子在縣城讀書就會成為問題。

我從紙袋裡取出這張片子,在陽光下觀望。我看不懂這片子的受傷情況,但我知道,那裡藏著我的舅舅—媽媽的弟弟的腰椎骨胳,它記錄了一個底層人的、靠手藝生活的莊稼漢的、很可能使他永遠失去勞動能力的一次事故。它是來自故鄉關乎命運的一張秘密圖案。

我帶著舅舅的工友來到單位附近的一所大醫院。已是下午四點,專家門診空無一人,我只好衝進了住院部,找到一名正在值班的骨科專家。我要讓我在不暴露身份的情況下從他的嘴中撬出有用的情報。我去買了一盒價格不菲的香菸—某種程度上它可以是一張城市通行證,也是一張名片,用來證明我城市人的身份。我客氣地給他點了煙,並開始運用與城裡人交談的一套話語系統,來表明我和他是同類的人。我儘量讓自己顯得鎮靜和有涵養,語言上既顯得謙恭又不失尊嚴。我臉上的表情也十分到位,沒有露出一點破綻。我費盡周折終於取得了他的信任、尊重和診斷。我成功了。

我聽到的結論是肯定要動手術,不然這輩子就廢了。當我不動聲色地提著裝了舅舅腰椎的牛皮紙袋走出醫院的時候,我的腰椎忽然傳出了一陣劇痛。


並不是每一次組織都會派人來跟我接頭,也有用電話、手機的時候。只要有來自故鄉區號的電話響起,我就心領神會這些只有我才能破解的密碼,我知道,那是故鄉正在給我發出新的指令。

有一天,我接到了一個電話。電話裡是個中年婦女的聲音,帶著哭腔。她問我是否記得她,曾經住在離我家隔幾棟房子的地方。她說我小時候叫她姑,她還經常抱我,小時候的我長得可胖呢。可後來她嫁到了我村隔壁的村子裡。她說是從我在老家的爸爸那裡要到我的手機號碼的。

說實話我對她並沒有什麼印象,只覺得聲音有點熟悉。再加上手機信號不太好,她的聲音帶著哭腔有些失真,我完全不知道她是誰。離開故鄉多年,故鄉的人和事,已有許多退出了記憶。但她的鄉音讓我對她的真實身份沒有產生一絲懷疑。

我知道,她是自己人。

電話裡開始絮絮叨叨地說事兒。她說她的兒子開長途汽車跑貨運,前幾天在廣州被別人的車撞了。她的兒子還住在醫院裡,傷得很重,肇事司機被當場抓著了,可不知什麼原因,不久就放了出來。這場車禍從此找不著事主了。兒子的傷得治,可眼看著快沒有錢了。她在電話裡哭起來,說這都是因為咱們是鄉下人啦,咱們在廣州人生地不熟的,石頭擲天也沒用啊。聽說只要有人給廣州打個電話那邊就會認真辦理。你是咱村裡的能人,聽說在省城當大官呢,你就給我打個電話過去吧,那邊也是省城,你這裡也是省城,省城還有不聽省城的?求求你了大侄子救救命呀,事情辦成後我打幾斤麻油專門到省城感謝你呀……

我沒有辦法幫這個忙。她想得太天真了。在廣州,除了家鄉的一些打工仔,我並不認識誰,更沒有打個電話就能把事兒給辦了的能力。廣州是我鞭長莫及的城市。我和那邊的城裡人沒有任何交情。我知道她正面臨的困境,可我一點辦法也沒有。我根本不是什麼官兒,我在城裡的身份只是一個靠寫字兒謀生的小文人。而此刻,我成了她落水時想抱著的一根虛弱的稻草。我艱難地回絕了她。她顯得多麼失望!在電話的最後,她嘟嘟囔囔,語氣中充滿了對我見死不救的埋怨。

我知道我讓我的組織失望了。我想我的故鄉肯定會有一段時間對我的忠誠產生懷疑。他們會以為我背叛了他們。﹁哼,人家是城裡人了,哪裡會看得起咱們鄉下人﹂,故鄉的人在議論我時肯定會這麼說。我將因此暫時蒙受冤屈。但作為一名臥底,蒙受冤屈是常有的事。對此,我已逆來順受。

我的親友們紛至沓來。可為了做好一名臥底,我必須承受更多。我必須讓自己越來越像一名城裡人。我必須討好領導,團結同事,善待他人,以取得這座城市更多的信任,從而讓自己在這座城市扎穩腳跟。我甚至對單位的守門人都不敢得罪,親友們給我帶來了紅薯辣椒我都要分給他一些,生怕他把故鄉來的人兇神惡煞地堵在門外。我必須更廣泛地熟悉城市,與更多的城裡人交朋友,以獲得更多的信息,竊取更多故鄉需要的情報。我必須擁有更多的資源:包括人脈資源和信息資源。我必須夜以繼日地工作。我的組織並不發給我所需要的資金,獎勵基本靠口碑,而我必須掙下所有的活動經費︵包括接待故鄉親友的食宿費、交通費和其他開支︶。在這個城市生活,我常常為資金的緊缺一籌莫展。我表面像城裡人一樣樂呵,可我內心的困窘,有誰知曉?一個臥底內心深埋的悲涼,又有誰分擔?我經常孤單地行走在這座城市的街頭,腳步遲疑,一方面對故鄉的命運憂心忡忡,一方面又為是否接聽顯示為故鄉號碼的座機、一直響個不停的手機而猶豫不決。


今夜故鄉又有人入城,說是半夜會來。從電話裡的聲音我知道那是我的一個遠親。他壓低著嗓音說他正在來城裡的夜班車上,可能要十二點左右才能到達。問他到底有什麼事,他欲言又止,顯然他說話不太方便。他的聲音在夜晚的班車上含混不清,呼呼的風聲和車輪在地面行駛的尖銳聲音隱約可聞,很讓我有一些風聲鶴唳的感覺。他乾脆說電話裡說不清。等到見面時一切就知道了。

接電話的前夕,我剛剛送走了一批來城裡的親友。家裡的餐桌上杯盤狼藉,我還來不及把一切清理乾淨。我在城裡用微薄的積蓄和一幢用對我來說是巨額的貸款買下的房子建成了故鄉在城裡的秘密交通站。我坐在親友們沉重的身體坐得凹陷了的沙發上,一動也不想動。故鄉親人們的蜂擁而來已經讓我疲憊不堪。我超負荷地在為我的組織工作。老實說,我受夠了。但我想起我的故鄉依然在苦難中掙扎,我的親友依然嘍蟻般活在蒼茫大地上,而我對他們的熱情款待和為他們的事情奔忙多少可以給他們帶來一星半點的希望和安慰,就一點脾氣也沒有了。是的,就像一名臥底不敢背叛他的組織,我怎麼會忍心改變對故鄉親友的忠誠,成為我貧弱不堪的故鄉千夫所指的叛徒?!

今夜,我依然靜靜地坐在家裡的燈光下,心平氣和地等著入城的親友,將我家的門,篤篤叩響。

分類
讀書

一塊小黑板

田園將蕪

——後鄉村時代記事

作者:江子

ISBN:978-7-224-10540-7-

江西人民出版社


我在這個城市所在的小區,旁邊是一排失地農民的安置房。與周圍到處是被稱為﹁花園﹂﹁街區﹂﹁新城﹂的住宅區不太協調的是,房子似乎有些年份了,而且樣式簡單,外表是裸露的、顯得陳舊的水泥。

沒有圍牆的五棟房子,外面看起來有些雜亂無章︵有一小塊地方甚至還種了幾行菜︶。每一天,我進出小區,都可以看到一些衣著簡陋的人在房子之間出沒。他們說著最為正宗的本地方言。有時候在樹下,他們還會湊在一起打牌,坐下是城裡很少見的那種舊竹椅。抓牌的時候,椅子會發出吱吱呀呀的響聲。

我不禁對他們產生了好奇。這些失地的農民,糊裡糊塗成了城裡人。他們的生活狀態發生了什麼樣的變化?他們有怎樣的悲歡?

在這排房子最前面的那棟房的牆上,砌了一塊小黑板。

最前面的房子是所有人必經的路口。從路口轉過去,就是通往市區的大路。我每天上下班都要經過那塊小黑板。有時候我會看到有人在寫字,旁邊圍著一群人在等著他繼續書寫。而大多數時候,書寫者已經離開,只留下一些字跡在上面,落款是﹁××社區﹂。而所書內容,無疑和這排房子的住戶有關。

我曾經是個老師,自然對黑板之類的東西有緣自職業的敏感。而書寫在上面的消息,又多少讓我對我家旁邊的那排房子裡的居民的生活有一些了解。

當然,黑板上大多是一些簡單的通知。有提請小區適齡兒童人學的:﹁如果你的孩子年滿 6 周歲,請帶好戶口簿於 9 月 3 日之前攜適齡兒童到學校報名。﹂有通知參加娛樂活動的:﹁我區將舉行好家庭體育競賽,歡迎本社區廣大家庭踴躍報名參加。有意者請到社區辦報名。聯繫人:×××。﹂這兩天,颱風﹁鳳凰﹂登陸我省,黑板上立即有了一項關於颱風登陸的通知:﹁據有關氣象部門消息,颱風﹃鳳凰﹄將於今日登陸我省。請大家及時做好應對準備,提防花盆、磚塊被颱風吹落砸傷行人,注意安全。請大家相互轉告。﹂

黑板上最讓小區居民關注的,大概是那些關於低保戶的公示了。隔一段時間,就會有一條公示出現在黑板上。

現將我用手機拍攝保存下的一條抄錄如下:

2007 年×月低保申請戶情況公示
本著充分發揚民主的宗旨,徵求廣大群眾意見,現將本月申請低保人員情況公布如下:
姓名 年齡 家庭人口 每月家庭收入
萬蒼生 48歲 三口 200元
萬孝全 40歲 三口 150元
李望桃︵女︶ 41歲 二口 119元
熊狗仔 52歲 三口 120元
如有不實,請撥打投訴電話:×××××××

我並不認識上面標示的四個人中的任何人。可是這樣一條公示讓我無由擔心:在這樣消費水平很高的城市裡,他們每月不到兩百塊錢的收入怎麼生活?

我死死地盯著第三個每月家庭收入裡的 119 這個數字。——為什麼是這個數字?它的諧音是﹁要要救﹂,這也是火警電話。是這戶人家實際的收入水平還是戶主出於對自身生活的怨氣故意虛報?

有段時間,本市要爭創全國衛生城市。小黑板上許多天都書寫了與爭創全國衛生城市相關的內容。有倡議書,注意事項,具體達標條款,等等。

有一天,小黑板上發布了一項通知:﹁現我市爭創全國衛生城市已到了攻堅階段。希望廣大市民密切配合。明日下午三點整,本小區內所有低保戶請自帶掃帚到××路某某飯店旁集中,由社區領導統一安排進行大掃除,任何人不得缺席,違者扣發三個月低保金。﹂

通知發布的第三天,小黑板上又換上了新的內容:﹁昨天下午,有低保戶萬國元公然視社區通知於不顧,不參加社區組織的大掃除活動,在整個全國衛生城市爭創活動中影響惡劣。現經社區研究決定,扣發萬國元低保金三個月。﹂

第四天我經過的時候,那條通知依然赫然在目。只是在通知空白的地方,有三個寫得大大的歪歪扭扭的字。字體風格完全迥異於通知的書寫,而且用的是不同於粉筆的類似板結的石灰塊之類的材料。三個字的後面是一個大得有些誇張的驚嘆號。

那三個字是:我有病!

分類
程序

在 CentOS 8 上使用 Xvfb 和 PyAutoGUI

本文介紹在 CentOS 8 上使用 PyAutoGUI 操作運行與 Xvfb 桌面環境中的 Firefox 所需的準備。由於所操作網頁需要登錄並有 OTP,所以還需要使用 VNC 來手動輸入密碼以做準備工作。

安裝 Xvfb

sudo yum install xorg-x11-server-Xvfb
#啓動 Xvfb
Xvfb :0 -screen 0 1366x768x24+32 -br +bs -ac &
#修改環境變量 DISPLAY
export DISPLAY=:0
#查看環境變量 DISPLAY
$DISPLAY
#查看 Xvfb 進程
ps -ef | grep Xvfb

如果只是爲了使用 webdriver 操作瀏覽器那麼這樣安裝運行 Xvfb 後就可以了,但是我們要使用 PyAutoGUI,所以還有依賴要裝。

安裝火狐瀏覽器

我個人比較偏好 ESR 版火狐,下載後解壓就可以使用了。

tar -xf firefox-115.5.0esr.tar.bz2
cd Firefox
./firefox
#如果要指定窗口大小可以
./firefox -width 1350 -height 764

安裝 x11vnc 服務

其實只要下載執行檔,運行即可,非常簡單。不過 x11vnc 12 年未有更新,不知道還能用多久。如果那天不能用了可以嘗試 Tiger VNC。

mv x11vnc-0.9.13_amd64-Linux x11vnc
chmod +x x11vnc
#運行 vnc server,密碼設置長一點,端口是 9999
./x11vnc -display :0 -ncache 0 -passwd piHrcHxmJauvIOftenUseA64digitPasswordpokiHJHQWdsgcGFTG -rfbport 9999
#上面命令在一次連接後會停止,如果要服務一直可以用,就加上 forever 參數
./x11vnc -display :0 -ncache 0 -passwd piHrcHxmJauvIOftenUseA64digitPasswordpokiHJHQWdsgcGFTG -rfbport 9999 -shared -forever
#注意服務器的網絡防火牆要打開 9999 端口
#用 nmap 查看端口是否打開
nmap -p 9999 1.1.1.1

Fedora 本地可以安裝 Remmina 作爲 VNC 客戶端。

sudo dnf install remmina -y
#如果想要使用 socks5 代理
proxychains4 /usr/bin/remmina

打開 Remmina 後在地址欄的協議選項裏選擇 VNC,然後地址填 1.1.1.1:9999 回車就可以連上服務器的桌面並看到剛剛打開的火狐瀏覽器進行設置了。點左上角的 + 號可以添加 profile,這樣下次雙擊就能連上 VNC 了。

安裝與配置 PyAutoGUI

我自己從源碼編譯的 Python 一直提示沒有 _tkinter 模塊。搜了半天沒有解決,於是使用系統自帶的 Python 3.9 得以解決。雖然我是用 Python 3.11 開發的,但是 Python 3.9 跑起來也沒問題。

import _tkinter # If this fails your Python may not be configured for Tk
ImportError: No module named _tkinter
#安裝 Python 3.9 以及依賴
sudo yum install libnsl ImageMagick xclip
sudo yum install python39 python39-tkinter

#xdotool 並不支持 Xvfb,所以不用裝
#sudo yum install xdotool

#創建虛擬環境
cd your_project
python3.9 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

示例代碼

import json
import random
import sys
import requests
import time
import logging
import os
import pyperclip
import subprocess
from urllib.parse import urlparse, parse_qs, urlunparse
from dotenv import load_dotenv
import psutil
import pyautogui
import pyscreeze

script_dir = os.path.dirname(os.path.realpath(__file__))

load_dotenv()
api_key = os.getenv("API_KEY")
proxy = os.getenv("PROXY")
de = os.getenv("DE")
batch = int(os.getenv("BATCH"))

if proxy is None:
    proxies = None
else:
    proxies = {"http": proxy, "https": proxy}

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger()
# Create a file handler to save logs to a file
log_file = os.path.join(script_dir, "logfile.log")
file_handler = logging.FileHandler(log_file)

# Set the logging level for the file handler (if different from the root logger)
file_handler.setLevel(logging.INFO)  # Adjust the log level if needed

# Create a formatter and add it to the file handler
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)

# Add the file handler to the logger
logger.addHandler(file_handler)


def activate_window(window_title):
    if de == "Xvfb":
        firefox_exists = False
        for proc in psutil.process_iter():
            try:
                if "firefox" in proc.name().lower():
                    firefox_exists = True
                    break
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                pass
        if firefox_exists is False:
            subprocess.Popen(
                [
                    "/home/centos/fred/programs/firefox/firefox",
                    "-width",
                    "1350",
                    "-height",
                    "764",
                ]
            )
            logger.info("firefox started")
            time.sleep(10)
    elif de == "Xfce":
        command = f"xdotool search --onlyvisible --name '{window_title}' windowactivate"
        subprocess.run(command, shell=True)


def wait_for_image(image_path, timeout=10):
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            location = pyautogui.locateOnScreen(
                image_path, region=(0, 0, 1360, 760), confidence=0.9, grayscale=True
            )
            if location:
                return location
        except pyautogui.ImageNotFoundException:
            pass
        time.sleep(1.5)
    return None


def wait_for_images(image_path, timeout=10):
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            location = pyautogui.locateAllOnScreen(
                image_path, region=(0, 0, 1366, 760), confidence=0.9
            )
            if location:
                return list(location)
        except pyautogui.ImageNotFoundException:
            pass
        except pyscreeze.ImageNotFoundException:
            pass
        time.sleep(1.5)
    return []

def right_click():
    pyautogui.mouseDown(button="right")  # Perform a right-click
    time.sleep(0.1)  # Adjust the delay if needed (time in seconds)
    pyautogui.mouseUp(button="right")  # Release the right-click


def triple_click(x, y):
    for _ in range(3):
        pyautogui.click(x, y)
        time.sleep(0.1)

def get_data(obj):
    start_time = time.time()
    time.sleep(0.5)
    activate_window("Mozilla Firefox")
    time.sleep(0.5)
    # load webpage
    url_profile = obj["contact_name_href"]
    if url_profile.find("?") != -1:
        url_profile = url_profile[: url_profile.find("?")]
    logger.info(url_profile)
    # open a new tab
    # pyautogui.hotkey('ctrl', 't')
    pyautogui.hotkey("ctrl", "l")
    time.sleep(0.5)
    pyautogui.typewrite(url_profile)
    time.sleep(0.5)
    pyautogui.press("enter")
    time.sleep(8)

    # check page loaded
    image_path = os.path.join(script_dir, "img/ContactDetails.png")
    loaded = wait_for_image(image_path, 40)

    # Box(left=535, top=541, width=74, height=19)
    if loaded:
        logger.info("Page loaded")

    # LinkedIn
    image_path = os.path.join(script_dir, "img/LinkedIn.png")
    l = wait_for_images(image_path, 3)
    l_index = 0
    retry = 0
    temp_l = None
    while l_index < len(l):
        logger.info(f"l_index:{l_index+1}/{len(l)}")
        if temp_l is not None:
            if temp_l[0] == l[l_index][0]:
                l_index = l_index + 1
                continue
        li = l[l_index]
        pyautogui.moveTo(li, duration=2, tween=pyautogui.easeInOutQuad)
        time.sleep(0.5)
        pyautogui.click(li)
        time.sleep(2)
        pyautogui.hotkey("ctrl", "l")
        time.sleep(0.5)
        pyautogui.hotkey("ctrl", "c")
        time.sleep(0.5)
        pyautogui.hotkey("ctrl", "w")
        time.sleep(0.5)
        copied_text = pyperclip.paste()
        url = copied_text.strip()
        url = get_link_from_redirect(url)

        if url is not None and url != "":
            # logger.info(len(url))
            if url.find("linkedin") == -1:
                logger.info("not found 'linkedin'")
                if retry < 3:
                    retry = retry + 1
                else:
                    l_index = l_index + 1
                continue

            # personal LindedIn
            if li[0] < loaded_x_left:
                obj["LinkedIn_Personal_URL"] = url
            else:
                obj["LinkedIn_URL"] = url
        temp_l = li
        l_index = l_index + 1

    # Supplemental_Email
    image_path = os.path.join(script_dir, "img/Supplemental.png")
    l = wait_for_image(image_path, 3)
    if l:
        pyautogui.moveTo(
            l[0] + l[2] + 50 + random.randint(-5, 5),
            l[1] + l[3] / 2,
            duration=1,
            tween=pyautogui.easeInOutQuad,
        )
        time.sleep(1)
        right_click()
        time.sleep(1)
        pyautogui.hotkey("l")
        time.sleep(1)
        pyautogui.hotkey("esc")
        # [email protected]
        copied_text = pyperclip.paste()
        obj["Supplemental_Email"] = copied_text.strip()

    # Local address
    image_path = os.path.join(script_dir, "img/Local.png")
    l = wait_for_image(image_path, 3)
    if l:
        x = l[0] + l[2] + 30 + random.randint(-5, 5)
        pyautogui.moveTo(x, l[1] + l[3] / 2, duration=1, tween=pyautogui.easeInOutQuad)
        time.sleep(1)
        triple_click(x, l[1] + l[3] / 2)
        time.sleep(1)
        pyautogui.hotkey("ctrl", "c")
        time.sleep(1)
        # [email protected]
        copied_text = pyperclip.paste()
        obj["Local_Location_Address"] = copied_text.strip()

def main(max_id):
    failed_list = []
    for i in range(batch):
        logger.info(f"batch:{i+1}/{batch}")
        obj = get_job()
        if obj is None:
            continue
        if obj["contact_name_href"] == "" or obj["contact_name_href"] is None:
            res = post_job(obj)
            logger.info(res)
            continue

        if obj["id"] > max_id:
            logger.info("max_id reached")
            continue
        obj = get_data(obj)
        if obj["message"] != "success":
            if obj["id"] in failed_list:
                res = post_job(obj)
                logger.info(res)
            else:
                failed_list.append(obj["id"])
            continue

        res = post_job(obj)
        logger.info(res)

if __name__ == "__main__":
    # time.sleep(10)
    # countdown
    for i in range(6):
        logger.info(f"countdown:{6-i}")
        time.sleep(1)

    # get max id from command
    if len(sys.argv) > 1:
        max_id = int(sys.argv[1])
        logger.info(f"max_id:{max_id}")
    else:
        max_id = 9999999

    main(max_id)
分類
程序

下載指定版本 Chromium 與其 Webdriver

以下載 Linux 平臺 119 版本 Chromium 與其 Webdriver 爲例:

首先在 Chromium 版本發佈頁 找到想要下載版本的 Branch Base Position 比如 1204232。然後在 https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/ 中搜索 1204232。如果發現搜索結果爲空,不用怕,把最後一位去掉試試,這時就會搜到 1204234。也可以再刪掉一位,就會出來更多相近的結果。點進去後下載 chrome-linux.zip 和 chromedriver_linux64.zip 就可以了。

unzip chrome-linux.zip
cd chrome-linux
#查看 Chrome 版本
./chrome --version
#Chromium 119.0.6045.0

在 Selenium 中指定 Chrome、Webdriver 與 Profile 位置

import os
import 
import datetime
import undetected_chromedriver as uc

MAX_RUNTIME_SECONDS = 240
# Check for and kill any Chrome processes that have been running for too long
for proc in psutil.process_iter():
    try:
        if "chrome" in proc.name().lower():
            create_time = proc.create_time()
            elapsed_time = time.time() - create_time
            if elapsed_time > MAX_RUNTIME_SECONDS:
                proc.kill()
                print("killed" + proc.name())
    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
        pass

script_dir = os.path.dirname(os.path.realpath(__file__))
profile_dir = os.path.join(script_dir, "chrome_profile")
path_to_chrome_binary = os.path.abspath(
    script_dir + "/../../" + "chrome-linux64/chrome"
)
driver_executable_path = os.path.abspath(
    script_dir + "/../../" + "chromedriver-linux64/chromedriver"
)
proxy = "socks5://127.0.0.1:8080"

options = uc.ChromeOptions()
options.add_argument("--disable-notifications")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-save-password-bubble")

# Limit CPU and memory usage
options.add_argument("--disable-software-rasterizer")
options.add_argument("--disable-extensions")
# options.add_argument("--disable-webgl")

options.add_argument(proxy)
options.binary_location = path_to_chrome_binary

# if os.getenv("DEBUG") != "True":
#     options.add_argument("headless=True")

prefs = {
    "credentials_enable_service": False,
    "profile.password_manager_enabled": False,
    "profile.privacy_sandbox_prompt_enabled": False,
    # "profile.managed_default_content_settings.images": 1,
}
options.add_experimental_option("prefs", prefs)

try:
    driver = uc.Chrome(
        executable_path=driver_executable_path,
        browser_executable_path=path_to_chrome_binary,
        options=options,
        version_main=119,
        user_data_dir=profile_dir,
        # use_subprocess=False,
    )
except Exception as e:
    return None


driver.set_window_size(1366, 768)
driver.set_page_load_timeout(30)

#fix timeout bug
try:
    driver.get(url)
except Exception as e:
    try:
        #send Esc key to stop loading
        driver.find_element(By.XPATH, '//body').send_keys(Keys.ESCAPE)
        time.sleep(1)
        # check page loaded
        WebDriverWait(driver, 10).until(
                EC.visibility_of_element_located(
                    (By.XPATH, "//dl[@class='dfn']")
                )
            )
    except Exception as e:
        #take a screen shot
        png_name = datetime.datetime.now().isoformat()[:19]
        driver.save_screenshot(png_name + "_load" + ".png")
        return None