阅读 409

使用 Native Messaging 来开发 WebExtensions

(图来自火狐壁纸 — 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" ]
}
复制代码

原生应用编写

重点来了!!

浏览器端调起原生应用,通过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 && .\target\debug\native_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 是一个全球社区,携手致力于让互联网保持开放、公开且人人可用。

关注下面的标签,发现更多相似文章
评论