使用 Native Messaging 來開發 WebExtensions

花生PeA發表於2019-02-25

(圖來自火狐桌布 — 2017年11月

WebExtensions是用來開發Firefox擴充套件(Extension)的技術,與其他瀏覽器擴充套件介面有很大程度的相容(比如Chrome擴充套件介面(Chrome Extension API)、Edge)。

關於WebExtensions相關教程可以訪問MDN火狐社群:如何開發 WebExtensions 擴充套件檢視,本篇文章主要介紹其Native Messaging部分,也就是瀏覽器擴充套件與本機應用通訊,並追溯一個需用到Native Messaging的擴充套件火狐捷徑的開發實現。


背景

由於Firefox 57+廢棄了除WebExtensions外的所有擴充套件,所以需要將很多實用舊式技術的擴充套件遷移到WebExtensions來。於是,將擴充套件 火狐捷徑 遷移到WebExtensions的成為了任務之一。

擴充套件 火狐捷徑 完成的功能簡單來說就是:

在Firefox中開啟計算器、記事本、畫圖等程式

當然,僅支援Windows系統。

看起來功能很簡單,但是很遺憾WebExtensions API並沒有提供如此細膩的介面來直接調起系統裡的程式,能想到的唯一實現的方式,就是Nativing Messaging。

關於Nativing Messaging,簡單來說就是:

在使用者的計算機上放一個.exe檔案,然後擴充套件將這個.exe檔案調起,並與之通訊

擴充套件結合Native Messaging開發幾乎可以掃清WebExtensions API許可權不足的障礙。所以火狐捷徑的功能也可以得以實現。

準備

MDN上關於Nativing Messaging的教程主要有2篇:

一篇講原理,一篇講配置。英語略無力,於是邊研究邊把文章翻譯成了中文:

研表究明,需要讓具有Native Messaging的擴充套件跑起來,需要:

  • 一個擴充套件(廢話)
  • 一個可被調起的原生應用(比如Windows的.bat.exe檔案)
  • 正確的配置了原生應用(Windows是修改登錄檔)

都不難實現,正確的引導使用者即可。可惜遺憾的是,筆者是名Web前端,所以不會寫能打包成.exe的編譯語言。

  • JavaScript打包.exe體積巨大
  • Windows不原生支援Python(而且筆者也不會)
  • C/C++基本上都忘光還給學校了

於是……筆者入鄉隨俗學了Rust:

所以,接下來的原生端例子將會是Rust語言寫的。

可能觀看本文的讀者你不瞭解Rust甚至完全沒聽說過,不過不要緊,Rust與C一樣是十分底層的語言,通過本文你可以更加細緻的瞭解Messaging底層實現而不是Rust語言本身。

這裡要吐槽一下,MDN文章中給出的例子的Native端是用Python寫的,幾乎只有一段程式碼沒有過多的說明。這給不會Python的筆者造成了許多麻煩——沒辦法通過閱讀Python程式碼來找到Messaging細節。當然,本文不存在這個問題。

開發

瀏覽器部分

Native Messaging使用的介面是browser.runtime.sendNativeMessage,方式與browser.runtime.sendMessage幾乎沒什麼區別,只是發給的物件有所不同。(當然還有基於連線的訊息傳送(Connection-based messaging)方式)

於是,筆者先在瀏覽器部分還原了原先火狐捷徑的瀏覽器動作按鈕(Borwser Action)和彈出層(Popup)。

browser-action-and-pupop

當然,砍掉了一些功能,現在火狐捷徑只能開啟圖中的4個程式。

UI實現沒什麼好說的。接下來繫結事件,將列表中的每個元素繫結點選事件,使用者點選時呼叫browser.runtime.sendNativeMessage傳送訊息,並在獲取到返回值後關閉視窗。

程式碼差不多是這樣的:

item.addEventListener(`click`,async function(){
    await browser.runtime.sendNativeMessage(
        `native_launcher`,
        {}// 傳遞給名字為native_launcher的原生應用的資料
    );
    window.close();
});
複製程式碼

需要注意的是,不要過早的呼叫window.close()來關閉彈出層,否則彈出層指令碼被過早的關閉會導致其收不到來自原生應用端的響應。這時瀏覽器工具箱(Browser Toolbox)通常會丟擲一個NS_ERROR_NOT_INITIALIZEDcan`t access dead object錯誤。

瀏覽器端的擴充套件差不多就是這樣,接下來是程式碼中的native_launcher原生應用配置與編寫。

原生應用配置

配置包括2點,清單檔案(manifest)的配置和登錄檔(Windows)的配置,關於配置方面,MDN中原生應用清單已經講的很詳細了。

下面是火狐捷徑的原生應用通訊清單(Native messaging manifests)與登錄檔的配置:

{
    "name": "native_launcher",
    "description": "Launch the native app of you computer.",
    "path": "target/debug/native_launcher.exe",
    "type": "stdio",
    "allowed_extensions": [ "quicklaunch@mozillaonline.com" ]
}
複製程式碼
使用 Native Messaging 來開發 WebExtensions

原生應用編寫

重點來了!!

瀏覽器端調起原生應用,通過stdio來收發資料。

在擴充套件向原生應用通過stdin發訊息時,會先:

  • 將訊息序列化成UTF-8字串
  • 計算字串的位元組

然後傳送資料,資料包括:

  • 上面計算得到的位元組數,用4個位元組表示
  • 訊息正文

還需要留意的一點是,位元組序使用本機位元組序,不要直接設定大端或小端。

每條訊息都將是JSON格式的,也就是說,如果原生應用想要返回一個字串hello,需要返回的訊息是"hello",這樣才符合JSON格式。

接下來的Rust程式碼均包括如下頭:

use std::env;
use std::process::Command;
use std::os::windows::process::CommandExt;
use winapi::winbase;
use std::io::{Read,Write,self};
use std::mem::transmute;

#[macro_use] extern crate json;
extern crate winapi;
複製程式碼

下面是Rust讀取stdin返回字串的函式:

//當傳入0時表示讀取前4個位元組來表示資料長度
fn get_stdin(len:usize) -> String{
    let mut stdin =io::stdin();

    let mut stdin_len:u32;
    if len==0 {
        let mut stdin_len_byte =[0u8;4];
        stdin.read_exact(&mut stdin_len_byte).unwrap();
        stdin_len =unsafe{transmute(stdin_len_byte)};
    } else {
        stdin_len =len as u32;
    };

    let mut byte =[0u8;1];
    let mut data :Vec<u8> =Vec::new();

    while stdin_len >0 {
        stdin_len =stdin_len-1;
        io::stdin().read_exact(&mut byte).unwrap();
        for elt in byte.iter() {
            data.push(*elt);
        };
    };

    return String::from_utf8(data).unwrap();
}
複製程式碼

返回訊息時,也遵從與接受同樣的規則,先需要在stdout輸出4個位元組表示正文位元組的長度,然後才是正文字身。

下面是Rust接受字串輸出到stdout的函式:

fn send_stdout(message:&str){
    let mut stdout =io::stdout();

    let message_length =message.len() as u32;
    let message_length_bytes: [u8; 4] = unsafe{transmute(message_length)};

    stdout.write(&message_length_bytes).unwrap();
    stdout.write(message.as_bytes()).unwrap();

    stdout.flush().unwrap();
}
複製程式碼

重點還沒完!

Windows中,如果希望主程式退出後依舊保留子程式,需要在程式啟動時傳入一個CREATE_BREAKAWAY_FROM_JOB標記,於是筆者照實做了,調起Windows程式Rust程式碼如下:

Command::new(windows_path+app_path)
    .creation_flags(winbase::CREATE_BREAKAWAY_FROM_JOB)
    .args(&app_args)
    .spawn()
    .expect("failed to execute child")
;
複製程式碼

但是執行時報錯了:

Error { repr: Os { code: 5, message: "拒絕訪問。" } }
複製程式碼

仔細看了下MSDN的文件,裡面CREATE_BREAKAWAY_FROM_JOB提到:

The child processes of a process associated with a job are not associated with the job.

If the calling process is not associated with a job, this constant has no effect. If the calling process is associated with a job, the job must set the JOB_OBJECT_LIMIT_BREAKAWAY_OK limit.

也就是說,程式必須被傳入JOB_OBJECT_LIMIT_BREAKAWAY_OK才能在執行過程給調起的子程式傳入CREATE_BREAKAWAY_FROM_JOB

可是怎麼才能給編譯的.exe傳入JOB_OBJECT_LIMIT_BREAKAWAY_OK

如果是直接通過擴充套件呼叫sendNativeMessage來啟動原生應用,那麼是有JOB_OBJECT_LIMIT_BREAKAWAY_OK標記的,但是測試的時候呢?

筆者是這麼做的。啟動一個cmd,在cmd中直接呼叫編譯出的.exe。拿Rust來說,編譯命令是cargo build,而編譯並執行的命令是cargo run。所以,由原先的:

cargo run
複製程式碼

變成了:

cargo build && .	argetdebug
ative_launcher.exe
複製程式碼

這樣調起的native_launcher.exe是擁有JOB_OBJECT_LIMIT_BREAKAWAY_OK標記的。

後記

於是,新版本的火狐捷徑就開發好了。

本篇文章使用到的火狐捷徑的程式碼可以在這裡找到:

最後是喊口號環節:

Mozilla, a global community working together to keep the Web open, public and accessible to all.

Mozilla 是一個全球社群,攜手致力於讓網際網路保持開放、公開且人人可用。

相關文章