Content Table

Watchdog

可以使用 watchdog 控制某些耗时操作的超时时间,当超时的时候执行指定的操作,例如中断线程、重置 Redis key 的超时时间等。Apache 的 commons-exec 包中提供了 watchdog 的实现供我们使用 (源码为文章后面的 Watchdog.java)。

According to wikipedia - watchdog is an electronic timer that is used to detect and recover from computer malfunctions.

案例演示

下面的例子执行拼接字符串的耗时操作,定义了一个 watchdog,超时时间为 1S,超时后 watchdog 中断线程结束字符串拼接操作。

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
import org.apache.commons.exec.Watchdog;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class Test {
private static final SimpleDateFormat formatter = new SimpleDateFormat("hh:mm:ss");

public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 创建 watchdog
Thread t = Thread.currentThread();
Watchdog watchdog = new Watchdog(TimeUnit.SECONDS.toMillis(1));

// 超时且未关闭 watchdog 时执行的操作
watchdog.addTimeoutObserver(w -> {
System.out.println("Watchdog trigger");
t.interrupt();
});

// 启动 watchdog
watchdog.start();

// 执行耗时任务,超时的时候当前线程的 isInterrupted() 为 true,结束循环
System.out.println("开始时间: " + formatter.format(new Date()));
int count = 0;
StringBuilder sb = new StringBuilder();
while (!t.isInterrupted() && count++ < 10000) {
sb.append(count);
}
System.out.printf("结束时间: %s,循环次数: %d", formatter.format(new Date()), count);

// 关闭 watchdog
watchdog.stop();
});

thread.start();
}
}

输出:

1
2
3
开始时间: 01:51:40
Watchdog trigger
结束时间: 01:51:41,循环次数: 48060741

Monterey 混合 VPN 访问公司内网

OSX Monterey 使用 VPN 中介绍了 Monterey 使用 L2TP-IpSpec VPN 的问题,并且使用 Windows Server 2016 进行中转处理的方案,虽然勉强能用,但是不够方便,本文中介绍了几种其他方案:

  • 方案一、Zerotier
  • 方案二、Wireguard
  • 方案三、Wireguard + Zerotier

由于各种原因最终使用了方案三,本地能够直接访问公司内网了,且速度能够接受,比使用 Windows Server 2016 的方案更好,但技术上也复杂不少。当有内网可使用 Wireguard 的机器时再切换为方案二,不考虑方案一是因为跨电信运营商时太慢。

方案一、Zerotier

Mac 终端打造

Mac 的终端打造主要综合使用 iTerm + Shuttle + Expect 这三个软件:

  • iTerm: 替代系统自带的 Terminal
  • Shuttle: 为快捷命令菜单,点击后在 iTerm 中执行相应的命令,可用于管理常用命令和管理 SSH 主机
  • Expect: masOS 自带了 Expect,可实现 SSH 自动登录

iTerm

iTerm 可匹配内容进行高亮显示、按下快捷键后窗口从屏幕上方滚动下来,请参考 iTerm 设置

如下图设置配置 iTerm 的主题,请参考 Mac Terminal Powerlevel

Java 执行命令

使用 Java 执行命令,可以:

下面以执行 ls -l / 为例演示相关代码。

ProcessBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void executeCommandUsingProcessBuilder() throws Exception {
ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/");
Process p = pb.start();

StringBuilder sbOk = new StringBuilder();
StringBuilder sbError = new StringBuilder();

// 读取正常输出
BufferedReader okReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String okLine = null;
while ((okLine = okReader.readLine()) != null) {
sbOk.append(okLine).append("\n");
}

// 读取错误输出
BufferedReader errorReader = new BufferedReader(new InputStreamReader(p.getErrorStream()));
String errorLine = null;
while ((errorLine = errorReader.readLine()) != null) {
sbError.append(errorLine).append("\n");
}

System.out.println(sbOk.toString());
System.out.println(sbError.toString());
}

疑问: 虽然如上能正常的获取到进程的正常和错误输出,慎用,因为缓冲区写满了的时候,由于没有读取其中的数据,无法继续写入数据,导致线程阻塞,对外现象就是进程无法停止,也不占资源,什么反应也没有,参考使用 JDK 写法

怎么理解不要用异常做控制流程

有很多文件介绍异常以及不要用异常做流程控制,如:

即使参考了很多文章,但对于什么是不要用异常做流程控制,什么时候抛出异常还是没有清晰的理解。并且可能会问:抛出异常就终止了函数的执行,这不就是控制了流程吗?

