2015年11月21日 星期六

multiple thread 存取共享資料的小技巧

最近看到不錯的寫法, 備忘一下。

通常我們不希望 main thread 被 block [*1], 所以會另外開 worker thread 處理耗 CPU 的工作。主要的需求是避免 block main thread, 其次是縮短完成工作的時間。

由於 main thread 和 worker thread 需要互相傳遞一些資料, 有可能造成 race condition, 導致程式行為有時不正確或是掛掉。這裡討論幾種避免 race condition 的作法。

用一個 "global" lock 保護整個 thread 的資料

worker thread 全部函式都用同一個 lock 保護。

  • 優點: 容易實作。
  • 缺點: main thread 在 worker thread 忙的時候存取共享資料, 還是會被 worker thread block。

不同群共享的資料用不同的 lock 保護

和上個作法類似, 不過資料分群用不同 lock 保護。

  • 優點: 減少 main thread 被 block 的機會。
  • 缺點: 實作有些複雜, 可能會漏保護到新加的變數。並且 main thread 仍有可能被 block。

避免共享資料

如果資料只有一個 thread 會用到, 就直接轉交給另一個 thread (改變 ownership)。若兩個 thread 都會用到, 先複製一份再轉交給另一個 thread。沒有共享的資料, 就不需用 lock 保護了 (但仍需透過 message loop 或 queue 傳遞資料到另一個 thread)。

  • 優點: 複製的時間不長, 且可以掌控占據 main thread 的時間, 完全不受 worker thread 影響
  • 缺點: 需要仔細設計複製和傳遞資料的機製, 確保沒有共享資料。

需要同步資料的時候, 記得只能允許一個方向的 block。比方說 main thread 可以透過 conditional variable 來 block worker thread, 但不允許反過來的操作, 這樣就不會有 dead lock。

不用 lock 的注意事項

每個函式都要放 assert 確保函式在正確的 thread 執行。若一個 class 有兩份資料分別在兩個 thread 下執行的話, 可以考慮訂兩個 struct, 比方說 MainData 和 WorkerData, 然後透過 private method 存取資料, 藉此確保 thread safe:

// C++
class MyClass
{
public:
   ...

private:
  MainData& GetMainData();
  WorkerData& GetWorkerData();

  struct MainData {
    ...
  };
  struct WorkerData {
    ...
  };
  MainData m_mainData;
  WorkerData m_workerData;
};

MainData& MyClass::GetMainData()
{
  assert(InMainThread());
  return m_mainData;
}

WorkerData& MyClass::GetWorkerData()
{
  assert(InWorkerThread());
  return m_workerData;
}

這樣程式寫錯時會造成 assert failed, 可以很快地修正。

備註

*1 比方說 daemon 的 main thread 會不斷收新的連線, GUI 的 main thread 會收使用者的輸入。main thread 被 block 而沒有反應的話, 使用者會覺得軟體有問題。

2015年10月3日 星期六

node.js 開發備忘

前陣子用 node.js 寫了一個上線的服務, 沒有意外的話接下來有一大段時間不會碰 node.js。備忘一下有用到和以後可能會用到的東西。

何時以及為什麼要用 node.js?

node.js 開發

主要弱點

其它

  • coding style: callback 函式的第一個參數總是放 error, 沒有 error 時傳入 null。
  • nodemon 在更新程式後自動更新。
  • logger: 有很多家, 我是用 winston, 還不錯用, 寫入 JSON log, 再用 jq 讀。
  • posix 使用缺少的 POSIX API (如 setrlimit), 還有可以參考它如何包 C/C++ 程式給 node.js 用。
  • web server: 大家都用 express, 要用 HTTPS 或 POST 需要作些設定。看起來彈性很大的 web server。
  • 用舊版 node.js (0.10以前) 記得設 http max socket 提高對每個 domain 送出的連線數量。

2015年10月2日 星期五

2015年9月29日 星期二

觀察 HTTP/HTTPS 傳輸的內容以及防範 man-in-the-middle attack

看到許多強者推薦 mitmproxy (MITM 是 man-in-the-middle 的意思), 試用了一下的確超好用的。它用 man-in-the-middle attack 的方式觀察 (甚至更改) HTTP/HTTPS 傳輸的內容。

How To: Use mitmproxy to read and modify HTTPS traffic 介紹原理和用法, 設定很簡單。

官方文件也很清楚, 可交叉參考:

這裡摘要一下 mitmproxy 作為 Transparent Proxy 的方式和原理:

  1. 備好一台機器 R 作為觀察目標裝置 D (如手機) 的 gateway, 這樣就完成 man-in-the-middle 的設置。
  2. 設定 R 的 firewall, 重導往外連往 port 80 和 443 的連線到在 R 上執行的 mitmproxy。
  3. 在 D 上安裝 mitmproxy 的 certificate, 這樣 D 會相信 mitmproxy 幫自己簽的 certificate。

測試 Android 手機在 MITM 情況下 App 的反應

我是用 ASUS Zenfone 2, 不過 App 的測試結果應該和手機無關。

結果如下:

  • Android OS 有出現訊息說網路可能被監控了, 但清掉訊息後就沒再出現了, 使用者應該不會注意到。
  • 沒裝 mitmproxy certificate 的情況, Google Play 會出現連線錯誤; 裝 certificate 後可以用 Google Play 安裝 app。
  • 沒裝 mitmproxy certificate 的情況, Chrome 會警告有問題; 有裝 certificate 會放行。
  • 沒裝 mitmproxy certificate 的情況, Gmail 會如常運作, 看不到 HTTPS 傳的內容。裝 certificate 的後可以看到 HTTPS 傳的內容。雖然 Gmail 用自己訂的資料格式, 在搜尋信件時, 可看到搜尋字、回傳信件的標題、內文前面幾句的文字。
  • 另外在有裝 mitmproxy certificate 的情況測了 Hangouts, Facebook, Messenger, Skype, Line。它們的主要傳輸內容都不是用 HTTPS, 改天再用 SSLsplit 看看它們傳什麼。
    • Line 會說 Wi-Fi 有問題無法用需要連線的功能 (訊息、電話、桌機 QR Code 登入), 剩下的 App 都可以用。
    • Facebook 有部份內容用 HTTPS 傳, 並且用 JSON 作為內文編碼。
    • Facebook 和 Messenger 有時候會顯示連線問題, 但放一陣子又通了, 也許它們有偵測網路情況用不同方式連線吧。
    • Hangouts 意外地都沒有安全警告, 有沒有裝 mitmproxy certificate 都一樣, 可以正常使用。

其它 Web Proxy

這些也是很有名的 Web Proxy, 有看到朋友推薦。不確定作為 transparent proxy 的效果如何, 至少監控本機的訊息應該滿方便的, 留著備忘。

防範 man-in-the-middle attack

Scott 提到可用 Certificate Pinning 防 MITM attack。原理是只相信自己指定的 certificate (或幫你簽 certificate 的 certificate), 這樣就會忽視攻擊者裝到作業系統的 root CA。當 SSL handshake 的 certificate 被替換的時候, 會變成 handshake error。Certificate Pinning 似乎有些不同的作法, 先記住關鍵字, 之後再來研究。

另一個好像可行但可能有問題的解法是不用 OS 提供的 certificate, 直接用自己包在 app 裡的 certificate (放 server 用的 certificate 或簽它的 CA 的 certificate, 意思一樣)。雖然這樣不用擔心裝置被裝了有問題的 root certificate, 但和 self-signed certificate 一樣, 需要處理另一個問題 (原本的問題): 如何確定 app 內裝了正確的 certificate? 這個難題原本是由 OS 處理, 假設 OS 沒安裝有問題的 root certificate, 所以可以用它的 root certificate。

2015-10-01 更新

Victor 告知, 發現 Gmail 應該有檢查 certificate, 我又測了一下, 並更正內文。

Gmail 和 Google Play 有檢查 certificate, 只是使用者從系統刪除信任的 certificate 時, app 沒有同步更新, 仍會相信這個 certificate。另一方面, Chrome 有同步更新。

HTTP library 支援 gzip response body 的情況

為了節省 HTTP response 的傳輸量, HTTP 有提供 request header Accept-Encoding: gzip 要求 server 「可以的話, 用 gzip 壓縮 body 再傳回來」, 還有 response header Content-Encoding: gzip 告知 client 「response body 有用 gzip 壓縮」。

在這樣的規定下, 推測 HTTP library 可能會有兩種實作方式:

  • 不處理。使用者自己加 request header "Accept-Encoding: gzip", 然後自己解 response body
  • 有個 flag 開啟後會自動處理。自動加上 "Accept-Encoding: gzip", 然後自動解 response body 並且移掉 response header "Content-Encoding: gzip"。另外要註明 API 取得的 content length 是否會受 gzip 影響, 結果如何變化。

不過實際情況是:

  • iOS NSURLConnection 文件沒有提到這件事, 但是會自動加 request header 並自動解 response body, 不過沒有移掉 response header "Content-Encoding: gzip" (Ref., 實測結果確實如此, 不需手動加 Accept-Encoding: gzip 就有作用)
  • Android HttpURLConnection 指明預設就會作, 並會清掉 Content-Encoding 和 Content-Length。可以用 Accept-Encoding: identity 覆寫預設值 (Ref.)。
  • curl 開啟 flag CURLOPT_ACCEPT_ENCODING 後會支援, 也是自動加 request header 並自動解 response body, 不過沒有移掉 response header "Content-Encoding: gzip" (Ref.)

結果只有 Android 的實作符合預期。使用前還是要先測過才準啊。

附帶一提, http://httpbin.org/ 會回傳 request/response header, 用來測試 HTTP library 很方便。或是用 mitmproxy 作 transparent proxy 也可以, 相對之下費工一點, 不過看得最清楚。

2015年9月21日 星期一

Android 線上原始碼

偵測網路環境是否有 Captive Portal (使用 Wi-Fi 時會先導到登入網頁)

Captive Portal 是指在可以正常用網路前, 先導到一個登入網頁, 登入後才可以正常上網。通常用在需要付費上網的地方, 或是飯店提供免費但需帳號登入的 Wi-Fi。不過不限於 Wi-Fi 連線

這樣作的好處是使用者可以透過網頁看到比較詳細的說明, 像是「使用者同意書」、「使用限制」、「收費標準」等。對於 Wi-Fi 連線的供應者, 在連上 Wi-Fi 時登入 Wi-Fi 帳號和密碼, 就無法提供這些資訊。

但缺點是使用裝置會誤以為有網路可用 (因為 Wi-Fi 層級有通), 但其實還不能用。如果用的 app 沒有作進一步的偵測, 不會開啟登入網頁, 就無法上網了。使用者會覺得「Wi-Fi 有通 app 卻不能用」, 而覺得是 app 的問題。

Android 4.0.1 開始有內建一套偵測機制 [*1], 發現有 Captive Portal 時會通知使用者:

private boolean isCaptivePortal(InetAddress server) {
    HttpURLConnection urlConnection = null;
    if (!mIsCaptivePortalCheckEnabled) return false;

    mUrl = "http://" + server.getHostAddress() + "/generate_204";
    if (DBG) log("Checking " + mUrl);
    try {
        URL url = new URL(mUrl);
        urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setInstanceFollowRedirects(false);
        urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
        urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
        urlConnection.setUseCaches(false);
        urlConnection.getInputStream();
        // we got a valid response, but not from the real google
        return urlConnection.getResponseCode() != 204;
    } catch (IOException e) {
        if (DBG) log("Probably not a portal: exception " + e);
        return false;
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
    }
}

作法是送 HTTP request 到 http://clients3.google.com/generate_204 [*2], 然後看 HTTP response code 是否為 204 No Content。另一個作法是連 http://www.google.com/blank.html, 然後檢查 Content-Length 是否為 0。

不過當使用者用 HTTPS 上網時, 這招就不管用了, 連線可能出現 certificate error 或是沒反應。原因是 HTTPS 會先加密連線才傳 HTTP, 提供 Wi-Fi 的 AP 無法偵測到有 HTTP 連線, 自然也就無法重導到登入頁面。這正是 HTTPS 要的結果: 使用者送出的連線不該被任意地導到其它地方 (會讓使用者誤以為那是他連往的頁面)。所以....這招應該會在 HTTPS 普及後而愈來愈少人用吧。另一方面也沒有更好的作法可以讓 Wi-Fi 供給者先顯示訊息再讓使用者登入使用。

備註

