Content Table

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
  • 多屏管理:
    • 在屏幕间移动光标: Ctrl+Z
    • 在屏幕间移动程序: Ctrl+X
  • 蓝牙管理:
    • 打开蓝牙: Alt+T
    • 关闭蓝牙: Alt+T
    • 下午 6 点后系统休眠时自动关闭蓝牙
  • 其他
    • 按下 Ctrl+H 隐藏或者显示桌面文件
    • 按下 Ctrl+D: 切换 Light 和 Dark 模式
    • 按下 Alt+Z: 前一个标签页
    • 按下 Alt+X: 后一个标签页
    • 按下 CMD+H: 方向左
    • 按下 CMD+L: 方向右
    • 按下 CMD+J: 方向下
    • 按下 CMD+K: 方向上
    • 按下 CMD+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();
}

Ajax 异步上传文件

异步上传

先选择文件,然后点击上传按钮使用 Ajax 异步的方式上传文件,代码如下:

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
<template>
<div class="about">
<input ref="fileElem" type="file" accept="image/jpeg"><br><br>
<Button type="primary" :loading="loading" @click="upload">上传文件</Button>
</div>
</template>

<script>
export default {
data() {
return {
file: null,
loading: false,
};
},
mounted() {
// 选择文件的事件处理
this.$refs.fileElem.addEventListener('change', () => {
this.file = this.$refs.fileElem.files[0];
});
},
methods: {
upload() {
this.loading = true;
var formData = new FormData();
formData.append('file', this.file, this.file.name);

uploadFile('/form/upload/temp/file', formData).then(response => {
console.log(response);
this.loading = false;
}).catch(error => {
console.error(error);
this.loading = false;
});
}
},
};

/**
* 上传文件
*
* @param {String} url 上传地址
* @param {JSON} formData 表单数据
*/
function uploadFile(url, formData) {
return new Promise((resolve, reject) => {
axios.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(response => {
// 提示: 响应部分根据使用的服务器端的响应结果进行简单调整
if (response.data.success) {
const uplaodedFile = response.data.data;
resolve(uplaodedFile);
} else {
reject(response.data.message);
}
}).catch(response => {
const error = response.response;
console.error(error);
reject(error);
});
});
}
</script>

自定义 Content Type Prober

在浏览器中点击 PDF 文件的链接:

  • 在 A 网站点击 a.pdf,浏览器自动下载 a.pdf
  • 在 B 网站点击 b.pdf,浏览器直接打开 b.pdf

被访问都是 PDF 文件,为啥在网站 A 和在网站 B 访问时,浏览器的行为不一样,是什么东西影响它在浏览器中的行为呢?答案就是浏览器会根据响应的 Content-Type 来决定下载还是打开它们 (当然 Content-Type 的值只是一个 hit,具体的操作还是要看浏览器的实现)。

文件的类型非常多,怎么获取文件的 Content-Type 是什么呢?Java 1.7 提供 java.nio.file.Files.probeContentType(Path path) 用于尝试获取文件的 Content-Type,但发现支持的文件类型不够全面,查看方法 probeContentType 的帮助文档:

This method uses the installed FileTypeDetector implementations to probe the given file to determine its content type. Each file type detector’s probeContentType is invoked, in turn, to probe the file type. If the file is recognized then the content type is returned. If the file is not recognized by any of the installed file type detectors then a system-default file type detector is invoked to guess the content type.

A given invocation of the Java virtual machine maintains a system-wide list of file type detectors. Installed file type detectors are loaded using the service-provider loading facility defined by the ServiceLoader class. Installed file type detectors are loaded using the system class loader. If the system class loader cannot be found then the extension class loader is used; If the extension class loader cannot be found then the bootstrap class loader is used. File type detectors are typically installed by placing them in a JAR file on the application class path or in the extension directory, the JAR file contains a provider-configuration file named java.nio.file.spi.FileTypeDetector in the resource directory META-INF/services, and the file lists one or more fully-qualified names of concrete subclass of FileTypeDetector that have a zero argument constructor. If the process of locating or instantiating the installed file type detectors fails then an unspecified error is thrown. The ordering that installed providers are located is implementation specific.

Java Classpath 加载 jar 的顺序

以在 Test.java 中加载 app-1.jar 和 app-2.jar 中的类 com.xtuer.Aloha 为例演示 classpath 中 jar 被加载的顺序。

app-1.jar 和 app-2.jar

创建项目,里面只有 1 个类 com.xtuer.Aloha,分别打包成 2 个版本的 jar 包:

  • app-1.jar: 返回 Aloha-1
  • app-2.jar: 返回 Aloha-2
1
2
3
4
5
6
7
8
9
package com.xtuer;

public class Aloha {
@Override
public String toString() {
return "Aloha-1"; // app-1.jar 使用
// return "Aloha-2"; // app-2.jar 使用
}
}

项目的目录结构 (此 gradle 管理的 Java 项目结构仅作为参考):

1
2
3
4
5
6
7
8
app
├── build.gradle
└── src
└── main
└── java
└── com
└── xtuer
└── Aloha.java

Spring Boot 使用 loader.path 加载其他 jar

Classpath

可以使用 classpath 指定类加载的路径,但 classpath 的生效是有条件的:

命令 classpath 生效 说明
java -cp .;lib/x.jar Test 运行 class
java -cp lib/x.jar -jar app.jar 运行 jar

Loader.path

Spring Boot 程序大多是打成 jar 包,使用 java -jar boot.jar 的方式启动 (此时 -cp 无效),可以使用 loader.path 指定类加载路径加载其他 jar,但 loader.path 生效是有条件的:

命令 MANIFEST.MF 的 Main-Class loader.path 生效 打包配置
java -Dloader.path=./lib -jar app.jar JarLauncher 默认配置
java -Dloader.path=./lib -jar app.jar PropertiesLauncher 额外配置

loader.path 实现了 classpath 的功能。

配置 Main-Class

为了使用 loader.path,需要把 jar 包的 Main-Class 配置为 PropertiesLauncher,在 build.gradle 中如下配置,可参考 Using the PropertiesLauncher:

1
2
3
4
5
bootJar {
manifest {
attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher'
}
}

LaunchedURLClassLoader

无论启动类是 JarLauncher 或者 PropertiesLauncher,loader.path 引入的 jar 和 Spring Boot 中 lib/*.jar 都是使用类加载器 org.springframework.boot.loader.LaunchedURLClassLoader 进行加载,也即是说他们使用的是同一个类加载器。

注意到同一个程序,打包成不同类型时,PropertiesLauncher (20s) 比 JarLauncher (8s) 启动慢很多。