利用 Job 内核对象实现父进程关闭时自动结束所有子进程

投稿工具的压制功能一直来都有一个问题:如果主进程被强行结束了(例如利用任务管理器),那么创建的 ffmpeg 压制进程仍然会继续运行。

因为 ffmpeg 是以二进制的方式部署的,因此不存在修改它的代码,自己和主进程建立 IPC 监控的方式。

至于采用远线程注入的方式来强行 HACK,我一直对这种无视客观规律的霸道方式都不太感冒,毕竟我们又不是做安全软件。

所以我想到了曾经在 Windows 核心编程中看到的一个方法:使用 Job 内核对象。

核心方法总结起来就是一句话:将 ffmpeg 压制进程加入到一个 Job 对象中,利用 Job 对象的 Kill-On-Close 特性,在 Job 内核对象被释放时,Job 内的所有进程都会被系统结束。

于是我写了一个 demo snippet,解决了几个坑之后验证了我的想法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <conio.h>
#include <cassert>
#include <iostream>
#include <string>
#include <Windows.h>
bool LaunchAndWait(const std::wstring& cmdline, HANDLE job)
{
wchar_t buf[255] {0};
wcscpy_s(buf, cmdline.c_str());
STARTUPINFO startup {0};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process_information {nullptr};
if (!CreateProcessW(nullptr,
buf,
nullptr,
nullptr,
FALSE,
CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB, // important
nullptr,
nullptr,
&startup,
&process_information)) {
auto err = GetLastError();
std::cerr << "Failed to create process: " << err;
return false;
}
if (!AssignProcessToJobObject(job, process_information.hProcess)) {
std::cerr << "Failed to assign process to job: " << GetLastError();
return false;
}
ResumeThread(process_information.hThread);
CloseHandle(process_information.hProcess);
CloseHandle(process_information.hThread);
return true;
}
int main()
{
BOOL is_in_job;
IsProcessInJob(GetCurrentProcess(), nullptr, &is_in_job);
std::cout << "Is the main process in job: " << is_in_job << std::endl;
std::cout << "Wait for constructing job object!\n";
HANDLE job = CreateJobObjectW(nullptr, nullptr);
if (!job) {
auto err = GetLastError();
std::cerr << "Failed to create job: " << err << std::endl;
return 0;
}
// Enforce limits.
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limits;
memset(&job_limits, 0, sizeof(job_limits));
job_limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &job_limits, sizeof(job_limits))) {
auto err = GetLastError();
std::cerr << "Failed to set job information: " << err << std::endl;
}
std::cout << "Job object created! Launch test subjects...\n";
bool r = LaunchAndWait(LR"(C:\Windows\notepad.exe)", job);
assert(r);
_getch();
return 0;
}

最终的产品代码思路是完全一样的,当然代码不可能像上面这样随意。

这里有两个比较隐蔽的坑(MSDN 上不一定能看到)需要注意一下:

第一个是创建进程时最好带上 CREATE_BREAKAWAY_FROM_JOB,因为 Job 属性存在自动继承性:如果一个进程属于一个 Job,那么他创建的子进程会自动关联到这个 Job。

explorer 和 VS 都是存在于 Job 中的,因此如果在这两个环境下运行程序, IsProcessInJob() 会返回 true

第二个坑特别隐蔽:我一开始的 test subject 是 calc.exe,在 Windows 10 上这是一个 UWP 程序,然而无论我怎么做,calc 进程都活得好好的,一点不像是被约束的样子。。。甚至所有 API 的调用都并没有返回错误。

而当我将 test subject 换成 Win32 程序 notepad.exe 之后,功能就可以正常跑了….

我猜想这大概和 UWP 程序的一些安全特性有关,不过因为本身对安全并没有偏执的热爱,所以在这个点上就没有继续深入了。

Send Specific Cookie in URLFetcher

使用 URLFetcher 作为网络请求的基础组件有一个优势:如果 server 的 response header 指明了 set-cookie,那么 URLFetcher 在发出下一个请求时会自动往 request header 中设置 cookie。

这为使用某些需要在请求时附带 cookie 的 API 提供了便利;但是同时也在某一些场合下导致了一些问题。

如果你需要为某个 API 请求的 request header 中携带自定义的 cookie 数据,例如 cookie: buvid=xxxx,那么,即使你在发送请求前通过 AddExtraRequestHeader() 添加了 cookie 头,你也会惊奇地发现,出去的请求并没有携带你设置的 cookie,而是只有 URLFetcher 自己设置的,服务端提供的 cookie 值。

