Mac 关闭内置显示器 不指定

// 日常吐槽莎币苹果。

MacOS 有很多很蠢的地方,比如没有快捷键可以让窗口在屏幕间移动;比如不能右键直接创建空文件;比如右键菜单有「复制」和「拷贝」两个选项;比如会莫名其妙出现左右声道不平衡的 bug 并且长期未修复;

再比如,不能在外接显示屏的情况下关闭内屏。

当然也不是完全不能,网友也有一些解决方案:

1. 合上屏幕当主机用,自然就息屏了。不过我需要用 mac 的键盘、触摸板和 TouchID 解锁。过。

2. 随身携带磁铁,欺骗 macOS 屏幕已经关闭。这可能是种行为艺术,可惜我不是艺术家。过。

3. SwitchResX,试了下,确实可以把内屏关闭,只不过拔了外显内屏还是关着的,折腾了几下甚至 mac 都不能识别外显了,重启才解决。卸。

4. 在「系统设置 -> 显示」里,将背光拉到 0;不能直接用 Fn 调节屏幕亮度,因为会把外接屏亮度一起调低(莎币值+1);而且每次接上外显或 wakeup 时,都要重新设置。过……吧?

好在作为一名码农,可以想一些自动化的方式来把方案 4 利用起来。

首先是用 brew install brightness (update: M3 Macbook can not use this. try Lunar, details below),可以用 `brightness -d 1 0` 把 display 1 的亮度调到 0 (注意把主屏设置为外接屏,且把内屏设置为拷贝外屏,这样内屏的编号就是 1,外显是 0)。