我们这里不去介绍异常的各种理论,而是从编码的角度来看,根据下面几条判断抛出异常的时机:

  • 一个函数的返回类型是 ReturnType
  • 在处理业务逻辑的时候,发现不满足条件要终止函数的执行,则有如下 2 种情况:
    • 类型 ReturnType 定义了无效值: 可以返回错误对应的无效值终止函数,让函数调用者明确的知道发生了什么错误
    • 类型 ReturnType 未定义无效值: 这个时候,为了终止函数,因为不能返回合适的无效值,那么就只能抛出异常终止函数,让函数调用者明确的知道发生了什么错误 (最好在异常中带上错误 code 和错误信息)
  • 函数的返回值要明确,不能有歧义
  • 为了终止函数的执行,没有合适的返回值时就抛出异常

下面以创建用户为例介绍什么时候应该抛出异常。

Hammerspoon 切换程序和窗口大小管理

Hammerspoon is a tool for powerful automation of macOS. At its core, Hammerspoon is just a bridge between the operating system and a Lua scripting engine. What gives Hammerspoon its power is a set of extensions that expose specific pieces of system functionality, to the user.

下面的脚本实现了:

  • 切换程序: 按下快捷键 Alt+键 就会切换到键对应的程序 (如果程序没有打开则打开,如果不是当前程序则激活为当前程序):
    • 按下 Alt+F 切换到 Finder
    • 按下 Alt+S 切换到 Safari
  • 窗口管理:
    • 窗口最大化: Alt+Ctrl+Return
    • 窗口左半屏: Alt+Ctrl+Cmd+Left
    • 窗口右半屏: Alt+Ctrl+Cmd+Right
    • 窗口居中: Alt+Ctrl+C
    • 窗口靠左: Alt+Ctrl+Left
    • 窗口靠右: Alt+Ctrl+Right
  • 多屏管理:
    • 在屏幕间移动光标: Alt+Z
    • 在屏幕间移动程序: Alt+X
  • 蓝牙管理:
    • 打开蓝牙: Alt+T
    • 关闭蓝牙: Alt+T
    • 下午 6 点后系统休眠时自动关闭蓝牙
  • 其他
    • 按下 Ctrl+H 隐藏或者显示桌面文件
    • 按下 Ctrl+D: 切换 Light 和 Dark 模式
    • 按下 Ctrl+Z: 前一个标签页
    • 按下 Ctrl+X: 后一个标签页
    • 按下 Alt+H: 方向左
    • 按下 Alt+L: 方向右
    • 按下 Alt+J: 方向下
    • 按下 Alt+K: 方向上
    • 按下 Alt+I: 删除

实现了 Thor 和 Rectangle 的功能,并且解决了 Thor 激活 Finder 的 Bug。

安卓中使用 Termux 提供 SSH 服务

Termux 是一个 Android 下一个高级的终端模拟器,开源且不需要 root,支持 apt(pkg) 管理软件包,安装软件包十分方便。

在 Termux 中运行 sshd,然后就可以从电脑上通过 ssh 操作安卓了:

  1. Termux 下载 https://apkpure.com/termux/com.termux 进行安装,它的 home 目录为 /data/data/com.termux/files/home
  2. 安装 OpenSSH: pkg install openssh
  3. 运行 SSH 服务器: sshd
  4. 设置密码: passwd
  5. 客户端访问: ssh android_ip -p 8022

建立互信:

  1. 电脑上微信把 id_rsa.pub 使用文件传输助手发给手机的微信
  2. 微信收到后点击 id_rsa.pub,然后点击右上角的三个点,保存到手机
  3. id_rsa.pub 会保存到手机的 Download/WeiXin 目录下
  4. 在 Download/WeiXin 目录下点击 id_rsa.pub,打开方式选择 Termux,会提示 Save file in ~/downloads/,点击 OPEN DIRECTORY,到终端里查看,id_rsa.pub 已经复制过去了
  5. 安卓的 Termux 中执行 cat ~/downloads/id_rsa.pub >> ~/.ssh/authorized_key 添加到互相文件
  6. 这样电脑通过 ssh 访问安卓时不需要再输入密码了

OSX Monterey 使用 VPN

在 Monterey 系统中,L2TP/IPSec 的 VPN 能够连接上 VPN 服务器,也能 ping 通内网 IP,但却不能访问网络,据说是苹果修改了 VPN 的加密方式导致的,可参考 VPN (L2TP over IPSec) stopped working after updating to Mac OS BigSur, does anyone have the same problem? 的讨论,在 Configuring L2TP VPN to use with iOS 14 and macOS Big Sur 中似乎给出了解决办法,但是需要修改 VPN 服务器端的配置,具体没有测试过,不知道行不行。