不要问为什么这个时候要用 cookie 传递这个信息,而不是某个独有的 request header,亦或者 query-string;你永远不知道和你对接的人是一个怎样的智障;毕竟,没有谁会真的去读 RFC。

自定义 cookie 丢失的原因大概如下:

  1. 你首先设置了 cookie,然后发出请求
  2. URLFetcher 在内部实际进行网络请求前,会设置服务端的 cookie 值(因为之前的请求存在 set-cookie
  3. 因为 RFC 规定,一个 request header 中, cookie header line 有且仅有一个,也就是说
1
2
cookie: buvid=xxx
cookie: sig=xxx

是不合法的;虽然可以通过

1
cookie: buvid=xxx; sig=xxx

来传递多个value,但是这要求 URLFetcher 在添加 cookie 时要能够 aware 到这点;很可惜,根据情况来看这点不成立

  1. 于是 URLFetcher 粗暴的替换了你设置的 cookie 值

扫了一遍代码之后,我想到一个 workaround,实践证明确实可以:

发请求前设置 URLFetcher 的 load flag,加上 LOAD_DO_NOT_SEND_COOKIE,阻止 URLFetcher 自己设置 cookie 的行为,完全由自己控制请求的 cookie。

话说用 URLFetcher 被坑了好多次(算上 chromium 其他的 lib,次数可以翻翻),还好每次都能够凭借“优秀工程师的第六感”蒙中问题的 root cause,进而化险为夷。我突然感觉自己理解了什么叫 文章本天成,妙笔偶得之

Bypass Proxy in URLFetcher

net::URLFetcher 默认情况下会使用系统代理,对于针对应用于浏览器而设计的网络组件来说,这是合情合理的;并且这也方便了测试对于网络接口的调试,因为只需要打开 Fiddler 或者 Charles,就可以看到应用发出去的 HTTP 请求。

但是有时候我们又希望默认情况下不开启代理支持,比如:在我用 net::URLFetcher 重写某直播姬的网络通信组件后,出现了不少傻逼用户因为不知道自己系统上为什么会各种乱七八糟的本地代理而导致无法登陆。

这个时候产品的策略就应该改为:默认不使用代理,如果有特殊需要,通过命令行参数启动。

因为产品项目用的 chromium 的代码比较老(估计是 2x 的),这个版本的 net::URLFetcher 只要是使用 URLRequestContextBuilder 创建的 URLRequestContext,那么就一定会使用系统代理,这是写死在代码里的…

而且 URLRequestContextBuilder 也没有提供额外的途径让我们修改;而一旦创建完 URLRequestContext,此时再设置新的 ProxyService 是没有用的…

我觉得这是一个设计上的失误,不知道新版本的代码有没有针对这块做了什么改动。

万幸,URLFetcher 对象可以通过 SetLoadFlags(),使用标志 LOAD_BYPASS_PROXY 来强迫当前 fetcher 对象绕过代理。

于是设计就变成:如果没有外部设置,fetcher 在发出请求前设置 LOAD_BYPASS_PROXY

Afterthoughts

在研究代码过程中我发现 URLRequestContext 理论上是可以使用自己定义的子类的;builder 创建的是内部实现的一个 BasicURLRequestContext。chromium 在这里的设计颇有点机制策略分离的意味。

不过因为这个问题发现在新版本即将提测前,因此就将这个想法列为后续的 TODO 了。

Monthly Read Posts in June 2017

一种头像缓存策略

本来还以为有什么惊天地的策略…这不是很普通的策略嘛…

Move Safety

这篇 post 引入了一个 move-safety 的概念;和 exception safety 类似,move safety 描述了一个实例被移动之后的 post-condition invariant.

Why do we need move safety? Because std::move() creates artificial temporaries, and we might want to use them again after the move operation.

4-Level Move Safety

  • No move guarantee: copy only
  • Strong move safety: moved object is valid and well-defined; like std::unique_ptr
  • Basic move safety: moved object is valid, but its state is unspecified; like std::string, due to possible SSO. It is what the standard library guarantees for all types unless otherwise specified.
  • No move safety: the state is invalid, and you can only call its destructor, or assign it a new value.

Stack and Heap

这篇 post 挺有意思。

作者观点是,无论 heap 还是 stack 来描述一个 C++ object 的 storage location 都是不准确的,因为标准并未规定 storage location 具体是什么,而规定了所谓的 storage duration:Storage duration defines the minimum potential lifetime of the storage that contains the object.

4 standard-defined storage duration:

  • Static storage duration
  • Automatic storage duration
  • Thread storage duration
  • Dynamic storage duration

Mix-in Based Programming in C++
Paper: Mixed based programming in C++

第一篇是介绍 mix-in 的 post,第二篇是在 mix-in 基础上提出一种 mix-in layer 的 paper。

mix-in 是一个挺有意思的 patter,但是因为没有自身语言支持,在 C++ 里的实现有时候会遇到比较 tricky 的问题。

第一篇 post 就提到了一个核心问题:如何有效地解决 mix-in classes 的 construction problem。但是讲真,post 里给出的 solution 有点诡异过头了;如果现实中的工程问题需要这样的一个解决方案,弃用 mix-in 弄不好是一个更靠谱的做法。

至于第二篇 paper,其实核心所谓的 mix-in layer,概括起来就是

1
2
3
4
5
6
7
8
9
10
template <class Next >
class NUMBER : public Next {
public:
class Workspace : public Next::Workspace {
// Workspace role members
};
class Vertex : public Next::Vertex {
// Vertex role members
};
};

paper 中还提到了对工程中使用 mix-in 的一些建议和 recommended practice,看看也还不错。

Connection Management in Chromium

Chromium 官方研发写的 post。

提到了他们试图解决 connection latency & parallelism 的问题。

于是就有这么几个点:

Handshakes (including TCP handshake & SSL handshake) are costly. Therefore, a better connection management is on demand.

Optimizations on transport layer (TCP or SPDY)

.etc

Apply tuple to function efficiently

Yet another implementation of apply-on-tuple

Use base::Bind With std::function

base::Bind()base::Callback 可以看作是对标准库 std::bind()std::function 的模拟;因为 chromium 项目早在 C++ 11 正式通过前就已经存在好多年了。

我为直播姬设计新的网络通信基础组件时,接口的回调 handler 通常设计为 std::function 对象,因为它可以“吸收”任何函数对象,大多数情况下这个设计运转的非常良好;但是这里有一个略微棘手的问题: base::Callback 对象无法被 std::function 使用。

问题本质很简单,因为 base::Callback 不是一个 function object,因为它不支持以函数形式调用(没有提供 operator() 的重载),instead,它提供了一个 Run() 函数来执行这个 callback…

虽然我之前吐槽过好多次 chromium 的很多架构设计完全是 Java-Style 的,但是这里并不打算再拎出来批判一次,我们要聚焦的是如何解决这个问题。

解决思路很简单:既然它不是一个 function object,那么我们就给他包一层 function object 的皮就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "base/bind.h"
template<typename>
class CallableCallback;
template<typename R, typename... Args>
class CallableCallback<R(Args...)> {
public:
using callback_t = base::Callback<R(Args...)>;
explicit CallableCallback(callback_t callback)
: callback_(callback)
{}
R operator()(Args... args) const
{
return callback_.Run(args...);
}
private:
callback_t callback_;
};
template<typename Callback>
auto MakeCallable(Callback callback)->CallableCallback<typename Callback::RunType>
{
using Sig = typename Callback::RunType;
return CallableCallback<Sig>(callback);
}

辅助函数 MakeCallable() 可以将一个 base::Callback 对象转换成一个 function object。

一个简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int Inc(int i)
{
return ++i;
}
void Foo(const std::string&, std::string, int)
{}
class Test {
public:
Test()
: msg_("test"), weak_ptr_factory_(this)
{}
void Bark(const std::string& data)
{
std::cout << data << ":\t" << msg_ << std::endl;
}
std::string msg_;
base::WeakPtrFactory<Test> weak_ptr_factory_;
};
int main()
{
std::cout << "--- test case 1 ---\n";
std::function<int(int)> fn(MakeCallable(base::Bind(&Inc)));
std::cout << fn(5) << std::endl;
std::cout << "--- test case 2 ---\n";
Test* ptr = new Test();
std::function<void()> mfn(
MakeCallable(base::Bind(&Test::Bark, ptr->weak_ptr_factory_.GetWeakPtr(), "inside")));
mfn();
std::cout << "release test instance\n";
delete ptr;
mfn();
return 0;
}

小巧优雅纯天然

禁止程序多实例并存并且自动激活第一个实例

多实例检测非常常规,在程序启动时直接检查用来标记的内核对象是否存在即可,一般都是使用 Mutex

麻烦的点在于如何激活第一个实例,显示它的主窗口。

某直播姬一开始的想法是利用 Pipe 建立 IPC 通讯连接,然后后续实例通过发送消息,让主实例 activate 自己的窗口。

嗯,看起来没毛病,但是做起来问题很多。

首先,IPC 建立需要时间,通讯需要时间,并且断开 IPC 后主实例(Windows 下)一定得需要重新创建一个 Pipe;更加诡异的是,我在本地编译的 Rlease 版本明明可以工作,利用构建机编译出来的版本,子实例管道连接一直失败….

于是看起来 IPC 是一个很自然地选择,实际上却是一个导致复杂度暴涨的下策。

另一个思路是创建一个 Event 内核对象,后续实例直接通过 signal 这个 event 内核对象来通知主实例。

但是这里实现起来有个麻烦的地方:如果要追求简单化,那么就需要一个单独的线程 wait 在这个内核对象上,负责接收通知,缺点是要浪费掉一个线程;如果不想资源浪费,那么考虑到很多框架都提供了 async i/o 的 wrapper,所以可以利用这个点,你开心的话可以挂到 IOCP 上。

嗯,等等,我们是想做啥来着?!

绕了一圈最后想想还是这样算了:

利用 RegisterWindowMessage() 注册一个自定义消息,主实例在 UI message loop / window message procedure 里监听这个消息;子实例启动后就利用 BroadcastMessage() 或者 PostMessage() 广播一下这个消息。

这样有个额外的好处,主实例收到消息直接是在 UI 线程,省掉一次 post task。

某直播姬最后采用的就是这个方案,为此我加了一个 class 起了个名字叫 SingleInstanceGuarantor,嗯,有点中二的感觉。

最后要注意一点,如果框架封装了 window procedure,将几个活动窗口的 window procedure 聚合到一个 message handler 里的话,可能会收到多条消息,可以利用消息的 timestamp 过滤一下。

Monthly Read Posts in May 2017

http resumable download

虽然之前写直播姬自动更新时实现过续传下载,但是功能规范上并没有太完备;而这篇文章很好的补充了几个断点续传中,严格实现会遇到的几个 key points。

例如:不应当假设资源一定支持续传,要首先使用 request header range 检查目标资源是否支持续传,支持 range 的 http resonse code 是 206。

另外,两次断点下载期间,资源可能发生变化,需要在请求时同时附带上 EtagLast-modified 记录,由服务器确定资源是否变化。

Why Exceptions Should be Exceptional

文章从性能角度阐述了为什么不能将 exception 机制作为 routine control flow。

但是个人认为这个切入角度不好,因为很容易给人一种异常机制开销大的错觉,导致读者之后避开使用异常。

异常处理一直都是一个大麻烦,相比 error code handling 不够直观,没有足够的经验很难控制好,加上某些语言自身特性导致固有复杂度暴涨(例如 C++)。

shared-ptr

文章大部分的内容(除了 aliasing)其实 Effective Modern C++ 里都有…

Magical Captureless Lambdas

核心总结出来就是一句话:Captureless lambdas 能够自动转换为对应的 C-Style 函数指针,而在 MSVC 里,implicit cast 能够自动处理不同的 calling convention

Leaky Closures Captureless Lambdas

An entity that is mentioned or used but is not ODR-used within the lambda body, does not need to be captured in the capture list.

Informally, an object is odr-used if its address is taken, or a reference is bound to it.

Lambdas Callbacks

又名:如何用 capturing lambdas 作为 c-style callbacks

Combining Static and Dynamic Polymorphism with C++ Template Mixins
C++ Mixins - Reuse through inheritance is good when done the right way

Mixins pattern in C++ 快速导读

The Very Real Mess of Virtual Functions

十足标题党

并且个人认为,作者试图解决一个前提错误的问题。

文中以 Init()UnInit() 为例阐述观点,然而,这正好说明了为什么如无必要不应该使用 two-phase initialization

文中试图实现的机制不就是 ctor 和 dtor 所做的么……

而对于其他可能需要子类调用父类虚函数的场景,我认为,non-vitual-interface idiom已经足够了。

BTW:就工程领域而言,可能对 GUI 框架来说,文中的一些套路还是有用的;不过因为个人在这方面没有什么经验,因此持保留意见