其次是有一个叫 hammerspoon 的软件(https://www.hammerspoon.org/),可以在 unlock 的时候自动执行一个 lua 脚本。

把下面这个 lua 脚本存为 ~/.hammerspoon/init.lua

-- init.lua 
    local log = hs.logger.new("", "info")

    local function ok2str(ok)
        if ok then return "ok" else return "fail" end
    end

    hs.caffeinate.watcher.new(function(event)
        local eventName = hs.caffeinate.watcher[event]
        log.f("got caffeinate event:%s (id:%d)", eventName, event)
   
        local script

        if event == hs.caffeinate.watcher.screensDidLock then     
            script = "${HOME}/.lock"
        elseif event == hs.caffeinate.watcher.screensDidUnlock then
            script = "${HOME}/.unlock"
        else       
            log.f("ignored event:%s (id:%d)", eventName, event)
            return
        end
   
        local ok, st, n = os.execute(script)
        log.f("exec:%s -> %s, %s, %d", script, ok2str(ok), st, n)
    end
    ):start()



然后再 touch ~/.unlock && chmod +x ~/.unlock,内容为:

#!/bin/bash

export PATH=$PATH:/usr/local/bin/

brightness -d 1 0
sleep 5 && brightness -d 1 0


sleep 5 是因为从睡眠状态恢复时,识别外屏会比较慢,可按需调整 sleep 时长。


如果你看到这里,说明你也需要注意日常用电脑的姿势了,避免头颈前倾。

(完)。

update @ 2023-12-12

实际体验下来还有一些问题,例如去倒杯水,锁屏但没有睡眠,解锁后就不会息屏。

最好用的方案往往只需要最简单的工具:crontab。

引用

MAILTO=""

* * * * * /Users/bytedance/bin/TurnOffScreen.sh
* * * * * sleep 15 && /Users/bytedance/bin/TurnOffScreen.sh
* * * * * sleep 30 && /Users/bytedance/bin/TurnOffScreen.sh
* * * * * sleep 45 && /Users/bytedance/bin/TurnOffScreen.sh



update @ 2024-01-26

brightness seems to be not working on M3 Macbooks, so I switched to lunar:

1. Download and install lunar
2. Run in terminal: /Applications/Lunar.app/Contents/MacOS/Lunar install-cli
3. Update the TurnOffScreen.sh to be (remember to replace with your monitor model or serial id in the pattern):
#!/bin/bash

pattern='S2721QS'

if ~/.local/bin/lunar displays | grep $pattern; then
    ~/.local/bin/lunar displays "Built-in" brightness 0
fi

一个奇怪(而又危险)的想法 不指定

2009年在 B 厂实习的时候,人手发一个硬件 token,它有个屏幕,实时显示 6 位 TOTP ,用于登录 relay 机器,再通过 relay 机器登录生产环境的机器。

由于 relay 机器禁用了端口转发等各种 fancy 的能力,所以除了 shell 上的输入和输出外无法实现额外的通信。

好在 SecureCRT 默认支持 zmodem 协议,大家都用 sz/rz 来上传/下载文件,也还算方便。

sz 是基于 zmodem 协议实现的,这个功能在早期的 BBS 上非常实用。

简单说就是,通过发送一个特殊的控制串,告诉 terminal 它想要发送一个文件,支持 zmodem 协议的 terminal 会将后续发送的数据保存到本地文件中。

当然具体的协议细节比较复杂,不是本文重点,就不展开了,详情可参考知乎这位老哥的整理 https://zhuanlan.zhihu.com/p/579720546

继续发散,我们可以想象一下,如果我们实现 2 个程序 A、B:

1. A 通过 ssh 连接到目标机器,打开 B
2. 用 B 的 stdin/stdout 来「模拟」一个 tcp 连接
3. 在这个「tcp 连接」上用 xtaci/smux 实现多路复用
4. 在 A 实现 socks5 的前端(监听),在 B 实现 socks5 的后端(转发)

这样我们就实现了一个穿透了 relay 的代理。

领导:明天去财务处结一下工资,后天不用来了。

Set goland to use gofumpt on file save 不指定

Why: kitex framework is using gofumpt as linter in gitlab CI, but goland defaults to use gofmt, which enforces weaker rules.

Procedure:

0. $ go install mvdan.cc/gofumpt@v0.2.0  # or any version as you need

1. Install file watcher plugin in goland settings;

2. Go to settings -> Tools -> File Watchers:

2.1. Click on "+", and choose "go fmt"

2.2. Replace "Program:" to be the full path of your gofumpt binary

2.3. Modify "Arguments:" to be "-w $FilePath$"

2.4 OK & OK again.

Go ASM:实现一个阶乘函数 不指定

先实现一个纯 Go 的版本:

// main.go
package main

import "fmt"

func fac(n uint) uint {
  var result uint = 1
  for n > 0 {
    result = result * n
    n -= 1
  }
  return result
}

func main() {
  fmt.Println(fac(10))
}


修改上述 fac 方法,只保留函数定义(即声明存在该函数,由汇编代码实现):
func fac(n uint) uint


新增 fac.s
TEXT ·fac(SB), $0-8
    MOVQ n+0(FP), CX
    MOVQ $1, DX
LOOP:
    IMULQ CX, DX
    DECQ CX
    JNZ LOOP
    MOVQ DX, result+8(FP)
    RET


编译运行:
引用

$ go run .
3628800


说明:

1. TEXT ·fac(SB), $0-8
- TEXT 表示这个方法在 TEXT 段中
- · 是Unicode的「中点」(中文输入法,1左边的按键),前面省略了包名,表示这是 main 包的 fac 函数
- SB 是 stack base pointer,Go ASM 中的「伪寄存器」(不是硬件寄存器),大致等同于程序的起始地址
- $0-8:0 表示这个函数没有局部变量,8 表示返回值占用8个字节

2. MOVQ n+0(FP), CX
- MOVQ 的 Q 表示 8 个字节
- n+0(FP) 表示变量 n 在 FP(Frame Pointer,伪寄存器,表示这个函数的栈帧起始位置) + 0 的位置(即第一个参数)。注意这个写法形式上是必须得,但是变量名n没有实际意义,只是用来注记。
- CX 即 x86/x86_64 的 CX(16bit),ECX(32bit),RCX(64bit) 寄存器,具体多长取决于前面的指令(MOVQ是64bit)
- 这句的意思是把第一个参数的值写入 RCX

3. MOVQ $1, DX
- $1:$开头的是立即数
- 这句的意思是给 RDX 赋值为 1

4. IMULQ CX, DX
- DX = DX * CX

5. DECQ CX
- CX = CX - 1

6. JNZ LOOP
- JNZ: Jump if Not Zero
- 当 CX 不等于 0 时跳转到 LOOP

7. MOVQ DX, result+8(FP)
- 将 RDX 的值写入到 FP+8 的位置。

8. RET
- 返回到调用方。


p.s. 这个汇编版本的实现并不等同于原来 Go 版本,只是这样写会更简单(只要一个jump)。

参考:
- Golang ASM 简明教程:https://jiajunhuang.com/articles/2020_04_22-go_asm.md.html
- A Quick Guide to Go's Assembler:https://go.dev/doc/asm

xdelta: 二进制文件patch 不指定

手头项目每次 mvn package 得到的 jar 是 160M 左右,有时候需要替换到服务器上,上传时间较长。

Google 搜到这么个项目:xdelta

https://github.com/jmacd/xdelta-gpl/releases

可以对二进制文件做 patch,对 jar 的效果还挺好,两个相近的版本做 diff,生成的 patch 文件只有 500KB 左右。


用法:
引用
# 生成 patch
xdelta.exe -es v1.jar v2.jar v1-v2.patch

# 应用 patch
xdelta.exe -ds v1.jar v1-v2.patch v2.jar


有个小问题是,服务器是 centos 7 ,yum install 的是 xdelta 3.0.7 不支持最新的 lzma 压缩,因此生成 patch 的时候需要加上 -S djw 参数,指定为 djw 编码:

引用
xdelta.exe -S djw -es v1.jar v2.jar v1-v2.patch

Swagger API: 上传文件 不指定

踩了个小坑,记录一下。

swagger api 定义:
引用

/upload:
  post:
    tags:
      - "tag"
    summary: "summary"
    operationId: uploadFile
    consumes:
      - multipart/form-data
    parameters:
      - name: "data"
        in: "formData"
        type: "file"
        required: true
        description: "file content"
    responses:
      200:
        description: "success"
        schema:
          $ref: "#/definitions/UploadFileResponse"


生成的 API:
    @RequestMapping(value = {"/sf/express/upload_channel_file" }, produces = { "application/json" }, consumes = { "multipart/form-data" }, method = RequestMethod.POST)
    default ResponseEntity<UploadFileResponse> uploadFile(@ApiParam(value = "file detail") @Valid @RequestPart("file") MultipartFile data) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
    }