1. 5.1.1 r6的程式碼有點不同, 變得更一般化, 可自訂測試用的網址還有將 200 + 無資料視為 204, 不過概念一樣。

2. 原始碼是用 http://clients3.google.com/generate_204, 不過用 http://www.google.com/generate_204 也通

參考資料

2015年9月18日 星期五

Web Proxy 是否能處理 HTTPS 連線?

在討論 HTTPS Proxy 前, 要先分清楚是在講那件事:

  • Proxy 和 client 之間的連線用 HTTPS
  • Proxy 允許 client 透過它往外用 HTTPS 連線

前者的好處是保護 client 和 Proxy 之間的連線, client 若是用 Wi-Fi 的話, 可藉此加密所有送出的 HTTP 連線, 防止有人偷聽 Wi-Fi 連線。

後者是本篇要討論的對象: Web Proxy 是否能支援 client 對外的 HTTPS 連線? 又是如何作到的? 畢竟 HTTPS 是基於 SSL 連線的 HTTP, 既然資料都用 SSL 加密了, Proxy 看不到 HTTP header 和 body, 要怎麼作好 Web Proxy 的角色?

用 HTTP tunnel

Proxy 接受 HTTP 1.1 的 CONNECT method, 讓 client 用 CONNECT 對外打通一個 TCP 連線 (又稱 HTTP tunnel), 然後只作 TCP packet forwarding, 於是 client 要作什麼都可以。比方說用 TCP 連線作 SSL handshake 轉成加密連線, 接著就可以走 HTTPS, SPDY 或 HTTP 2 了。

這個作法下, Proxy server 不知道 client 對外傳的內容, 只能經手封包, 不能作其它事。

置換 HTTPS 的 certificate

Proxy 一樣接受 CONNECT method 建立 HTTP tunnel, 不過在 SSL handshake 時, 換掉目的地 Web Server 回傳的 certificate, 改用 Proxy 自己的。然後分別和 client 和外部 Web Server 完成 SSL handshake, 形成這樣的連線:

  • client <-> Proxy: 用 Proxy 的 certificate 建立 SSL 連線, 使用 key K1。
  • Proxy <-> 外部 Web Server: 用 Web Server 的 certificate 建立 SSL 連線, 使用 key K2。

Proxy 收到 client 的資料先用 K1 解密, 再用 K2 加密傳出去; 資料回來的時候反過來比照辦理。這樣 Proxy 就能得知傳送的內容, 可以作 content filtering (如學校可以擋掉成年人才能看的內容)。

這個作法和 SSL man-in-the-middle attack 一樣, 瀏覽器為了避免這類攻擊, 會檢查 certificate 和對應的 public key 資訊是否一致, 不一致時會跳出警告叫使用者不要繼續使用。

不過在可控制的環境下, IT 部門可以在使用者的電腦上裝上 Proxy 的 certificate, 這樣瀏覽器就不會跳出警告。另一方面, 使用者還是可以點選 HTTPS 的圖示檢查 certificate 資訊, 確認 certificate 是否有被換過, 有的話表示傳輸資料有可能被監聽。有興趣的話可以自己裝 mitmproxy 作一個 transparent proxy 試看看

自己實作 HTTP client 時, 可以用 certificate pinning 擋掉 man-in-the-middle attack, 原理是只相信自己指定的 certificate, 這樣就會忽視攻擊者裝到作業系統的 root CA。

其它討論

藉由了解 Web Proxy 如何處理 HTTPS, 可以得知: 在沒有對使用者裝置動手腳的情況下, Web Proxy 只能放行但不知道內容, 或是依網域名稱決定是否放行 (無法細到網址, 因為那在 HTTP header 裡)。另外, Proxy 可以透過 HTTP 1.1 CONNECT method 傳的參數或是 SSL handshake 傳的 certificate 資訊得知網域名稱。藉此決定是否要放行某些網域的 HTTPS 連線。

雖然 Google 鼓勵大家多用 HTTPS 並且朝向 HTTP 2 邁進, 不過反向思考一下: 用 HTTP 然後自己在 application logic 裡加密資料, 也許可以讓 API 能用在更廣的地方。在特殊的情境下 (比方回傳無法連線的資訊), 或許值得一試?

參考資料

HTTPS Proxy 相關資料

關於 HTTP 1.1 CONNECT method

關於 Certificate Pinning

2015-09-29 更新

Scott 的建議補上 certificate pinning 和 mitmproxy 的資訊。還有刪掉部份內容

2015年9月17日 星期四

用 Ubuntu 14.04 作為 Hotspot

最近需要模擬被擋封包的網路環境, 想想用筆電作為 hotspot 再設 firewall, 應該滿容易的。然後用手機連上 hotspot, 就可以模擬出各種擋封包的情況 (如立即回傳 connection error, 或只是 drop 封包)。

實際操作後的確很容易, 裝好 Ubuntu 14.04 後, 需要的東西都齊了, 內建的 Network Manager 有圖形介面, 點一點就可以分享 Wi-Fi。iPad/iPhone 可以連得上, 但是 Android 不行, 原因是 Ubuntu 預設 hotspot 用 Wi-Fi ad hoc mode, 但是 Android 不支援, 要改用 Wi-Fi infrastructure mode 才行。作法見 How do I create hotspots in Ubuntu 14.04?

其它

  • Android 4.0 後支援 Wi-Fi P2P, 可以讓 Android 裝置互連, 不過還是不支援 Wi-Fi ad hoc mode。
  • 用 Wi-Fi ad hoc mode 的時候, iPad/iPhone 會在 Wi-Fi 列表的「裝置」看到筆電用的 SSID; 若改用 infrastructure mode 的時候, 會在 AP 列表看到筆電用的 SSID。
  • 我有試過用 Mac OS X 作一樣的事, 但是新版 (我用 10.10.4) 內建只能用 pf 作 firewall, 和這東西不熟, 看起來沒 iptables 直覺。另外開啟 Internet Sharing 後 pf 就失效了, 就沒再深入研究。想想以後還是專心用 Linux 就好, 時間有限, 搞熟兩套 OS 的成本太高了。

2015年9月16日 星期三

HTTP POST 使用的編碼格式

三不五時會用到相關的東西, 所以花了點時間查清楚, 本篇重點是參考資料。

用 HTTP 的 POST 的時候, 參數會放在 request body, 然後用 HTTP header Content-Type 決定 request body 編碼方式。

常用的格式 ( Content-Type) 有兩種:

  • application/x-www-form-urlencoded: HTML form 的預設值。和 GET 在 URL 後面帶的參數差不多, 用 URL encoding 編碼 (主要差異是空白字元用 '+' 取代), 適合用在簡單的資料。
  • multipart/form-data: 適合用在 binary data, 上傳檔案時要改用這個, 比較省空間 (檔案內容不需另外編碼)。

application/x-www-form-urlencoded 用 URL encoding 編碼, 遇到大量 binary 資料時, 資料會變大很多 (可能會到三倍)。若先用 base64 [*1] 編碼再套 URL encoding 會好一些 (變成 4/3 倍), 但還是沒有 multipart/form-data 省。

除了上述的格式也可以用 application/json 或其它格式, 不過得看 web server 是否支援。JSON 不支援 binary data, 所以有 binary data 時要先用 base64 編碼, binary 資料一樣會變 4/3 倍大。雖然 base64 會增加 1/3 倍的資料量, 但它簡單易用, 我個人滿喜歡 base64 的設計。

若目的是要壓縮上傳的文字資料, 除了 application layer 自己壓縮資料外, 也可以在 transport layer 作: 使用 HTTP header Content-Encoding: gzip 然後用 gzip 壓縮 request body [*2]。雖然 response 很常用 Content-Encoding: gzip, 但 request 很少這麼用的樣子, 需要改 web server 設定才能作用。如 apache2: 需另外設定 Input Decompression

備註

1. 嚴格來說, 要用 base64url (最後兩個 byte 不同), 對 URL encoding 比較安全。Google API 有用這個, 轉換的時候要留意。

2. 另一個選擇是用 Transfer-Encoding: gzip, 這個作法語意比較正確, 但一來實作不如 Content-Encoding: gzip 常見, 二來 Content-Encoding 是 end-to-end, 不用擔心 Proxy 改變內容, 所以還是用 Content-Encoding 較好。

參考資料

POST Content-Type 相關:

Content-EncodingTransfer-Encoding 相關:

其它:

2015年9月3日 星期四

Mac OS X 雜項常識

備忘用, 持續更新。

硬體列表 (裝置沒反應時可看硬體形號找 driver)

左上角 Apple 圖示 -> About This Mac -> System Report -> Hardware

分享無線網路 (當作 hotspot )

System Preference -> Internet -> Internet Sharing。開啟 Internet Sharing 後, 自訂的 pf rules 會失效。

備忘 nginx 設定

未來的某天應該會需要更了解 nginx, 現階段先隨便備忘用到的東西。

request route

參考資料

用 try_files 設定 URI 對到不同 URI 的順序; 用 rewrite 改寫多種組合到同一位置。兩者都可用來實現 request route。

2015年9月1日 星期二

用 mtr 找出 routing 中有問題的節點

以前只知道用 traceroute 和 ping, 但用 traceroute 看完資訊後也不太清楚情況。用 "traceroute better" 搜了一下, 找到 mtr, 試了一下挺確不錯用。這裡有篇介紹: How To Use Traceroute and MTR to Diagnose Network Issues

備忘指令

  • $ mtr HOSTNAME # 看 realtime 變化
  • $ mtr -w HOSTNAME # 搜集一段時間內的資料

2015年8月26日 星期三

找出 node.js 中 block main loop 的程式

環境

問題描述

node.js 的 single thread event loop 架構容易處理 I/O bound, 也避開 multi-thread 的各式問題, 但弱點是 main loop 被卡住了, 整個程式就不會動了。幸好找出錯誤原因的方法滿多的, 若確定是 CPU bound 而不是自己呼叫到 sync API, 也有很多成熟的解法

在開發環境偵錯

node.js 有很多種 profiling 的方法, 試過幾種作法, 我偏好這兩個作法:

兩者都會產生一個 web server 讓你可以連到指定的 port 用網頁作的 UI 看資料。也都不用傳資料到別人機器上。配合 stress test 可以找出潛在的 CPU bound。look 的介面比較簡單, 啟動速度較快。

在上線環境除錯

但若 CPU bound 只有在上線後才會遇到, 就不容易找了。試了一些方法, 覺得用內建的 command line debugger 最管用。

node.js 已內建 debugger, 不需特殊設定就可以用 debugger attach 上線執行的程式。執行方法如下:

$ kill -USR1 PID  # PID 為 node.js pid
$ node debug -p PID

然後執行 pause 暫停程式,接著執行 bt (backtrace) 才會有效。這樣就有線索找出 main loop 卡住的時候在作什麼了。

下面是一個完整的小例子:

$ cat b.js
function fib(n) {
  if (n <= 1) {
    return 1;
  }
  return fib(n - 1) + fib(n - 2);
}

setTimeout(function() {
  console.log(fib(100));
}, 10000);
$ node b.js
Hit SIGUSR1 - starting debugger agent.
debugger listening on port 5858

在另一個 shell 用 debugger:

$ ps auxw | grep node
fcamel          44656   3.2  0.2  3075620  18524 s013  S+    9:23PM
$ kill -USR1 44656
$ node debug -p 44656
connecting... ok
debug> bt
Can't request backtrace now
debug> pause
break in b.js:6
  4   }
  5   return fib(n - 1) + fib(n - 2);
  6 }
  7
  8 setTimeout(function() {
debug> bt
#0 b.js:6:1
#1 b.js:5:10
#2 b.js:5:23
#3 b.js:5:23
#4 b.js:5:23
#5 b.js:5:10
#6 b.js:5:10
#7 b.js:5:23
#8 b.js:5:10
#9 b.js:5:10
debug> repl
Press Ctrl + C to leave debug repl
> n
6

不過 CPU 太忙的時候 kill -USR1 好像會失效 (node.js 沒有顯示收到 SIGUSR1 進入 debug mode), 然後 debugger attach 會失敗。或是 debugger attach 成功, 有時 pause 也會失敗, 像這樣卡住沒反應:

debug> pause
break in [unnamed]:1
  1

有閒再深入了解原因。

2015年8月17日 星期一

為什麼需要 async programming 以及相關技術的演進

幾種使用 async programming 的情境:

GUI: 像是Web、iOS、Android apps。在 event-driven 的架構下用 callback 滿直覺的, 但是為了避免卡住 UI thread 而需要連續多個 non-blocking I/O 才能完成一件事時,寫起來就不直覺了。這時新增 thread 用 blocking I/O 寫起來比較直覺,但是不容易處理 thread-safety。

server: 像是 Web server、API server、reverse proxy。要能同時承受上千筆連線,必須用 non-blocking I/O。遇到 CPU 吃重的工作時,可以轉交給其它專門的 server (如向資料庫讀資料或用 Lucene 處理 text search)。

其它像是一些會用到多筆 http request 取資料的小程式,例如用 Gmail API 取出所有信件的附件,或是 stress test 的工具,或是 Web crawler,這些也會用 non-blocking I/O 加速程式 (或用 multi-thread/multi-process + blocking I/O 也行,最近才寫了個小工具 prunner.py)。

共通需求

  • 用 non-blocking I/O 避免卡住服務或縮短完成時間。
  • 用 single thread 降低實作複雜度。

為了滿足上述需求,主流的解法是用 single-thread event loop。但遇到 CPU bound 時還是得開新 thread。

注意這個共通需求對 Web page 和 server 是必要需求,但對其它情境只是「優先希望的作法」,Android、iOS 或 crawler 還是可以選擇新增 thread/process 使用 blocking I/O,然後自行承擔 multi-thread/multi-process 衍生的問題。

non-blocking I/O 的問題

single-thread event loop 只有幫忙監聽所有 non-blocking I/O 的動靜,用 callback 的方式通知結果。用 callback 處理 GUI 反應很自然,但是需要依序發數個 http request 的時候,在 callback 裡跳來跳去就頭大了,很難看出程式的流程。

換句話說,我們需要以下功能:

  • 用 synchronous API 來使用 non-blocking I/O。方便看出控制流程。
  • 執行中可以得到 call stack,方便除錯。

提供底層使用 non-blocking I/O 的 synchronous API

如何在不增加新的語法下提供這樣的功能?換句話說,我們還是想新增 thread 用「看起來像 blocking I/O 但底層是 non-blocking I/O」的 API。Python 的 gevent 提供一個不錯的解法:

  • 以 event loop 為底,實作 coroutine (由 greenlet 完成)
  • 提供 synchronous API + non-blocking I/O 實作 (用 yield 主動讓出執行權)
  • task scheduler 會在 I/O 完成後再回到當初 yield 處,讓使用 API 的人用起來像 blocking I/O 一般
剩下的唯一的問題是:所有程式都要用 gevent 提供的 I/O API。實務上這很難作到,所以 gevent 另外提供 monkey patch 處理沒有用 gevent API 的 third-party library。去除 monkey patch 的不穩因素 (比方說 lxml.etree.parse(URL) 會用 native code 從網路讀資料而 block main thread),效果意外地不錯。除了 Python 以外,Go 也有 goroutine,不過我沒研究也沒相關使用經驗。

值得一提的是,coroutine 是 non-preemptive multitasking,比使用 native thread 的 multi-thread (preemptive multitasking) 容易避免 race condition,不過代價是要小心不要寫出 busy loop 卡住整個 process。

新語法:async/await

相對於 Python 陣營有龐大使用 blocking I/O third-party library 的問題,JavaScript 陣營 (Node.js / Browser) 天生就使用 non-blocking I/O (長年以來只有一個 thread 的自然結果),這點很有優勢。

JavaScript 不用 coroutine 而是發展出兩種方式提供 synchronous API:

  • Promise: 看起來像 synchronous API,只是囉嗦了點,還有可能會搞混執行順序。
  • async/await: 基於 Promise 定義新的語法,簡化使用並突顯使用 async 的程式。

詳情見The long road to Async/Await in JavaScript。另外 What Is Asynchronous Programming?Managing Asynchronous Code - Callbacks, Promises & Async/Await 也值得一看。

另外,Python 也會在 3.5 加入新語法 async/await,用它們明確地定義 coroutine,藉此區分 generator。async/await 去掉 thread 的概念,應該會比 coroutine 適合。但對習慣 multi-thread 的人來說,可能 coroutine 比較親切吧?

整體來說,未來繼續用 Node.js 或 Python 寫 server 程式還是不錯的選擇,語法簡單、用戶眾多又有廣大的 third-party library 可用 (目前各用 geventNode.js 寫過一次上線的服務)。

雜談

話說大家都用 synchronous API 提供 non-blocking I/O 後,還能稱這為 async programming 嗎?不過語法裡有 async/await啦。具體來說,async programming 的定義到底是什麼......

2015年8月14日 星期五

prunner.py: 連猴子都會用的加速框架

問題

寫 python script 時有時會遇到 I/O bound (例如需發出大量 HTTP request) 或 CPU bound, 這時可用 multiprocessing 加速。但是自己寫 multi-process 有點麻煩:

  • 要留意 API 使用細節
  • 容易發生 race condition 或 dead lock。
  • 程式較複雜時會切成多個階段作業, 有時會閒置部份 processes 而浪費了加速的機會。
  • 辛苦寫好的平行架構不易重覆使用 (我至少重寫三次以上...泣)。

解法

prunner.py 是我寫好的框架, 中心思想是透過 task queue 提供容易重覆使用的架構, 還有協助減少閒置的 process。

以計算 sum( (i+1) * 2 ) 為例, 下面的程式會用很蠢的方式透過 10 個 processes 平行計算出結果:

import prunner

def begin():
    prunner.get_dict()['sum'] = 0
    prunner.post_task(init, range(2000))

def init(numbers):
    for i in numbers:
        prunner.post_task(add_one, i)

def add_one(n):
    prunner.post_task(double, n + 1)

def double(n):
    prunner.post_task(sum_up, n)
    prunner.post_task(sum_up, n)

def sum_up(n):
    with prunner.global_lock():
        prunner.get_dict()['sum'] += n

def end():
    print prunner.get_dict()['sum']

prunner.init(10, False, begin, end)
prunner.start()

每個 function call 會在一個 process 上執行, 藉此增加使用到的 CPU。也可以盡情地用 blocking I/O, 反正有很多 processes 會平行執行程式。

若不習慣 message loop 的寫法, 也可以用較 low level 的 API, 直接繼承 ParallelTaskRunner 然後覆寫 begin(), run(), end():

import prunner

TASK_INIT = 'task_init'
TASK_ADD_ONE = 'task_add_one'
TASK_DOUBLE = 'task_double'
TASK_SUM = 'task_sum'

class MyRunner(prunner.ParallelTaskRunner):
    def begin(self, options):
        self.dict_['sum'] = 0
        self.queue.put(prunner.Task(TASK_INIT, range(2000)))

    def run(self, task):
        if task.label == TASK_INIT:
            for i in task.data:
                self.queue.put(prunner.Task(TASK_ADD_ONE, i))
            return

        if task.label == TASK_ADD_ONE:
            self.queue.put(prunner.Task(TASK_DOUBLE, task.data + 1))
            return

        if task.label == TASK_DOUBLE:
            self.queue.put(prunner.Task(TASK_SUM, task.data))
            self.queue.put(prunner.Task(TASK_SUM, task.data))
            return

        if task.label == TASK_SUM:
            with prunner.ScopeLock(self.lock):
                self.dict_['sum'] += task.data

    def end(self):
        print self.dict_['sum']


runner = MyRunner(10, False, None)
runner.start()

和前面一樣, 同時有多個 processes 執行 run(), 每次呼叫帶有不同的資料。

雜談

1. 因為底層是 multiprocessing, prunner.py 可以處理 CPU bound 和 I/O bound, 直接用 blocking I/O 也沒問題, 方便配合 third-party library。

不過 I/O 需求量極高時, 還是用 gevent 較適當, 但是要找合 gevent 的 third-party library 或用 gevent monkey patch

2. 這個架構中比較需要巧思的部份是判斷程式的中止條件並且提供有效率的實作。中止條件是「所有 process 閒置且 queue 是空的」。

各個 process 有一個獨立的 flag 表示閒置與否, 這樣比用一個共用的計數器有效率。在我的電腦上執行 prunner_example.py 的時候, 獨立 flag 和計數器的時間分別是 2.5s 和 3.6s。

3. 雖然可以用 post_task() 執行 instance method, 不過會隱含 serialize/deserialize instance 以及傳到其它 process 的成本。這是用框架的缺點, 用起來方便, 但不小心會造成不必要的 overhead。我寫了兩個例子比較: prunner_example3.py 用 instance method, pyrunner_example2.py 用 function。執行時間分別是 2.8s 和 2.6s。

4. 試過各種架構後, 覺得 task queue (或稱 message queue) 還是最直覺的運作方式。有想過 producer-consumer 或 MapReduce 的架構, 不過還是 task queue 最易使用。這種單機層級的運算, 用 task queue 應該綽綽有餘。若要跨機器執行, 太過彈性可能會增加實作的複雜度。那時應該用別人作好的成熟工具。

2015年8月10日 星期一

用 grequests 作 stress test

作了一個 C10K 的 server 後, 還要作 stress test 才能驗證 server 真的挺得住 C10K 的連線。試了幾套工具, 最後覺得 grequests 最好用 (用 gevent monkey patch 的 requests), 用法就這麼簡單:

import grequests

def exception_handler(request, exception):
    print "Request failed: %s" % exception

url = 'SERVER_URL'
rs = [grequests.post(url, data={...})
      for i in range(10000)]
rss = grequests.map(rs, size=1000, exception_handler=exception_handler)

這樣就寫好一個會同時產生 1k 個連線, 總共產生 10k 個連線的 client。因為是直接寫程式, 可以自訂各式各樣的內容, 測得出真正的效果, 不會被簡單的 cache 消掉絕大多數的 requeests。若要改用 get 就用 grequests.get(url) 產生 request, 詳細用法見 requests

幾個測試情境和作法:

  • Connection bound: 提升 size 數, 或多跑幾個 client
  • CPU bound: 同上, 故意使用耗 CPU 的 API
  • Memory bound: 依 API 的性質, 產生 1MB 或更大的資料, 由 data=... 傳過去

grequests.map() 不設 size 的話, 會盡可能產生最多連線, client 有可能自己先爆掉。依自己的網路環境和 server 狀況, 可試看看不同的值。

執行前記得先用 ulimit -n 提升 open file number, 不過要用 root 才能提升, 作法如下:

$ sudo bash
$ ulimit -n 50000
$ su YOUR_ID

這樣可以用一般帳號測試, 但保有 max open file number = 50k。

以下是我試過但不管用的方法, 憑印象寫下放棄使用的原因 (不見得是工具不好):

  • ab: 很久以前用過覺得不夠順手, 查到文章推薦用 httperf, 這回我就跳過 ab 了。
  • httperf: 雖然有 --wsesslog 可以讀文字檔產生不同內容, 還是不夠彈性。另外有遇到 open too many files 的問題, 似乎要編程式才能提升 limit, 就懶得再試了
  • loadtest: 看起來不錯, 可用 -R 指定 script自訂程式, 由 loadtest 幫忙處理其它事, 不過沒有試成功, 就試別套了
  • nodeload: 還不錯用, 可以自訂程式, 執行後還會自動產生報表, 附一個 local http server 跑在 port 8000 可以即時看報表變化。不過量大了以後會炸, 有些 bug, 作者又說沒在維護了, 就放棄它了。
  • Btw, 用這些工具的時候, 都要自己設好 http header 才可以使用 POST, 不如 requests 直覺。

2015年8月8日 星期六

http 1.1 pipeline 失敗的原因

網路連線的基本指標是 latency 和 throughput。用 pipeline 同時提升 latency 和 throughput 還滿直覺的, 但在 http 1.1 上卻意外地以失敗收場, 所以花了點時間查一下為什麼。

What are the disadvantage(s) of using HTTP pipelining? 提到幾個問題:

  • http 1.x 是 stateless 的 protocol, client 沒有能力知道 request 之間的 order (沒有流水號)。
  • 因此, server 必須依 request order 回傳 response, 但是有些 server 實作錯了。
  • 即使 server 作對了, 因為要照 order 回傳 response, server 可能需要暫存大量回應, 有被 DoS 的風險。
  • 因為 response 要依順序回傳, client 有 head-of-line blocking 的風險, 比方說剛好先要一個大圖檔再要一個 JS 檔, 結果 JS 檔因此比較慢收到, 讓網頁比較慢取得較重要的檔案。
  • client 端的實作比想像中複雜, client 要先偵測 server 是否支援 pipeline 接著才能開始用 pipeline, 因此有些延遲。再者, 因為 client 不會真的只開一個連線, client 還需考慮如何分配 request 到不同連線作 pipeline。
  • 因為 http 1.x 過往的限制, 網站早就依 domain 拆開回資料, client 也會同時發多個連線加速。結果是即使 pipeline 都作對了, 可能也沒賺到什麼。
  • 此外, server 和 client 中間的 proxy 也可能不支援 (如擔心 DoS) 或是作錯, 又降低了可行的機率。

如果 http 裡面有含流水號, 由 http client library 自己匯合好再依順序回傳資料給上層程式 (像 TCP 那樣), 問題會少一些。設計協定時決定由 server 或 client 處理還挺關鍵的, 一但作錯決定, 協定普及後就很難改了。

HTTP/2SPDY 直接支援 multiplexing, 在一開始就作對了 pipeline。剩下的問題只有使用 TCP 造成 head-of-line blocking (block 的層級不同, 不像 HTTP/1.1 pipeline 那樣嚴重), 但這個得繞過 TCP 才能解決, 也因此 Chrome 又作了 QUICQUIC 看來挺有趣的, 不過不像 HTTP2 看來快要普及了 (從 Chrome 6 開始用 SPDY, 這也花了四年以上的時間...)。

Btw, What are the disadvantage(s) of using HTTP pipelining? 附了許多有趣的連結, 有興趣讀讀可以了解更多細節。

gold 和 GNU ld 行為差異造成的問題

原因

ToolChain/DSOLinking - Debian Wiki 介紹得很清楚, 主要的差異是處理 DT_NEEDED section 的行為不同。

對 program A 和 shared lib B 和 C 來說, 假設相依性如下:

  • A 用到 B 和 C
  • B 用到 C

照理說 link 的設定要如實反應出相依性。若 link 的設定如下:

  • A: link B
  • B: link C

linker 是 GNU ld (BFD-based linker) 的時候會成功, 但用 gold 的時候會失敗。原因是 linker 會在目標的 DT_NEEDED section 記錄用到的 shared lib, 然後 GNU ld 預設會將 DT_NEEDED section 的值一併「往上傳」。所以 A link B 的時候不只會在 DT_NEEDED section 加上 B 的記錄, 還會取得 B 的 DT_NEEDED section 內的值, 所以沒寫 link C 也會有 C 的記錄。

這造成一個問題: 日後 B 沒有用到 C 的時候, 會因為升級 B 而造成 A 的 link error, 錯誤原因是沒有 link C。

解法是用 GNU ld link 時也要下 --no-add-needed 避免自動取用 shared lib 內的 DT_NEEDED section 的內容。這樣開發的時候才會注意到漏加 shared lib。

這種情況容易發生嗎? 就我個人的經驗來說, 比我預期的容易。比方說寫 GTK+ 也會用到 glib, 但 GTK+ 自己也有用 glib, 所以 GTK+ 的 DT_NEEDED section 有含 glib, 然後自己就忘了加。若用 GNU ld link, 沒加 glib 也會成功。或是很多 lib 用到 pthread, 自己也用到或加入的 static lib 間接用到, 然後忘了 link pthread。

改用 gold 為預設 linker

gold link 的行為不但比較正確,速度比較快也比較省記憶體。可以的話, 改用 gold 作為系統預設 linker 比較省事。Mac OS X 已經用 gold 了, Ubuntu 12.04 要另裝 binutils-gold:

$ sudo apt-get install binutils-gold

然後檢查看看:

$ ls -l /usr/bin/ld
lrwxrwxrwx 1 root root 7 Apr  2 00:05 /usr/bin/ld -> ld.gold*

安裝前會是連到 GNU ld, 像這樣:

$ ls -l /usr/bin/ld
lrwxrwxrwx 1 root root 7 Apr  2 00:02 /usr/bin/ld -> ld.bfd*

若沒有成功, 可能要用 update-alternatives 切換預設 linker (Ubuntu 14.04 好像需要), 詳見這裡

其它

1. GNU ld 預設會加入所有指定要 link 的 shared lib 到 DT_NEEDED section, 即使沒有用到那些 lib 也一樣。若希望只加入用到的 lib, 要加上參數 --as-needed

2. 可以用 ldd 列出 executable 或 shared lib 相依的 lib, 像這樣:

$ ldd /bin/ls
        linux-vdso.so.1 =>  (0x00007fffe2351000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007ff642698000)
        librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007ff642490000)
        libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007ff642287000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff641ec8000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff641cc4000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff6428ce000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ff641aa6000)