在 Monterey 中使用 L2TP/IPSec 的 VPN 的办法可以使用下面的这些方式:

  • 尝试使用上面的解决办法:
    • 需要修改 VPN 服务器 (不知道对其他系统的 VPN 客户端有无影响,且需要管理员去处理)
    • 没测试过,不一定能行
  • 服务器端开通 OpenVPN,Monterey 中使用 OpenVPN 进行连接
    • 需要修改 VPN 服务器
    • 需要公网 IP
  • 安装虚拟机 + Windows,在其中使用 VPN
    • 费钱 (虚拟机要钱,Windows 系统要钱)
    • 消耗系统资源
  • 购买一个便宜的 Windows Server 云主机 (3 年 200 块的),在其中使用 VPN
    • 费钱,但可以不多
    • 有点技术难度
  • 有 iPhone 的话可使用 iPhone 的 USB 网络共享
    • iPhone 开启 USB 网络共享
    • iPhone 连上无线
    • Mac 关闭无线
    • Mac 与 iPhone 用 USB 数据线连接起来
    • Mac 登录 L2TP 的 VPN

现在不少云服务器商提供了很多便宜的云主机活动,例如良心云目前的 2 核 4G 8M 网络的云主机 3 年才 222 块,于是买了一个安装 Windows Server 2016,下面介绍在 Windows Server 里面使用 VPN 遇到的问题解决办法 (定位到路由问题花了好几天)。

Safari 中去除 Google 搜索结果的重定向

以前去除 Google 搜索结果重定向 Safari 插件几乎都不能用了,现在可选的方案有:

  • 油猴插件加载去除Google 搜索结果重定向脚本 (油猴需要购买)
  • 使用 Userscripts 插件,加载自定义 JS 实现 (免费,但需要自己写代码)
  • 使用 Xcode 编写 Safari 插件

下面使用 Userscripts 插件实现去除 Google 搜索结果的重定向:

  1. 安装 Userscripts: 在 App Store 中搜索 Safari 插件 Userscripts 进行安装
  2. 在 Userscript 的 Open Extension Page 页面中点击 + > New Javascript 创建 JS 脚本
  3. 复制下面的 JS 到第 2 步创建的 JS 脚本中

打开 Google,点击搜索结果,不出意外目标链接能直接在新标签页中打开了,没有使用 Google 的重定向链接。

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
// ==UserScript==
// @name Remove Google Redirect
// @description 去除 Google 搜索结果的重定向
// @include *://www.google.com/*
// @include *://www.google.com.*/*
// ==/UserScript==

// 只有 Google 才生效 (在 @include 中配置)
// if (!window.location.href.includes('www.google.com')) {
// return;
// }

// 获取所有第三方链接
// 广告链接: div[data-text-ad] a
// 普通链接: .g a[rel="noopener"]
let as = document.querySelectorAll('div[data-text-ad] a, .g a[rel="noopener"]');

// 打开链接:
// 1. 遍历所有第三方链接
// 2. 保存 a 标签的 href 到 data-href 属性中,因为在 click 事件的时候 Google 会先把 href 处理为需要跳转的链接
// 3. 点击标签 a 时获取 data-href 得到链接的 url 在新标签页打开,并且阻止点击事件冒泡
for (let a of as) {
a.setAttribute('data-href', a.getAttribute('href'));
a.addEventListener('click', function (event) {
const url = a.getAttribute('data-href');
window.open(url, '_blank');

event.preventDefault();
return false;
});
}

Qt WebSocket

Qt 自带了 WebSocket 的客户端,使用起来也很方便,我们在其基础上进行了简单的封装,支持自动重连以及定时心跳发送。WebSocket 客户端的调用者只需要关注下面代码中的几个方法和信号就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MainWidget::MainWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MainWidget) {
ui->setupUi(this);

// 提示: 创建 Websocket 连接,服务器的 IP 根据实际情况填写,端口为 9321
wsClient = new WsClient("127.0.0.1:9321", "gw-1", "gw-1");

// 连接成功或者连接断开
connect(wsClient, &WsClient::connected, [this](bool yes) {
this->ui->stateLabel->setText(yes ? "连接成功" : "连接断开");
});

// 收到服务器发来的消息
connect(wsClient, &WsClient::messageReceived, [this](const QString &message) {
this->ui->responseLabel->setText(message);
});

// 点击按钮发送消息
connect(ui->pushButton, &QPushButton::clicked, [this] {
this->wsClient->sendMessage("ECHO", this->ui->lineEdit->text());
});

// 连接到服务器
wsClient->connectToServer();
}