直接用这个 API 请求会报错:
swagger Required request part 'file' is not present

细看发现是 swagger 生成的是 @RequestPart("file") 而不是 @RequestPart("data"),需要手动修改过来才能正确读取到文件字段。

Java: NPE和单测覆盖率 不指定

切到新语言就是不断地踩坑呀。

手头有一个函数,输入一个 itemList,统计不同类型的个数,核心代码如下:

int xCount = 0, yCount = 0;
switch(typeParser.parse(item)) {
  case X:
    xCount++;
    break;
  case Y:
    yCount++;
    break;
  default:
    log.error("unknown type");
    break;
}


其他 typeParser 在遇到异常数据时会返回 null。

重构的时候,稳妥起见为它写了个单测,结果就在上述代码的第二行 NPE 了。

debug 时确认了 typeParser 和 item 都不是 null,猜测是 switch 不能处理 null ,但不熟悉 java 语法,搜了一下,果然如此。

由于 case 数量不多,就暂且改成了 if ... else if ... else 的结构。

由此可见,单测对于代码的覆盖率确实是很有帮助。

解决 Intellij IDEA 不识别某些 class 的问题 不指定

又遇到一个灵异的 intellij idea 问题。

背景:在项目 P 的同一个 Module M 下面, PKG P1 需要 import PKG P2 下面的 C1 和 C2 两个 class。

现象:import C1 成功,但是import C2失败;然而通过 mvn clean install 是可以正常完成编译的。

点击在新窗口中浏览此图片

删除 project 下的 .idea 目录重新 import 并没有解决这个问题。

在另一个同学的电脑上尝试,报的错竟然不一样,是在 C2 这个 class 下无法 import 另一个 class C3,但同样可以通过 mvn 命令行完成编译。

通过 Google 搜到 StackOverflow 的这个 thread:

  https://stackoverflow.com/a/66167190/802910

解决方案很简单:删掉 idea 的cache 目录,让它重建就好了。

在 windows 下,这个目录位于 %LOCALAPPDATA%\JetBrains\IdeaIC2021.2\caches (注意替换为自己版本号)

在 mac 下,目录应该是 ~/Library/Caches/JetBrains/IdeaIC2021.2

似乎 IDEA 本身也有清空 cache 的功能(File -> Invalidate Caches...),下次遇到再验证一下。