2015年8月7日 星期五

PHP + JavaScript 提供 L10N 的文字

我希望的用法如下:

  • PHP 和 JavaScript 共用同一套文字格式。
  • PHP 和 JavaScript 用同一套 API。
  • 各國語系用字串 key 取出翻譯的內容。優點是可以隨意更改預設語系的內容; 缺點是網頁原始檔有一點難閱讀,無法像 L10N 前那樣直接看到內文。
  • 選用的目標語言缺內容時,改用預設語言。
  • L10N 的文字存成 JSON, 方便用各種工具編輯。
  • 函式庫相依性愈小愈好。

多數人使用的 gettext 不合我的需求, 查了一下沒找到順手的工具, 最後自己寫了一個。只有一個檔案 l10n.php, 程式不到 100 行。我不太熟 PHP 和 JavaScript, 可能寫得不怎麼道地。

用法是在網頁內 <?php require('l10n.php') ?>, 之後 PHP 和 JavaScript 都可以用 _m(KEY) 取得對到目前語言的字。可用 ?lang=LANG 切換語言,不然會以 Accept-Language 內 weight 最大的為目標語言。預設語言為 en。

API 還有一些可以改進的地方,像是支援替換文字內的變數,不用自己取出來後再替換;或是在 JSON 格式內加些輔助翻譯的描述。不過比較細微就是了,有這打算時再來參考 Chrome extension API 的作法, 還滿直覺的。

2015年8月1日 星期六

近期閱讀有物報告的心得

整理一下最近閱讀的心得,只是雜亂的片段想法。也許有天能串出更深的想法。

一個預測未來的簡單方法

原文: 有物報告 | 一個預測未來的簡單方法

用正面的方式描述是: 任何沒考慮 smartphone 的產業都會被衝擊。反過來說則是: 基於「不是人人都有 smartphone」的假設而發展出來的產業, 勢必會因 smartphone 普及而被衝擊。我比較喜歡後者的說法,前者聽起來比較空泛,後者有比較強的因果關係 (或著說比較強的推測)。

現行的例子是叫計程車。以前沒有 smartphone 要透過中間的服務人員對上雙方的位置, 幫忙湊合人和車。現在大家都有 smartphone, 定位和上網都很容易。建立平台的成本大幅下降,也不用透過服務人員配對。

去中介化的犧牲者

原文:

能用數位傳遞的東西, 終將被網路取代。書店的定位必須轉型提供不同的服務,比方說營造店內的氣氛讓人想來逛, 像是誠品。或是經由個人品味挑選書本、寫評論分享自己挑選的書。

全程創業,販售服務

原文:

販售長期的服務,金流比賣一次性的產品來得穩定。也因為更了解用戶,有機會撥掘和提供新服務取得更多獲利。理論上從上至下都自己來,使用者用起來會更順。像選用 iPhone,和 Apple TV 結合的很流暢 (其它 iCloud 之類的我沒有用, 不知效果如何)。Android phone 和 Miracast 則不容易作到流暢。

什麼都自己來, 代價是要作更多事,分散火力。如今透過 smartphone 上搭配的「商城」 (App Store、Google Play), 傳遞產品和收取費用比以前簡單許多。至少降低了一個門檻。

Btw, 這段話讓我特別有感觸,注重在賣服務,而不是工具:

哈佛商學院教授 Theodore Levitt 說:「人們要的不是鑽孔機,而是牆上的洞。」他認為購買鑽孔機是種不完美的妥協。訂閱制往往能創造企業與客戶的雙贏,因為雙方的目標是一致的。

雜談

總體來說,產品本身是數位內容, 全程都可數位化; 本身不是數位內容 (如餐廳、搭車), 背後的媒合平台也可以作些改善 (O2O, Online-to-offline)。smartphone 和行動網路的普及, 讓人們可以重新思考如何將習以為常的事作得更好。習慣的包裹愈輕, 愈容易想到新方法, 也更有機會找到更好的方法 。

看這些評論很有意思,不過對於實際作的事到是沒有直接的幫助。主要是了解目前的趨勢,偶而檢查一下自己的思維是否被舊習慣所綁住,放寬思考的方向。產品的本質還是為使用者解決他們願意付錢解決的問題。

2015年7月26日 星期日

使用 user agent 和 meta viewport 的目的和造成的影響

對行動瀏覽器和網頁的開發者來說,都希望提供最佳的使用者體驗, 不過彼此在意的事有點不同。

瀏覽器在意的事:

  • 用 user agent 告知網站自己是什麼瀏覽器, 讓網站有機會提供最佳的內容。
  • 也提供切換 user agent 的選項,若網站自動提供行動版網頁, 而使用者想看桌機版網頁時, 可換用桌機瀏覽器的 user agent 「騙」網站提供桌機版的網頁。
  • 如果頁面有用 meta viewport 設定 layout width=device width, 表示網頁有針對 device width 作最佳化, 照著辦就是了。
  • 承上, 不過若網頁沒作好, 比方說在頁面裡寫死某個元件寬度, 寫死寬度的元件仍會超出螢幕寬度, 看起來怪怪的。
  • 若沒有設 layout width=device with, 表示網頁開發者沒有針對行動裝置作最佳化, 至少要用夠寬的 layout width (>螢幕寬度), 不然網頁內容會縮成一團無法閱讀 (就像在桌機將瀏覽器視窗縮到很窄一樣)。常見的實作寬度是 980px 或 1024px。
  • 承上, 因為手機螢幕比 layout width 小, 載入完頁面後使用者無法一眼看到網頁全貌, 瀏覽器會自動縮小 (zoom out) 頁面以貼齊螢幕寬度。預期使用者可以先看見全貌, 找到有興趣的地方,再自行放大 (zoom in) 想看的部份。

網站開發者在意的事:

  • 透過 user agent 得知使用者用什麼裝置和瀏覽器。粗略分成行動裝置和桌機 (PC 或筆電) 兩種。後者通常計算能力強、網路快、螢幕大,可提供豐富內容。另外提供前者客制化的網頁,以提供更好的使用體驗。
  • 桌機網頁也可使用 Responsive Web Design (RWD), 讓大螢幕有更好的使用體驗 (但不會在意螢幕太小的表現)。不需使用 meta viewport (用了也會被桌機瀏覽器忽略)。layout width 由視窗寬度決定。
  • 行動網頁必須使用 meta viewport 要求瀏覽器用 device width 作為 layout width, 不然瀏覽器會用 980px 或 1024px 排版, 結果是字縮得太小 [*1],使用者得放大縮小外加水平捲動觀看內容, 用起來不方便。最好使用 RWD 應付不同手機的不同寬度。不然就要用比較窄的寬度為基準來排放內容, 然後在兩側或右側留白,看起來比較遜。
  • 沒餘力搞兩套網頁就用 RWD 一套通吃。雖然排版難度變高許多, 但不用寫兩套網頁流程, 應該會比較省事? 除了排版技術較深外, 可能因此對手機裝置多傳了些用不到的資料,浪費頻寬甚至拖慢載入速度。

在手機上用 desktop user agent 看到奇怪的排版內容,可能的原因:

  • 網站在桌機網頁用了 meta viewport, 但沒處理好 layout width 較窄的情況。可能的原因是同一網址會依 user agent 提供不同內容: 若是手機瀏覽器的 user agent, 就提供行動版網頁, 因此沒有測到「在手機上用 desktop user agent」的情況。
  • 網站自己有作好 RWD, 不過嵌入其它家服務的內容 (如廣告) 出槌。出槌的原因是提供內容的網站是看 user agent 決定內容, 因此提供太寬的內容而超出螢幕寬度。若嵌入的內容有依 layout width 提供內容就沒問題了。

參考資料

備註

1. 行動瀏覽器有提供 "text reflow" 的功能, 會在不改變排版的情況下, 自動放大太小的字。好處是在手機上用 980px (或1024px) 排版後,不用放大就可以看清楚主要的內文。壞處是部份內文變得比標題大, 看起來怪怪的。

印象中是 2012 或更早就有的功能, 當時看起來頗酷的也不錯用。不過在 RWD 盛行後, text reflow 大概會愈來愈少發揮效果。在行動瀏覽器 (如 Chrome 或 Puffin) 用 desktop user agent 看 Mobile 01 內文, 會看到 text reflow 的效果了。

2015年7月19日 星期日

iOS kCFErrorHTTPSProxyConnectionFailure 和 Android ERR_TUNNEL_CONNECTION_FAILED 的解法

iOS kCFErrorHTTPSProxyConnectionFailure (錯誤代碼 310) 的意思是 proxy 不允許 HTTPS 的連線,可能是 client 用 https 連往 443 以外的 port, 然後 proxy 設定不允許這樣的連線。解法是改在 443 port 執行用 https 的 web server, 或是 client 不要使用 proxy。

我用同樣的設定 (在 port 443 以外跑 https + client 用 proxy) 改測 Chrome on Android, 結果顯示的錯誤代碼是 ERR_TUNNEL_CONNECTION_FAILED, 也許這個代碼有用在其它情況。遇到的時候也可以看看是否和這有關。

2015年7月14日 星期二

Python, JavaScript, C++, Java 和 Unicode 以及 UTF-8 等編碼

基本觀念

Unicode 是一張表,定義每個文字對應的數字是什麼,比方說 'A' 是 65, '我'是 25105。

UTF-8, UTF-16, UTF-32, UCS-2, UCS-4 等則是 Unicode 的各種編碼。所謂的編碼,是某種定好的儲存格式 (或說 serialization/deserialization format 也許比較好懂?), 方便讓不同的應用程式之間傳遞資料。

比方說應用程式 A 寫入文字到檔案裡,應用程式 B 從檔案讀出文字,兩者要說好用的編碼格式 (或能從資料辨別使用的編碼格式), 才有辦法互通資料。舉例來說, 網路傳輸常用 HTTP 作為溝通協定, HTTP 有提供 client 或 server 指定傳輸內容編碼的方法 (Accept-Charset 和 Content-Type), 可以指明是用 UTF-8 或其它編碼。

寫程式的時候,基本上是假設外部讀來的資料是某種 Unicode 編碼 (最常用 UTF-8 ), 內部使用 Unicode 處理,要傳出去 (寫入硬碟/網路傳輸) 前再轉回 Unicode 編碼。用 Unicode 的好處是,可以正確計算字串長度, 這影響到使用 length, index, 取出 substring, 比對 substring (包含 regexp) 等字串 API。

這裡有以前寫的介紹,有多提一點相關的事。

Python

Python 2 就有定義 type unicode 和 type str。type str 可看成一般性的 binary, 以 byte 為單位。Python 2 的字串 '...' 的型別是 str, 要特別用 u'...' 才會改用 unicode。

有了 type unicode, 處理 unicode 比較輕鬆:

  1. 外部讀進來的字 (UTF-8) 存在 type str, 馬上轉成 unicode
  2. 函式的輸入輸出都用 unicode (len(), [] 的行為正確)
  3. 要輸出到螢幕、檔案、網路時再轉成 UTF-8。

參考 All About Python and UnicodeUnicode in Python: Common Pitfalls 了解細節。

另外, Python 可能用 UCS-2 或 UCS-4 實作 Unicode, 可從 sys.maxunicode 的值得知, 65535 表示用 UCS-2。因為 UCS-2 / UCS-4 每個字分別占 2 / 4 bytes, 對使用的記憶體的量有很大的影響,不同作業系統會有不同考量, 像 Mac OS X 用 UCS-2, Ubuntu 12.04 用 UCS-4。然後在 Python 3.3 後都支援 UCS-4 了 (因為有實作 PEP 0393 -- Flexible String Representation)。

例如 u'𝌆' 會被當成兩個字 [*1]:

In [1]: u'我'
Out[1]: u'\u6211'

In [2]: len(u'我')
Out[2]: 1

In [3]: u'𝌆'
Out[3]: u'\U0001d306'

In [4]: len(u'𝌆')
Out[4]: 2

Btw, Python 3 改變字串預設的型別: '...' 是 unicode, 要用 b'...' 才會用 str,用意是希望大家預設就用 Unicode 避免混亂,不過卻造成 2 升 3 的一些問題。直接引用以前碎碎唸的心得:

在 python2.7 踩到 python3 unicode 的雷,import unicode_literals 會讓 'abc' 自動變成 u'abc' ( 原本的行為是 ‘abc' == b'abc') ,難怪怎麼看都不太對勁,third-party code 寫 'abc',別的 third-party package 預期收到 raw string 卻收到 unicode string, 於是回報 TypeError: 'unicode' does not have the buffer interface

用 ipdb 除錯時覺得相當奇怪,一樣的程式碼 (如 s = 'abc'),在 ipdb 內執行得到的結果是 raw string, 但原本的程式執行時卻變 unicode ....

搜了一下才看到也有不少人反應這點: Proposal to not use unicode_literals #22 https://github.com/PythonCharmers/python-future/issues/22

JavaScript

JavaScript’s internal character encoding: UCS-2 or UTF-16?: 詳細到不行的介紹,沒耐性就直接看文中的 conclusion 吧。

結論是 JavaScript 直接用 UCS-2 表示字元, 不像 Python 分成 str 和 unicode。開發者幾乎不用操心 Unicode、UTF-8 之類的事, 反正在 JavaScript 內都是用 UCS-2 表示。缺點是超出 65535 的 Unicode 不會被正確處理:

> '我'.length
1
> '𝌆'.length
2

不過網頁還是可以正確顯示字的外觀,大概是因為瀏覽器內部用 UTF-16 處理,字的 Unicode 仍然是對的,畫字時有選對 font glyph。但是受限於 JavaScript 的語言規範,只能斷成多個字表示。

C++

C++ 沒有定義型別 Unicode, 要自己用 std::string, std::wstring 或其它型別表示。

查到幾種轉換方法

  • 用 C++11
  • 用 Boost
  • 用 icu
  • utf8-cpp

最後一個作法最簡單, include 幾個 header 檔即可,不像其它方法要改 compiler 參數或引入一大包函式庫。所以我後來是用 utf8-cpp, 待要非得用到其它方法時再來研究為啥需要其它方法。stackoverflow 裡有很多討論 (由此可見 C++ 使用 Unicde 的方法有多混亂....), 這是其中一篇

Java

Java 的基本單位 char 一樣用 UCS-2 實作, 但 String 提供另外的 API 可正確存取 Unicode。直接引用別人的結論:

Han Guokai wrote on 20th January 2012 at 18:28:

In Java5, ‘char’ means code unit (16-bit), not character (code point). One character uses one or two chars. String’s length method return the char count. And there are some additional methods based on code points, i.e. codePointCount. See: http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/String.html

比方說 Java String 提供 charAtcodePointAt。後者是正確的 Unicode 字元,而前者是 char, 遇到 UCS-2 外的字一樣會出錯。

結論

總算弄清楚過去常用的四種語言實作 Unicode 和 Unicode 編碼的方法,結果四種語言有四套作法, 真是出乎意料出外...。推薦閱讀JavaScript’s internal character encoding: UCS-2 or UTF-16? 了解 BMP, supplementary planes 和 surrogate pairs, 會比較清楚 JavaScript 和 Java 使用 UCS-2 作為字元定義遇到的問題 (為了省記憶體, 是必要的 trade-off)。

備註

1. Blogspots 無法正式顯示這個字, 可從這裡看到測試用的字。https://codepoints.net/ 可找到更多範例。

2015/07/19 更新

Scott 指正, 更新內文對 Python Unicode 實作的描述。

2015年7月11日 星期六

從舊有的子目錄建立新的 git repository

如果要搬移的目錄在 repository 的最上層,可以用 git subtree。若是要搬的目錄是中間某層目錄 (例: a/b/c 的 c), 可以用 filter-branch

筆記一下我操作成功的流程:

1. 在 git server 上建立新的 headless repository

git-host$ cd /path/to/repo && mkdir new_repo && cd new_repo && git init --bare

2. 在自己的機器上從原本的 repository filter 出要搬移的目錄。注意: 目錄內的內容會變成只剩目標目錄,我是 clone 一份新的來作。

myhost$ git clone ssh://path/to/old_repo
myhost$ cd old_repo
# Filter the master branch to path/to/folder and remove empty commits
myhost$ git filter-branch --prune-empty --subdirectory-filter path/to/folder master
Rewrite 48dc599c80e20527ed902928085e7861e6b3cbe6 (89/89)
Ref 'refs/heads/master' was rewritten

3. 合併舊有的 git commits 到新的 git repository 並更新回 server

myhost$ cd /path/to/somewhere
myhost$ git clone ssh://path/to/new_repo
myhost$ cd new_repo
myhost$ git pull /path/to/old_repo # 取得剛才 filter 過的 old_repo
myhost$ git push --set-upstream origin master
myhost$ rm -rf /path/to/old_repo  # 移除用不到的目錄

2015年6月22日 星期一

使用 Node.js 實作 server push

前幾天才說用 Tornado 實作 server push 很簡單, 且可以直接用在上線環境裡。今天要來自打嘴巴說我要改用 Node.js 了。有興趣了解相關基本知識的人, 可以參閱之前的說明

Python 圈 event-based framework 的問題

這篇文章所述, Python 天生不是 async, 既有函式庫自然也不是 async, 使用 event-based framework (*1) 後, 要另外找配套的函式庫。比方說 http client 用慣了 requests, 直接拿到 Tornado 裡用會出事, 因為 Tornado 是 single process single thread 的架構, 一發出網路連線就會 block 住全部動作。要嘛改用 Tornado 提供的 httpclient, 不然就要找別人用 Tornado 提供的 API 包好的 requests

每套函式庫都這樣找頗麻煩的 (資料庫、第三方服務的 SDK、etc), 而且不見得找得到牢靠又有效率的版本, 比方說 Google API Python Client 就沒有 Tornado 版的, 要自己想辦法弄。以前用 gevent 時作過類似的事, 先搞定 Dropbox Python SDK, 再來搞不定 Google Drive API, 只好用 gevent monkey patch。感覺不是很可靠。

Node.js 的優勢

反觀 Node.js 天生就是 async, 既有函式庫自然也是 async。Google 也有提供 Google API Node.js Client。既有的社群也滿龐大的, 可以用的套件很多。對我來說, 這點最重要。

效能方面, 因為使用 V8 轉成 native code 執行, 表現也不錯。從這裡這裡的 benchmark 來看, 數值計算大勝 CPython, 然後和 PyPy 持平。我自己比較常用 dictionary, 作個簡單的 benchmark 測試使用 for, if 和 dictionary, 結果 Node.js 小勝 PyPy, 然後大勝 CPython。整體來說執行速度夠用了, 到是記憶體有 1.7G 的限制 (64bit OS 預設 1G), 要多留意一下。

使用 Express 作 web server 提供簡單的 API, 然後用 ab 作簡單的 benchmark, 結果效能比 Tornado 好 (或可視作差不多, 都達到標準), 一杪內湧入 1k 連線可在一秒內解決; Node.js 底層使用 libuv, 承受 C10k 以上的連線也不是問題。

此外, 配合既有寫網頁的經驗, 改用 Node.js 開發少了學習新語言的成本, 這也是一個優勢。

Server Push 實作

這裡有完整的程式碼, 我只有作最基本功能, 沒控管記憶體也沒提供 timeout 之類的額外參數。寫起來很直覺, /get 沒有資料的時候, 就產生一個 callback function 存到全域變數 g_cache 裡。待 /set 再逐一呼叫這些 callback function, 完成 long polling 的要求。

var error = {
  ok: 0,
  invalid_input: 1,
};

var g_cache = {};

app.get('/set', function (req, res) {
  var key = req.query.key;
  var value = req.query.value;
  if (key === undefined || value === undefined) {
    res.send(JSON.stringify({
      error_code: error.invalid_input
    }));
    return;
  }

  if (!(key in g_cache)) {
    g_cache[key] = {};
  }

  var entry = g_cache[key];
  entry.value = value;
  if ('callbacks' in entry) {
    var cbs = entry.callbacks;
    for (var i = 0; i < cbs.length; i++) {
      cbs[i]();
    }
    while (cbs.length > 0) {
      cbs.pop();
    }
  }
  res.send(JSON.stringify({
    error_code: error.ok
  }));
});

app.get('/get', function (req, res) {
  var key = req.query.key;
  if (key === undefined) {
    res.send(JSON.stringify({
      error_code: error.invalid_input
    }));
    return;
  }

  if (!(key in g_cache)) {
    g_cache[key] = {};
  }

  var entry = g_cache[key];
  if ('value' in entry) {
    res.send(JSON.stringify({
      error_code: error.ok,
      value: entry.value
    }));
  } else {
    if (!('callbacks' in entry)) {
      entry.callbacks = [];
    }
    entry.callbacks.push(function() {
      res.send(JSON.stringify({
        error_code: error.ok,
        value: entry.value
      }));
    });
  }
});

Node.js 雜項備忘

備註

1. 我指使用 epoll 之類作法的架構。

2015-06-22 更新

Scott 提到 Go 也是不錯的選擇。搜了一下, 還真多人從 Node.js 跑到 Go。另一方面抽查了幾個 third-party 服務, 看起來 Node.js 的支援度比 Go 廣。對我來說, 現階段還是用 Node.js 比較划算 (學習成本和風險考量)。一兩年後再來評估看看是否值得改用 Go。

2015年6月19日 星期五

server push by long polling

程式碼

沒興趣讀落落長心得的人, 這裡有用 Tornado 實作的程式測試碼

心得

server push 是指從 server 主動送訊息給 client, 這裡有圖解一些達成 server push 的方式。其中我個人偏好 long polling 的作法,運作方式就和字面一樣: client 先發出一個連線, server 不要立即回應。等 server 需要主動通知 client 時, 再回傳資料。

這個作法的好處是:

  • 各種 client 都適用, 只要能用 http 連線即可, 不是瀏覽器也OK, 現在各個平台都有好用的 http 函式庫。
  • 不用擔心 client 網路環境問題。client 能主動建立往 server 的連線, 反過來就很難說了。

另一方面, long polling 的缺點是 server 會有許多閒置的連線占資源。像 apache 這類每個 client 用獨立的 thread 處理連線的作法, 就不適合處理 long polling。假設一個 thread 占據 10MB 空間, 1k 個不作事的連線, 就占掉 10G 記憶體了。另外大量 thread 之間的 context switch 也是可觀的時間負擔。

但是改用 epoll 之類的 API 寫成 event-driven 的架構, 可用 single process single thread 的方式同時處理大量連線, 就沒有浪費記憶體和 context switch 的時間負擔。現在有不少現成的工具使用 epoll 包成 green thread, 使用上和 multi-thread 一樣容易上手 (而且還不用擔心 race condition, 因為實際上只有一個 native thread),降低實作門檻。其中 Tornado 是 Python 寫成的 web framework 並內建可上線用的 web server。我作了簡單的 benchmark, 的確可以快速反應一秒內同時擁入的一千個連線。用 Tornado 省去不少工夫 (若不想用 web server 而是自己從頭作 server, 也可用 gevent)。

實作 long polling 的另一個問題是: 如何在察覺資料更新時, 能立即通知 client? 想像 client 透過連線 S 連往 server, server 先不作回應。接著 server 在其它地方取得新資料, 再透過 S 回傳資料。處理連線 S 的程式要怎麼暫停? 之後要怎麼返回執行? 這個作法可以 scale 嗎?

對於 single process 程式來說, 直覺且低成本的作法是: 在 thread A 讀資料, 發覺沒資料時改用 condition 變數 (搭配 lock) 停住程式, 然後在 thread B 收到新資料時, 用同一個 condition 變數通知 thread A 可以繼續讀資料。對應到 Tornnado 的寫法是用 locks.Condition:

condition = locks.Condition()

@gen.coroutine
def waiter():
    print("I'll wait right here")
    yield condition.wait()  # Yield a Future.
    print("I'm done waiting")

@gen.coroutine
def notifier():
    print("About to notify")
    condition.notify()
    print("Done notifying")

@gen.coroutine
def runner():
    # Yield two Futures; wait for waiter() and notifier() to finish.
    yield [waiter(), notifier()]

io_loop.run_sync(runner)

有了 locks.Condition, 要實作一個完整可上線使用的 long polling 只是小事一椿, 下面的程式碼實作兩個 API /get 和 /set: 提供基本的 "get value by key" 和 "set value for key"。/get 沒資料時會先停住不回應, 等到 /set 取得資料後再立即返回。這只是示意用程式, 沒防錯也沒控管記憶體的用量:

class Entry(object):
   ...

g_cache = {}

class GetHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def post(self):
        global g_cache

        key = self.get_argument("key", None)
        entry = g_cache.get(key, None)
        if not entry:
            entry = g_cache[key] = Entry()

        value = entry.get_value()
        if value is not None:
            self.write(json.dumps({
                'error_code': ERROR_OK,
                'value': value,
            }))
            return

        # Wait the data to be ready.
        timeout = self.get_argument("timeout", DEFAULT_TIMEOUT)
        now = tornado.ioloop.IOLoop.current().time()
        timeout = now + float(timeout)
        condition = entry.get_condition()
        try:
            yield condition.wait(timeout=timeout)  # Yield a Future.
        except Exception, e:
            logging.exception('condition timeout? timeout=%s' % str(timeout))
            self.write(json.dumps({'error_code': ERROR_TIMEOUT}))
            return

        value = entry.get_value()
        self.write(json.dumps({
            'error_code': ERROR_OK,
            'value': value,
        }))


class SetHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def post(self):
        global g_cache

        key = self.get_argument("key", None)
        value = self.get_argument("value", None)
        entry = g_cache.get(key, None)
        if not entry:
            entry = g_cache[key] = Entry()
        entry.set_value(value)
        condition = entry.get_condition()
        condition.notify_all()
        self.write(json.dumps({'error_code': ERROR_OK}))

application = tornado.web.Application([
    (r"/get", GetHandler),
    (r"/set", SetHandler),
])

完整的程式見開頭的連結。

apache ab 使用 post 傳資料

用法:

$ cat post.txt
key=value&key2=value2

$ ab -n 1000 -c 1000 -p post.txt -v4 -T 'application/x-www-form-urlencoded' http://.../

注意使用 -p 指定 post data 時,同時必須用 -T 指定傳送 content type 為'application/x-www-form-urlencoded。反應不如預期時可用 -v 輸出收到的資料,協助除錯。

2015年6月1日 星期一

Chrome NaCL 開發心得小記

目前開發到一半, 先加減記一下。之後有多什麼心得再來補充。

為什麼要用 NaCL

自然是方便重用既有的 C/C++ 程式啦。

雜項

  • NaCL 無法在本機檔案中執行 (file://...)。web pages、Chrome Web App、 Chrome Extension 才能用 NaCL。若要在本機試 web pages 的話, 需要跑 local web server。NaCL SDK 內有附一個 local web server, Getting Started 的範例用 make serve 執行 local web server。建議先玩 Getting Started 裡面的兩個範例, 了解 NaCL 的架構。
  • 有支援 POSIX 和 ptherad, 還算堪用, 不過 port Linux 的程式時會遇到很多 Linux 才有但 POSIX 沒有的程式碼 (網路、時間、pthread 各式各樣都有)。
  • 分成 PNaCL (編成 bitcode 執行檔, 使用前再轉成執行檔) 和 NaCL (native binary)。若需要放到 Chrome Web Store, 後者比較不方便, 得編多份不同平台的 binary
  • 得用 NaCL SDK 附的工具, 我試過用 nm 和 ar 都無效, 改用 NaCL SDK 附的 llvm-nm 和 llvm-ar 才有正常結果。
  • 若想使用 stdio 的函式 (如 FILE 相關函式), 要另外使用 nacl_io library。需要在 Makefile 補上 -I$(NACL_SDK_ROOT)/include -I$(NACL_SDK_ROOT)/include/pnacl-lnacl_io

除錯

參考資料: 官方文件

1. 基本的除錯方式是透過 stderr 導到執行 Chrome 的 terminal (預設行為), 或寫到 DevTools 的 console (透過 /dev/console1 ~ 3)。

但是導到 DevTools console 的輸出會被限制行寬, 超出的字會被截掉。還有由於 stderr 是 unbuffered, 輸出會亂掉, 若想將 stderr 導到 /dev/console1, C/C++ 程式內記得改設 stderr 為 line buffer ( setlinebuf(stderr) )

2. 開發 extension 時, 即使沒需求, 也可考慮加個 background page, 這樣可以長駐在 Chrome 內, 方便寫 log 到 background page 的 console, 用 DevTools 觀察 console 的 log 除錯。

3. 可以用 NaCL SDK 附的 nacl-gdb attch 到使用 NaCL 的 process, 但是手續頗麻煩的。但是不用 gdb, native code 出錯時更麻煩。

參考資料: Running nacl-gdb

我一開始在 Mac OS X + Pepper 42 試失敗, 後來在 Linux + Pepper 43 試成功了。不確定和平台或版本是否有關, 先前有遇到 Mac OS X + Pepper 42 的 bug (使用 nacl_io library 後, 輸出到 stderr 的資料沒有顯示在執行 Chrome 的 terminal 上), 但在 Linux + Pepper 43 沒事。

以開發 Chrome extension 為例:

  • 執行 Chrome 要加參數 --enable-nacl-debug --no-sandbox
  • 編譯時用 -O0 -g
  • 需要設一些 gdb 參數, 我包成兩個 script, 放在 native code 專案的根目錄:
$ cat run_gdb.sh 
#!/bin/bash

if [ "$NACL_SDK_ROOT" = "" ]; then
        echo "Please set the env variable NACL_SDK_ROOT"
        exit 1
fi

echo -e "\033[1;33m"
echo "Please ensure that Chrome is run with --enable-nacl --enable-nacl-debug --no-sandbox"
echo "Ref: https://developer.chrome.com/native-client/devguide/devcycle/debugging"
echo -e "\033[m"

cd $(dirname $0)
ROOT=$(pwd)
cd ../../
${NACL_SDK_ROOT}/toolchain/linux_x86_glibc/bin/i686-nacl-gdb -x sync/nacl/nacl.gdb
$ cat nacl.gdb 
target remote localhost:4014
remote get nexe my.nexe
file my.nexe
remote get irt my.irt
nacl-irt my.irt

先重新載入 extension, 再執行 run_gdb.sh, 成功後會出現 gdb 的 prompt。注意, 執行 Chrome 有加 --enable-nacl-debug 時, 就一定要跑 nacl-gdb, 因為 Chrome 會停住 NaCL process, 等著被 gdb attach。

在 Ubuntu 14.04 上編 stable chromium 43

參考資料

步驟

1. 從這裡找到最近的 stable version VERSION

2. 取得程式和安裝相依套件

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ export PATH=$PATH:/path/to/depot_tools
$ fetch --nohooks chromium
$ fetch --nohooks blink
$ cd src
$ ./build/install-build-deps.sh
$ git checkout VERSION  # 可和 git tag 對照一下
$ git show VERSION # 取得 commit 編號 COMMIT
$ (edit ../.gclient) # 設 "safesync_url" 為 COMMIT
$ gclient sync --with_branch_heads  # 沒有 --with_branch_heads 會出錯

3. 產生編譯所需的檔案和編譯

$ cd /path/to/chromium/src
$ gclient runhooks
$ ./build/gyp_chromium
$ ninja -C out/Debug chrome

2015年5月25日 星期一

macports 和 aptitude / apt-get 指令對照

有用到再繼續補充:

操作Ubuntu apt-get / aptitudeMacports
列出已安裝的套件dpkg -l port installed
列出已安裝套件 PKG 包含的檔案pkg -L PKG port contents PKG
搜尋包含 NAME 字串的套件aptitude search NAME port search NAME
顯示套件 PKG 的詳細資訊aptitude show PKG port info PKG

2015年5月24日 星期日

JavaScript 測試 framework 評估小記

找了一下 javascript unit test 的 framework, 看了一陣子, 還是很久以前試過的 QUnit 最順眼, 載入一個 JS, 一個 CSS 檔就可以用了, 語法也簡單 (或著說比較習慣, BDD 風格看起來頗囉唆的)。

一開始不小心找到牛刀 Karma (是 test runner 不是 test framework), 糊里糊塗的裝來用, 看看預設用的 Jasmine, 還有 Jasmine, Mocha 和 QUnit 的比較文 (前兩者是 BDD 風格)。順便得知可搭配 PhantomJS 作 headless browser 測試。那天真的需要 Karma 時再來用吧。或是直接找個 QUnit + PhantomJS 的 runner, 大概也可滿足下一層級的需求。

參照這幾篇應該可以順利裝好 Karma。用Ubuntu 12.04 的話, 需要先手動升級 npm 至新版, 才能成功安裝 Karma。

雖然不會去用牛刀級的 framework, 看牛刀級的 framework 可以得知生態圈的全貌以及相關的熱門套件, 也是不錯的入門方法。

大幅提升開發時間的 vim 必備 plugin: syntastic

用 IDE 開發的一大好處是: 寫程式的當下, IDE 會即時編譯程式碼, 指出編譯錯誤的地方。可以大幅縮短「寫碼 -> 編譯失敗 -> 寫碼 -> 編譯失敗 -> ...」的流程。VIM 可透過 syntastic 達到一樣的效果, 存檔後立即編譯, 然後指出編譯錯誤的地方。Script language 也有同樣效果。安裝後我不需改任何設定就可以愉快地寫 C++ 和 Python。

通常寫 C/C++ 時, 會在編譯時加上 "-I/path/to/header", 這樣編譯器才知道上那找標頭檔。syntastic 同樣地也需要這個設定。設定方法是在程式碼的目錄下 (或父目錄, 或父父目錄...) 建立 .syntastic_cpp_config , 裡面直接寫 "-I/path/to/header", syntastic 就知道上那找標頭檔了。

參考資料: http://stackoverflow.com/a/19143873/278456

2015年2月23日 星期一

字型雜記

sans-serif vs. serif

單字的字義就說明了一切: serif (襯線字) 是在字的邊端加裝飾 (如垂直線條), 用意是利於辨識相似的字, 而 sans-serif (無襯線字)則是沒有加裝飾。serif 最早是用在西方文字印刷體的內文, 如今在中文字或是電腦、行動裝置螢幕上, 情境大不同, 無此必要。

中文相對於的例子是明體的橫線右上角有個三角形, 類似 serif; 而黑體則沒有, 類似 sans-serif。字型的運用大家有不同的說法, 原本我以為內文該用明體, 標題用黑體, 就如同 serif 和 sans-serif 的用法。但是《字型散步:日常生活的中文字型學》用內文用黑體, UDN 改版後內文也用黑體。我現在也覺得黑體較順眼 [*1]。

另外, 明體也有可能用在大字, 《字型散步》舉例福音戰士用明體作為大字, 就用得很漂亮, 所以還是要看選用的字型。換句話說, 了解這些知識是清楚它們的特色。如同其它CS技術一樣, 要依配套措施決定如何使用。

明體特色

除水平線的右上角有個三角形外, 還有水平細, 直筆粗, 避免文字擁擠。

黑體特色

筆劃筆直, 大多無襯線。

康熙字典體

直接取用康熙字典內的字, 沒有修正, 相較之下較破碎和零亂。原本應該是缺點, 但因為與眾不同, 反到成了優點, 每個字看起來比較「有感覺」, 比較文青。

定寬字和比例字

中文字無此差異, 主要是西方文字、數字和符號有差。

細明體是定寬字, 裡面附的英數字每個字都占「半格」; 新細明體是比例字, 英數字是依字的實際大小決定寬度。用 terminal 或開發程式的 IDE, 習慣用定寬字。閱讀內文則是用比例字。

字體和字型

字體是概念, 字型是實踐的產品格式。設計一套字體, 然後作成字型供電腦使用。類似音樂和 MP3。不過大多數情境兩者混用也通。

瀏覽器用字的順序和方塊字

瀏覽器會依 CSS font-family 定的順序找字, 有找到就用, 沒找到就用下一個。依此規則, 一般會先放英文字型再放中文字型, 因為英文字型不會附中文字, 但中文字型會附英文字。藉此用比較好看的英文字型, 沒有的時候再套用中文字型附的英文字。看到方塊字的話, 我猜應該是字型裡沒作這個字的樣子, 卻填了個方塊, 瀏覽器就拿去用了。如果字型檔不附上方塊圖, 應該可以選用下一個字型。實際情況如何, 要研究字型檔和字型函式庫才知道了。

日文字型和中文字型

有些日文字型比較漂亮, 但是用日文字型有個風險, 少部份字的樣子可能寫法和中文不同, 或是缺字造成方塊字。待有看到強烈的例子再來思考是否因此避免使用日文字型。

像 UDN 目前用的字體順序是「Helvetica,Heiti TC,Segoe UI,Meiryo,微軟正黑體」, 其中 Meiryo 是日文的 sans-serif, 我用的 Windows 7 有內建 Meriyo微軟正黑體, 因此瀏覽器會選用 Meriyo。但是... Meriyo 真的比微軟正黑體好看, 沒遇到什麼狀況的話, 我應該也會用 Meriyo 吧。

Web Font

Web Font 是在載入網頁時再立即下載用到的字的字型, 因為用到的字不多, 理論上載入速度很快。Web Font 讓使用字型更為靈活, 可避免用圖替代文字。利於日後修改還有內文搜尋、複製貼上。

我覺得有兩個情況不適合使用 Web Font:

  • 內文: 用在內文時, 有時會看到內文一片白, 然後才顯示出來。慢了一點還以為網頁壞了, 就關掉網頁了。
  • 行動裝置, 特別是手機: 用 2.5G/3G 上網時 latency 遠大於一般的情況, 即使下載的字型檔只有 1 byte, 也可能等個一會兒。雖然 4G 大幅縮小 latency, 但普及率不高, 目前還不是使用時機。

另外使用在粗體字時 (如標題), 要小心 Web Font 的設定。如果設定只取普通的字型, 瀏覽器會自己模擬出粗體, 結果就不好看了。配套解法見 Fake Bolding of Web Fonts

其它

還有許多有趣的知識, 照樣抄寫下來有些瑣碎, 這裡只有留日後自己可能會用到的東西。另外留下參考資料供日後備查。

參考資料

備註

1. 懶得改既有 blog 設定了, 因為不能只是改字型, 字高段落間隙和標題等都要改。要作新網站的時候, 再多放些心思在字型上吧。

2015年2月6日 星期五

跳掉檔案開頭數 bytes 的作法

假設有個大檔案 (e.g. 500MB) 在開頭存有 meta data, 後面存有內容, 要怎麼跳掉開頭的 meta data, 只留下內容呢?

命令列的作法

這裡看到兩個有效率的作法:

使用 subshell: 先用一個 dd 跳掉開頭 1131 bytes (block size=1131, read 1 block), 再用第二個 dd 用正常的 block size 讀寫檔, 這樣效率就和直接用 dd 複製檔案差不多了。

$ ( dd bs=1131 count=1 of=dev_null && dd bs=4K of=out.mp3 ) < 100827_MR029_LobbyControl.mp3

使用 pipe 配合 a group of commands: 概念差不多, 只是改用 pipe。

$ dd if=filtered.dump bs=512k | { dd bs=1131 count=1 of=/dev/null; dd bs=512k of=trimmed.dump; }

Python 不建立新檔的作法

產生新檔案的時間成本有點高, 可能會花到數秒。如果是 Python 程式內要用的話, 可以直接用 file object 然後想辦法跳掉開頭資料。

偷懶的作法是直接用 file object, 然後先呼叫 file.read(N) 跳掉開頭 N bytes。缺點是若 file object 會傳入 third-party lib 使用, 其它程式碼可能會用 seek() 回頭讀開頭的內容。

保險的作法是自訂一個 file object wrapper。這個 wrapper 行為和內建 file object 一模一樣, 只是讀不到開頭數個 bytes。這樣效率和一般的 file object 幾乎沒差。 程式碼見這裡, 另外這裡是測試碼。關鍵是覆寫和移動檔案位置有關的 seektell。然後用 Python 提供的 __getattr__(self, attr) 實作剩下沒改到的 method。

那麼, 要怎麼知道該覆寫那些 method 呢? 用 ipython 建一個 file object, 然後利用補字功能, 再一個個看 method 說明, 很快就知道該覆寫那些了。

以下是示意的操作過程:

$ ipython

In [1]: f = open('/etc/fstab')

In [2]: f.<TAB>
f.close       f.encoding    f.fileno      f.isatty      f.name        f.next        f.readinto    f.readlines   f.softspace   f.truncate    f.writelines
f.closed      f.errors      f.flush       f.mode        f.newlines    f.read        f.readline    f.seek        f.tell        f.write       f.xreadlines

In [2]: f.close?
Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method close of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
close() -> None or (perhaps) an integer.  Close the file.

Sets data attribute .closed to True.  A closed file cannot be used for
further I/O operations.  close() may be called more than once without
error.  Some kinds of file objects (for example, opened by popen())
may return an exit status upon closing.

In [3]: f.encoding?

...

In [21]: f.seek?

Type:       builtin_function_or_method
Base Class: <type 'builtin_function_or_method'>
String Form:<built-in method seek of file object at 0x1a9a660>
Namespace:  Interactive
Docstring:
seek(offset[, whence]) -> None.  Move to new file position.

Argument offset is a byte count.  Optional argument whence defaults to
0 (offset from start of file, offset should be >= 0); other values are 1
(move relative to current position, positive or negative), and 2 (move
relative to end of file, usually negative, although many platforms allow
seeking beyond the end of a file).  If the file is opened in text mode,
only offsets returned by tell() are legal.  Use of other offsets causes
undefined behavior.
Note that not all file objects are seekable.

...

2015年1月29日 星期四

取得執行檔所在的目錄名稱

有時自己寫的程式, 會在執行檔所在的位置放其它資源檔或設定檔。如果偷懶直接用相對路徑讀檔, 可能因為先前有用 chdir 切換 process 所在的位置, 而讀不到檔案。保險見起, 可以改用執行檔所在的位置作為起始路徑來讀檔。

這裡有取得執行檔所在的目錄的程式碼, 重點是利用 /proc/self/exe 找出產生目前 process 的執行檔的路徑。平時需要寫 shell script 也可用同樣方法取得路徑

2015/02/06 更新

原本寫用 /proc/PID/exe, 經 Kito Cheng 提醒, 改為 /proc/self/exe 更方便。

2015年1月25日 星期日

The Python Standard Library By Example

看到不錯的書: The Python Standard Library By Example, Table of Contents 有不少有用的關鍵字,知道要做什麼事的時候,可以用什麼內建模組。

另外, 這裡有書上附的範例碼, 玩玩小程式比看文件快上手。

抓網站內容和使用 lxml.html 讀取 DOM 內容

lxml 功能強大, 不過提供太多 API, 不太容易在官網找資料 (或是我太沒耐性吧...)。記錄一下抓網站取內容常用的 code snippet:

下載網頁內容轉成 lxml.html 的 Element object

import sys

import requests
import lxml.html

DEBUG = False

def get_web_content(link):
    if not link:
        return None, lxml.html.fromstring(u'<html></html>')
    try:
        r = requests.get(link)
        try:
            content = r.content.decode('UTF-8')
        except UnicodeDecodeError, ude:
            if DEBUG:
                msg = (
                    'The content is not in UTF-8 (ude=%s). '
                    'Try ISO-8859-1 instead.\n' % ude                )
                sys.stderr.write(msg)
            # Try another encoding. If fail, just let it fail.
            content = r.content.decode('ISO-8859-1')

        if DEBUG:
            sys.stderr.write('Get content of link %s: %s\n' % (link, content[:50]))
        return r.status_code, lxml.html.fromstring(content)
    except Exception, e:
        if DEBUG:
            sys.stderr.write('Fail to get content of link: %s (e=%s)\n' % (link, e))
        return None, lxml.html.fromstring(u'<html></html>')

使用 cssselect 取值

取得 lxml.html Element 後, 用 cssselect() 和 CSS path 可輕鬆取值。剩下的就是在 ipython 上試 API, 看怎麼操作 Element 物件, 這樣比看文件快。

python multiprocessing 小記

因為 CPython 有 GIL 的緣故, 需要提升 CPU 效率時, 不會用 multi-thread, 會改用 multi-process。內建模組 multiprocessing 提供許多好東西, 實作 multi-process 簡單許多。習慣用 multiprocessing 後,有時不是 CPU bound 而是 I/O bound, 我還是用 multiprocessing。反正....不是 CPU bound 的話, 多耗 CPU 也無所謂。

之前寫 crawling 的程式,為了增加同時抓網頁的數量,就用 multiprocessing 加速 [*1]。以下是兩個例子:

從例子裡找片段程式碼來用比看文件快。這裡記錄一下相關心得:

  • 若要讓不同 process 共享資料,需要用 multiprocessing 訂的物件, 有含 list, dict 等,單一物件 (如 int, float, str) 則是用 Value 指定 typecode, 如: shared_integer = multiprocessing.Value('i', 0)
  • 使用 shared data 會有 race condition, 要用 Lock 保護,不然就直接用 Queue 或 Pipe
  • 還有 EventSemaphore 等物件,要作比較複雜的機制時,可以拿來用。
  • 有提供跨機器的 multi-processing, 有需要再來細看。
  • 程式架構照 producer-consumer 的方式寫比較容易, 也就是各個 process 共用 Queue, 然後 producer 用 blocking put, consumer 用 blocking get, 另有一個 main process 監控情況, 確保程式會結束 (這大概是最麻煩的地方)。
  • 注意量大的時候 put 會因 queue full 卡住,所以不能在 main process 用 blocking put。即使很容易填入 Queue 的初始資料,用另外的 process 填入初始資料比較保險, 因為在初始資料太多時,可能因 queue full 卡住不動了。不然就是先產生 consumer process 再初始化 Queue, 避免因為沒有 consumer 而在 queue full 時卡死。我偏好用另外的 process 初始化 Queue 的資料, 因為新增 process 成本不高 (只有一次), 程式邏輯比較單純。
  • 雖然有 non-blocking put/get, 但是使用 non-blocking 操作, 要多記操作是否失敗, 失敗要另排時間重試, 還是新增 process (or thread) 用 blocking 操作易寫易懂。
  • 各種意外都有可能發生,process 因 exception 掛掉的話,可能會讓 main process 搞錯情況而不會結束,process 執行的函式最上層要加個 try-catch 確保發生意外時能重置管理 process 的狀態,確保 main process 會結束。crawl_ratings.py 分成 _parse_url() 和 _do_parse_url() 就是要處理這個問題。
  • multiprocessing 有提供 log, 可開啟輸出到 stderr, 方便追蹤各 process 情況。用法是: multiprocessing.log_to_stderr(logging.INFO)

備註

*1 遇到 I/O bound 時, 除 multi-thread, multi-process 外,還有一個選擇是用 single thread event-based 的解法。Python 有 gevent 可用。

但是 gevent 的缺點是,一但用了 gevent, 全部 I/O 都要用 gevent, 不然 main thread 就被卡住了。於是 third-party 程式也要找使用 gevent 的版本。比方說 requests 很好用,但用了 gevent 後要用 gevent 版的 requests, 用其它公司提供的 SDK, 也要自己 patch 成 gevent 版本, 所以使用前要三思。不是要作到 C10K 等級的 daemon, 還是用 multiprocessing 省事。之前作一個上線的服務是用 gevent, 為此費了一些工夫作到全盤使用 gevent

相關資料

2015-09-01 更新

懶得研究這麼多可以考慮用 prunner.py

2015年1月23日 星期五

用 jQuery load 跨 HTML 檔使用重覆的 HTML 內容

寫網頁到一定規模後, 會抽出重覆的 JavaScript 到獨立的 JavaScript 檔, 再用 <script src="FILE.js"></script> 載入, 以供不同 HTML 使用; CSS 則是用 <link rel="stylesheet" type="text/css" href="FILE.css"> 載入外部檔案。那 HTML 怎麼辦?

若是會寫 PHP 的人, 會使用 include 載入重覆使用的 HTML; 用 Python/Ruby/Perl 寫 CGI 的話, 會搭配 web framework 內 template language 的語法, 所以也沒問題。但若是只會寫 HTML + CSS + JavaScript 的人該怎麼辦? 查了一下, 發覺用 jQuery load 可以輕易做到 [*1]。唯一的問題是, 在本機電腦實作, 用瀏覽器開啟本機檔案後會發現行不通。有如下的 JavaScript 錯誤訊息:

XMLHttpRequest cannot load file:///C:/.../FILE.html. Cross origin requests are only supported for HTTP.

這是因為瀏覽器基於安全考量, 禁止 JavaScript 讀取本機檔案

解套方式是在本機跑 web server, 透過 http (而不是 file://) 讀取檔案。

於是問題變成: 如何讓這類不擅長程式設計或系統管理的人, 能在自己的電腦測試 jQuery load? 畢竟會有這種需求的人, 大概也不擅長在 Windows 或 Mac 裝 apache2 或 lighttpd。從這裡看到有人推薦用 mongoose

作法如下:

  1. 下載 mongoose 執行檔
  2. 執行檔放在網頁目錄下
  3. 執行它就可以連到 port 8080 看到結果 (即連往 http://localhost:8080/ 瀏覽目錄下的網頁)。

也可參考官網的教學了解更多設定

要關掉 mongoose 的話, 可以從右下角的系統選單找到 mongoose 的圖示, 按右鍵再選 Exit。或從系統管理員直接結束它。

Btw, 另一個作法是安裝 Python, 然後跑 SimpleHTTPServer:

python -m SimpleHTTPServer 8080

不過得和非程式設計非系統管理背景的人解釋一下命令列就是了。

備註

*1: 事實上這個作法沒有 PHP include 或 CGI-based + template language 的作法好, 因為網頁內容不是一次載入, 而是透過 AJAX 補上, 會延遲內容出來的時間。不過作小東西或雛型的時候, 可忽略這一點負擔。

2015年1月18日 星期日

網頁顯示彈出畫面同時禁止捲動主畫面的技巧

作法應該有相當多種,這裡提兩種我從一些網站中觀察到的作法。

設定 body 為 overflow:hidden

準備兩個 div, 一個是 main content (id=main), 一個是 pop-up (彈出畫面), 平常隱藏 pop-up, 要顯示 pop-up 時,設 body 的 overflow: hidden,並顯示 pop-up。這樣主頁面就不能捲了。

關掉 pop-up 時再設 body 的 overflow: visible (預設值), 主頁面就可以捲了。

程式碼像這樣:

var popup = document.getElementById('popup');
function show_popup() {
  popup.style.display = 'block';
  document.body.style.overflow = 'hidden';
}

function close_popup() {
  popup.style.display = 'none';
  document.body.style.overflow = 'visible';
}

對 main content 為 position:fixed

這個作法比較複雜,是拆解 facebook 彈出照片的頁面得知的。

準備兩個 div, 一個是 main content (id=main), 一個是 pop-up (彈出畫面), 平常隱藏 pop-up, 要顯示 pop-up 時,設 main content 的 position: fixed,並顯示 pop-up。這樣主頁面就不能捲了。但是因為 scroll offset 歸零 (content height 改變的副作用), 要自己維護 scroll offset (後述)。

這個作法與上個作法有幾點差異:

  • 網頁主動管理頁面大小, 造成主頁面不能捲; 上個作法是設 body 為 overflow: hidden, 隱含瀏覽器會禁止捲動主頁面。這個規則有點詭異,多數瀏覽器應該有這麼作吧。
  • 因為 main content 變成 position: fixed, 它的高度也就變成 viewport height, 然後 body 的 height 也因此縮水, 最後主頁面的內容高度和 viewport height 一樣,所以就不能捲。但也因如此, scroll offset 會歸零,若之前有捲動主頁面才顯示彈出畫面, 背景的主頁面會跑掉,所以要自己記下 scroll offset, 並在 main content 設 top = -scrollY, left = -scrollX, 這樣畫面才會定住。取消彈出畫面後,要再自己 scroll 回當初的位置。程式碼如下:
var main = document.getElementById('main');
var popup = document.getElementById('popup');
var scrollX = 0;
var scrollY = 0;
function show_popup() {
  popup.style.display = 'block';
  scrollX = window.pageXOffset || document.documentElement.scrollLeft;
  scrollY = window.pageYOffset || document.documentElement.scrollTop,
  /*
    Set "fixed" and then the height becomes to fit to the window height.
    and then the body height becomes window height.
    You can observe the change by typing "document.body.scrollHeight" in DevTools console
  */
  main.style.position = 'fixed';
  main.style.left = -scrollX;
  main.style.top = -scrollY;
}

function close_popup() {
  popup.style.display = 'none';
  delete main.style.left;
  delete main.style.top;
  main.style.position = 'static';
  window.scrollTo(scrollX, scrollY);
}
  • 優點: 跨瀏覽器 (大概吧,不然 facebook 不會這麼作)
  • 缺點: 寫起來比較囉嗦, 而且顯示或關掉彈出畫面後, 頁面內容高度和 scroll offset 會改變, 若網頁有針對這些事件作事,要另外處理這裡觸發的「false event」

其它

若要幫彈出畫面設背景是半透明的黑畫面,要用 background + rgba 設秬明度。另一個作法是用 opacity,但是 opacity 會順便影響到子元素,所以用 background 配上 rgba 設值,比較合適。例如:

background: rgba(0, 0, 0, 0.5);

2015年1月10日 星期六

Python 處理 UTF-16 的 CSV

Python 內建的 csv 模組預設無法處理 UTF-16, 直接讀的話會出現

Error: line contains NULL byte

自行轉換編碼成 UTF-8 即可,作法如下:

import codecs
import csv

# 用 codecs 解碼 utf-16 為 unicode object
f = codecs.open(filename, 'rU', 'utf-16') 

# 再轉回 utf-8 給 csv.reader 用
cr = csv.reader(line.encode('utf-8') for line in lines)
for row in cr:
   # row 的內容就是 CSV parse 完的結果

或是先用 iconv 轉檔, 再用 Python csv 模組處理亦可:

$ iconv -f utf-16 -t utf-8 file_in_utf16 > file_in_utf8

備註: Python csv 模組不接受 unicode 的輸入,若來源不是檔案,也要記得轉成 utf-8 再傳入 csv.reader()

自動統計 app 在 Google Play 內各版本的的分數和常見問題

最近想統計 app 在 Google Play 上各版的分數,了解最近版本是否比較不穩定。如果有問題的話,統計一下問題分成那幾類。透過 Google Play 觀看 review,如果沒有邊看邊統計分類,看過只會有個模糊印象,不知道那類問題比較嚴重。但是.......又懶得邊看邊記。

為了自動分類 reviews, 首先要抓下 Google Play 上的 review。幸好 Google Play 存放 reviews 和其它資料在 Google Cloud Storage, 並提供工具 gsutil 可以從命令列取出 Google Cloud Storage 的資料, 作起來並不費工。官方說明在這裡的 "Export ratings and reviews"。摘要步驟如下:

1. Mac 或 Linux 下載解開 gsutil:

https://cloud.google.com/storage/docs/gsutil_install

2. 更新、設定 gsutil:

$ export PATH=$PATH:/path/to/gsutil
$ gsutil update
$ gsutil config

其中 gsutil config 會認證帳號, 照著作即可。最後要求輸入 project id 的時候, 輸入 app package name,例如 com.mycompany.myapp。相關說明見這裡

3. 下載 review

從 Google Play App Console -> App -> Ratings & Reviews 最下方得知 reviews 存放的位置

Ratings and reviews are also available for programmatic access through Google Cloud Storage and the gsutil tool. Your data, updated daily, is stored in this private bucket:
pubsite_prod_rev_NUMBER

接著用 gsutil 列出目錄下的檔案

$ gsutil ls gs://pubsite_prod_rev_NUMBER

再來就簡單了,gsutil 用法和 shell 指令差不多,有 ls, cp 之類的。下面的指令會取出所有 reviews 到本機目錄下:

$ data=$(gsutil ls gs://pubsite_prod_rev_NUMBER/reviews/)
$ for f in $data; do gsutil cp $f . ; done

取出來的檔案是每個 app 每個月一份 CSV 檔, 編碼為 UTF-16。

取得 CSV 後,就可以自行寫程式分析了。這裡有我寫的一份簡單版本,將要觀察的 CSV 檔放到同一目錄下,執行 script 帶入目錄名稱,會輸出各版本的分數還有統計最新版 reviews 裡面出現關鍵字 (如 "crash", "freeze" ) 的數量。需要用的話,記得修改 OBSERVERED_WORDS 以符合自己的需求。

2015年1月4日 星期日

線上刷卡流程的簡介

同事介紹關於線上刷卡流程的介紹文章, 滿淺顯易懂的, 改天有需要再複習一下:

git 常用指令

有鑑於一兩年沒用 hg 就忘得差不多了, 還是備忘一下目前覺得好用的 git 指令, 比較長的就加到 alias 裡了。

還沒實際用過, 先記著:

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...