如何优雅地寻找 Tesla ?

近期,Tesla APP 发布了 4.24.0 版本,在 iOS 的“快捷指令”中可以直接调用部分 Tesla 命令了!借助快捷指令,你能很优雅地找到你的 Tesla ! 有时候我们将车停在了地库中的一个不常去的位置,用车时往往就找不到车。之前我经常使用 Tesla APP 上的“鸣笛”功能来加速自己找车的速度,而在近期,Tesla APP 发布了 4.24.0 版本,在 iOS 的“快捷指令”中可以直接调用部分 Tesla 命令了!这是一个非常令人兴奋的功能!因为这意味着,我们完全可以设定一个 iOS 快捷指令,直接呼唤 Siri 来寻找我们的 Tesla 。 安装「我的 Tesla 在哪里?」快捷指令 使用方法: 对 iPhone 说出「嘿 Siri ,我的 Tesla 在哪里?」来触发快捷指令,运行后根据提示操作即可。 Q: 为什么快捷指令安装后无法使用? A: 请确保 iPhone 上的 Tesla APP 版本在 4.24.0 及以上。 Q: 可以在 Android 设备上使用吗? A: 此快捷指令依赖 iOS,无法在 Android 设备上使用。

2023/8/29
articleCard.readMore

Mastering Git Cherry-pick

本文希望教你如何成为一个 git cherry-pick 的 “master”!通过使用 git cherry-pick 来轻松地维护多个分支版本,再也不会让 multi-version maintaining 成为你心头上的那把令你屡次痛心的剑了! 本文所有内容都会基于以下(精心构造的)示例,该例子涵盖了大部分工程上容易出现的 Git log pattern(如有其他 corner-case ,欢迎联系我,一起努力让该文变得对大家更有帮助)。 贯穿本文的示例场景,这是该示例基于时间序的 Git 提交历史。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $ git --no-pager log --oneline --graph --date-order * f2c1619 (HEAD -> red) R6 * e6899ea R5 merge branch 'blue' into 'red' |\ * \ 0979d45 R4 merge branch 'green' into 'red' |\ \ | | * 186da41 (blue) B3 | * | c950910 (green) G3 * | | 17e2629 R3 | | * 69edfc9 B2 | * | 059425a G2 | * | 05719c8 G1 | | * ebb218d B1 | |/ * / 8c6595b R2 |/ * 6581ff8 R1 * 2787f8f (master) init commit 快速创建该示例。 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 mkdir cherry-pick; cd cherry-pick/ git init echo "init" >> init; git add -A; git commit -m "init commit"; sleep 1 git checkout -b red echo "red" >> red; git add -A; git commit -m "R1"; sleep 1 git branch green git branch blue echo "red" >> red; git add -A; git commit -m "R2"; sleep 1 git checkout blue echo "blue" >> blue; git add -A; git commit -m "B1"; sleep 1 git checkout green echo "green" >> green; git add -A; git commit -m "G1"; sleep 1 echo "green" >> green; git add -A; git commit -m "G2"; sleep 1 git checkout blue echo "blue" >> blue; git add -A; git commit -m "B2"; sleep 1 git checkout red echo "red" >> red; git add -A; git commit -m "R3"; sleep 1 git checkout green echo "green" >> green; git add -A; git commit -m "G3"; sleep 1 git checkout blue echo "blue" >> blue; git add -A; git commit -m "B3"; sleep 1 git checkout red git merge green -m "R4 merge branch 'green' into 'red'"; sleep 1 git merge blue -m "R5 merge branch 'blue' into 'red'"; sleep 1 echo "red" >> red; git add -A; git commit -m "R6"; sleep 1 git --no-pager log --oneline --graph --date-order 当前 Git 提交历史示意图如下。 提交记录示意图 Git cherry-pick 的命令的基本原理是根据用户所选择的提交,根据提交中的差异信息(diff)将这些提交移植至用户目标版本中。如将 hotfix 应用至其他 LTS 版本中是该功能的一个典型应用。 git cherry-pick 的大致用法为: 1 git cherry-pick [options] <commit>... 此处的 <commit>... 即为用户希望移植的提交(集合),这是本文讨论的要点。 <commit> 可以为单一提交(commit),也可以为一个版本区间(revision range)。若为 revision range,则该命令会将该 revision range 中的所有 commit 都解析出来,最终成为一连串的单一 commit 1。cherry-pick 可以同时接受多个 <commit> ,此时表现类似于 git rev-list 中的 --no-walk 行为2。 那我们依次来讨论 <commit>... 为单一 commit 以及 revision range 的情况。 Single commit Normal commit 回到上文的例子,如果仅需要将 G2 选取出来,我们可以这样操作。 1 2 3 4 5 # 回到 master 新建一条分支用于测试 $ git checkout master $ git checkout -b cp-single-normal-commit # G2 的提交 SHA 值为 059425a $ git cherry-pick 059425a 此时会出现合并冲突(merge conflict),输出如下所示。 1 2 3 4 5 CONFLICT (modify/delete): green deleted in HEAD and modified in 059425a (G2). Version 059425a (G2) of green left in tree. error: could not apply 059425a... G2 hint: after resolving the conflicts, mark the corrected paths hint: with 'git add <paths>' or 'git rm <paths>' hint: and commit the result with 'git commit' 这段内容告知我们这些信息:green 文件在当前(暂存)版本 HEAD 中并不存在,但在选取的 G2 提交中存在。如果需要该文件,则使用 git add 将其提交至暂存区,若希望保留当前暂存版本的状态,即删除该文件,则使用 git rm 将 green 文件舍弃。 我们希望在选取 G2 之后能够保留 green 文件,故采取如下操作。 1 2 3 4 # 将 green 提交至暂存区 $ git add green # 已修复所有合并冲突,继续进行 cherry-pick $ git cherry-pick --continue 此时 cherry-pick 操作已经完成,如果继续执行 git cherry-pick --continue ,则此时会显示 error: no cherry-pick or revert in progress ,即当前没有进行任何 cherry-pick 任务。 查看一下当前的提交记录,则会发现 G2 已经在我们当前的分支 cp-single-normal-commit 上了。 1 2 3 $ git --no-pager log --oneline --graph --date-order * 0457362 (HEAD -> cp-single-normal-commit) G2 * 2787f8f (master) init commit Merge commit 那如果我们想选取一个 merge commit 呢,比如将 R4 选取出来。 1 2 3 4 5 # 回到 master 新建一条分支用于测试 $ git checkout master $ git checkout -b cp-single-merge-commit # R4 的提交 SHA 值为 0979d45 $ git cherry-pick 0979d45 当执行完这条 cherry-pick 命令之后,你会得到以下输出。 1 2 error: commit 0979d45f1b46f72730188c5c01b3f2c7f41b18e6 is a merge but no -m option was given. fatal: cherry-pick failed 默认情况下,cherry-pick 不处理 merge commit 并直接报错。因为在 merge commit 中,会有多个 parent 信息,但此时 Git 并不知道该使用哪个 parent 作为 mainline。在错误信息中,也同时提示了我们,如果要选取 merge commit ,则需要使用 -m (亦为 --mainline)选项来指定哪个 parent 是主线3。 通过 git show 命令可以获得 merge commit 的多个 parent,且从 1 开始编号。由于该例中我们需要选取的 mainline parent 是 R3(17e2629) ,因此在 cherry-pick 中选择的是 -m 1。 1 2 3 4 5 6 7 $ git --no-pager show 0979d45 commit 0979d45f1b46f72730188c5c01b3f2c7f41b18e6 Merge: 17e2629 c950910 Author: Triple-Z <me@triplez.cn> Date: Thu Mar 31 01:29:31 2022 +0800 R4 merge branch 'green' into 'red' 让我们再来试一次。 1 $ git cherry-pick -m 1 0979d45 cherry-pick 圆满完成!此时再看一下当前的提交记录,则发现在 cp-single-merge-commit 分支上产生了一个新的 R4 提交。 1 2 3 $ git --no-pager log --oneline --graph --date-order * 987aba7 (HEAD -> cp-single-merge-commit) R4 merge branch 'green' into 'red' * 2787f8f (master) init commit 现在我们再来讲讲刚刚的 -m 1 发生了什么。如果现在去看 cp-single-merge-commit 这个测试分支上的文件,则会发现有 green ,而没有 red 。 1 2 3 4 $ ls -lh total 16 -rw-r--r-- 1 triplez staff 18B 4 7 19:06 green -rw-r--r-- 1 triplez staff 5B 3 31 01:29 init 这是因为我们在选取 merge commit 时,使用的是 mainline 1 ,即 red 分支。因此 cherry-pick 事实是以 red 为基础,寻找 mainline 2 green 分支与 red 的差异,选取的就是 green 分支上所做的修改了。 Revision range Git 中可用多种方法来表示 revision (版本,或修订快照)4,这里我们主要讨论 revision range(版本区间)5。 对于 revision range,有以下六种表示法: ^<rev> :(脱字符-表示法)表示排除 <rev> 以及它所有可到达的父辈 commit。 <r1>..<r2>(两点-范围表示法):等同于 ^r1 r2 ,即 包含 <r2> 以及其可到达的父辈 commit ,并排除 <r1> 以及其可到达的父辈 commit。 如果需要包括 <r1>,可使用这种写法:<r1>^..<r2> 。 <r1>...<r2> (三点-对称差分表示法):包含所有 <r1> 或 <r2> 及其可到达的父辈 commit,并排除 <r1> 和 <r2> 两者可到达的共同父辈 commit。 <rev>^@ :包含 <rev> 的所有父辈,但排除 <rev> 本身。 <rev>^! :包含 <rev> 本身,但排除 <rev> 所有父辈。即表示单个 <rev> commit。 注意: <rev> (表示 <rev> 及其所有父辈)在 revision range 的语境中不同于 <rev>^! 。仅有指定 --no-walk 参数时,两者才可以认为是相同的(都仅表示 <rev> 本身)。 <rev>^-[<n>] :包含 <rev> 及其所有父辈,但排除 <rev> 的第 <n> 个 parent 及其可到达的所有父辈。 <n> 的缺省值为 1。 看起来很复杂,我们来用文中的场景来举两个范围表示法的例子。 首先,考虑 <r1> 和 <r2> 都在同一分支上的情况,如 G1 (05719c8) 和 G3 (c950910)。 1 2 3 4 5 # 回到 master 新建一条分支用于测试 $ git checkout master $ git checkout -b cp-range-same-branch # G1 的提交 SHA 值为 05719c8,G3 的 SHA 值为 c950910 $ git cherry-pick 05719c8^..c950910 05719c8(G1)^..c950910(G3) 的含义应当是: 包含 G3 及其所有父辈。 并排除 G1 的所有父辈(不排除 G1)。 因此结果应当是选出从 G1 到 G3 的所有提交,示意图如下,黄色为被包含的节点,灰色则代表被排除的节点。 git cheery-pick G1^..G3 让我们再看看当前的提交记录。Bingo! G1 ,G2 ,G3 这三个提交已经被选取出来了。 1 2 3 4 5 $ git --no-pager log --oneline --graph --date-order * 32eac39 (HEAD -> cp-range-same-branch) G3 * d3b1130 G2 * c82c4c7 G1 * 2787f8f (master) init commit 那 <r1> 和 <r2> 在不同分支上,是什么情况呢? 我们以 G1 (05719c8) 和 B2 (69edfc9) 作为用例。 1 2 3 4 5 # 回到 master 新建一条分支用于测试 $ git checkout master $ git checkout -b cp-range-diff-branch # G1 的提交 SHA 值为 05719c8,B2 的 SHA 值为 69edfc9 $ git cherry-pick 05719c8^..69edfc9 05719c8(G1)^..69edfc9(B2) 的含义应当是: 包含 B2 及其所有父辈。 并排除 G1 的所有父辈(不排除 G1)。 由于 B2 及其所有父辈中,并不包括 G1。因此我们可以将 G1^..B2 理解为包含 B2 及其所有父辈,且排除 B2 和 G1 的共同父辈后的结果。自然就只剩下 B1 和 B2 两个 commit 了。示意图如下,黄色为被包含的节点,灰色则代表被排除的节点。 git cherry-pick G1^..B2 让我们再看看当前的提交记录,确实是只选择了 B1 和 B2 两个提交。 1 2 3 4 $ git --no-pager log --oneline --graph --date-order * e63f214 (HEAD -> cp-range-diff-branch) B2 * aed6717 B1 * 2787f8f (master) init commit Rerere Rerere 是“重用已记录的冲突解决方案(reuse recorded resolution)”,它是一种简化冲突解决的方法6 7。 如果你经常进行大量的 merge, rebase 或 cherry-pick,或在维护一个长期不同于主干的分支8,那么非常建议开启 rerere 功能。 开启 rerere 非常简单,仅需要进行一次全局配置即可。 1 $ git config --global rerere.enabled true 在本地仓库中直接创建 .git/rr-cache 文件夹,也可以为该仓库开启 rerere。 What’s next 在笔者撰写该文的过程中,也看到了 Microsoft 的 Raymond Chen 写的 Stop cherry-picking, start merging 系列文章,他在其中提及了许多工程实践中 cherry-pick 可能导致的 pitfall。接下来的时间里,笔者将会逐一阅读该系列文章,并根据文中案例去分析 cherry-pick 是否能够在常用软件开发工作流给我们带来足够的收益,以及,是否应该 stop cherry-picking, start merging。 只有在深入了解工具后,我们才能更好地运用工具,真正实现效率提升。 git-cherry-pick <commit>… https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt-ltcommitgt82308203 ↩︎ git-rev-list –no-walk https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---no-walksortedunsorted ↩︎ git-cherry-pick -m, –mainline https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--mltparent-numbergt ↩︎ gitrevisions: Specifying Revisions https://git-scm.com/docs/gitrevisions/#_specifying_revisions ↩︎ gitrevisions: Specifying Ranges https://git-scm.com/docs/gitrevisions/#_specifying_ranges ↩︎ Pro Git (zh): Git 工具 - Rerere https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-Rerere#ef_rerere ↩︎ Pro Git (en): Git Tools - Rerere https://git-scm.com/book/en/v2/Git-Tools-Rerere ↩︎ Pro Git (zh): 分布式 Git - 维护项目 - Rerere https://git-scm.com/book/zh/v2/%E5%88%86%E5%B8%83%E5%BC%8F-Git-%E7%BB%B4%E6%8A%A4%E9%A1%B9%E7%9B%AE#_rerere ↩︎

2022/4/8
articleCard.readMore

通过 systemd 服务配置链接动态库

序 在设置动态链接的方法中,rpath 有其自身的问题1 , ld.so.conf 为 OS 全局配置,可能会因为单一服务的动态库版本而影响到其他服务,不是一个优雅的方法。 那如何才能不借助 rpath 和 ld.so.conf ,即能使目标服务找到对应的动态链接库,又能不影响其他服务呢? LD_LIBRARY_PATH 对于动态链接库的路径配置而言,除了 rpath 和 ld.so.conf ,还有 LD_LIBRARY_PATH。只要在二进制启动的环境中设置 LD_LIBRARY_PATH 变量,则 glibc 会将该变量中的路径配置作为动态链接库的查找路径之一,使得二进制可执行文件能够正常链接到其依赖的动态链接库。 LD_LIBRARY_PATH 会在当前 shell session 中“全局”生效,不过,由于笔者服务采用 systemd 来管理其生命周期,按照上述思路,应该只需要在服务启动前,将 LD_LIBRARY_PATH 注入启动环境即可,这样还能够借助 systemd 来实现一定程度的环境“隔离”。systemd 服务配置中的 Environment 字段是用于描述启动环境的环境变量的,我们先在这里加入 LD_LIBRARY_PATH (如下),重新加载 systemd 配置并重启服务,观察效果。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [Unit] Description=Nginx After=syslog.target network.target remote-fs.target nss-lookup.target [Service] User=triplez Group=triplez Type=forking +Environment="LD_LIBRARY_PATH=/your/path/to/openssl/lib:/your/path/to/jemalloc/lib:/your/path/to/luajit/lib" PIDFile=/run/nginx/nginx.pid RuntimeDirectory=nginx RuntimeDirectoryMode=0755 ExecStartPre=/your/path/to/nginx -t ExecStart=/your/path/to/nginx ExecReload=/bin/kill -s HUP $MAINPID ExecStop=/bin/kill -s QUIT $MAINPID PrivateTmp=true Restart=always [Install] WantedBy=multi-user.target 1 2 $ sudo systemctl daemon-reload $ sudo systemctl restart nginx.service 事实上,对于大多数服务,只需要加入环境变量即可解决问题。但很不幸的是,由于我们的服务需要监听 1024 及以下端口(如 80、443),笔者对二进制可执行文件进行了 setcap 操作,赋予其监听低位端口的能力。 1 2 $ sudo getcap /your/path/to/nginx /your/path/to/nginx = cap_net_bind_service+ep Capability 由于该文件含有 capability,glibc 会将 LD_LIBRARY_PATH 忽略2 3,导致服务还是无法正确链接到动态库上。 了解原因之后,接下来要做的事情也很清晰了:一是先将二进制可执行文件上的 capability 移除(chown 就可以将其移除, setcap -ep 亦可),二是利用 systemd 的服务配置来实现配置 capability (如下)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [Unit] Description=Nginx After=syslog.target network.target remote-fs.target nss-lookup.target [Service] User=triplez Group=triplez Type=forking Environment="LD_LIBRARY_PATH=/your/path/to/openssl/lib:/your/path/to/jemalloc/lib:/your/path/to/luajit/lib" PIDFile=/run/nginx/nginx.pid RuntimeDirectory=nginx RuntimeDirectoryMode=0755 ExecStartPre=/your/path/to/nginx -t ExecStart=/your/path/to/nginx ExecReload=/bin/kill -s HUP $MAINPID ExecStop=/bin/kill -s QUIT $MAINPID +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE PrivateTmp=true Restart=always [Install] WantedBy=multi-user.target 再次重新加载 systemd 配置并重启服务,问题完美解决。 RpathIssue https://wiki.debian.org/RpathIssue ↩︎ Use of file capabilities disables LD_LIBRARY_PATH - Red Hat Bugzilla https://bugzilla.redhat.com/show_bug.cgi?id=448594 ↩︎ shared libraries - Linux capabilities (setcap) seems to disable LD_LIBRARY_PATH - Stack Overflow https://stackoverflow.com/questions/9843178/linux-capabilities-setcap-seems-to-disable-ld-library-path ↩︎

2022/3/31
articleCard.readMore

chaosblade 生成指定 CPU 利用率负载的原理

最近笔者在做的一个降级功能,与机器资源情况密切相关。然而在测试时发现控制 CPU 利用率来构造测试条件,并不是一个容易的事情。借助时间片的思想,笔者用一个非常简单的 shell 一定程度上解决了这个问题。但转念一想,对于混沌测试的软件,这应该是个必备能力。查找了一下 chaosblade 的相关资料,果然支持生成指定 CPU 利用率的负载。故读了读其这部分源码,看看它是怎么实现的。 使用 chaosblade 来构造指定 CPU 利用率的负载非常简单: 1 blade create cpu load --cpu-percent 80 即能够生成使 CPU 利用率到达 80% 的负载。 burncpu 的核心逻辑位于这里: https://github.com/chaosblade-io/chaosblade-exec-os/blob/318c52d83a851bc75012abc7d880d4f440f1f972/exec/bin/burncpu/burncpu.go#L140-L168 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 func burnCpu() { runtime.GOMAXPROCS(cpuCount) var totalCpuPercent []float64 var curProcess *process.Process var curCpuPercent float64 var err error totalCpuPercent, err = cpu.Percent(time.Second, false) // 获取当前所有 CPU 一秒内平均利用率 // ... curProcess, err = process.NewProcess(int32(os.Getpid())) // ... curCpuPercent, err = curProcess.CPUPercent() // 获取当前进程的 CPU 利用率 // ... otherCpuPercent := (100.0 - (totalCpuPercent[0] - curCpuPercent)) / 100.0 // 除去已有进程,可操作的 CPU 利用率。值的范围为 [0, 1] go func() { t := time.NewTicker(3 * time.Second) for { select { // timer 3s case <-t.C: totalCpuPercent, err = cpu.Percent(time.Second, false) // ... curCpuPercent, err = curProcess.CPUPercent() // ... otherCpuPercent = (100.0 - (totalCpuPercent[0] - curCpuPercent)) / 100.0 } } }() // 每 3s 更新一次 totalCpuPercent, curCpuPercent 和 otherCpuPercent if climbTime == 0 { // 不需要爬坡时间 slopePercent = float64(cpuPercent) // 爬坡值与目标值一致 } else { // ... } // cpuCount 是由 runtime.NumCPU() 得来,获取的是当前 CPU 的逻辑核数量 for i := 0; i < cpuCount; i++ { go func() { busy := int64(0) idle := int64(0) all := int64(10000000) // 设定 10ms 为一个周期 dx := 0.0 ds := time.Duration(0) for i := 0; ; i = (i + 1) % 1000 { // 死循环 startTime := time.Now().UnixNano() if i == 0 { // 每 1000 次进入 // 这个赋值语句是整个 burncpu 的灵魂。 // 我们最终希望获得的是 slopePercent% 的 CPU 利用率 // 应该生成的 CPU 压力即为 (slopePercent - totalCpuPercent[0])% // 理想条件下,我们只需对 (slopePercent - totalCpuPercent[0]) 个 0.1ms 时间片设置为 busy 状态即可 // 但由于系统中存在其他进程,burncpu 无法真正获得到 (slopePercent - totalCpuPercent[0]) 个 0.1ms 时间片 // 因此需要按比例放大时间片的个数,而这个比例则是当时 burncpu 实际可用的 CPU 利用率 // 将这个赋值语句转换为如下方程,则更好理解了: // slopePercent = totalCpuPercent + dx * otherCpuPercent // ^ ^ ^ ^ // 最终获得的CPU利用率 当前CPU利用率 一个周期内busy的时间片个数 burnCpu真正可操作的CPU比例 dx = (slopePercent - totalCpuPercent[0]) / otherCpuPercent busy = busy + int64(dx*100000) // 有 dx 个 0.1ms 需要为 busy 状态 if busy < 0 { busy = 0 } idle = all - busy if idle < 0 { idle = 0 } ds, _ = time.ParseDuration(strconv.FormatInt(idle, 10) + "ns") } for time.Now().UnixNano()-startTime < busy { } // 阻塞 CPU,使 CPU 位于 busy 状态,直至设定的时间片结束 time.Sleep(ds) // 空闲 CPU,使 CPU 处于 idle 状态,直至设定的时间片结束 runtime.Gosched() } }() } select {} // 阻塞 burnCpu 函数,保活 goroutines }

2022/1/26
articleCard.readMore

Lexer & Parser Toolchain

推荐一个非常好的编译器工具链入门教程: https://pandolia.net/tinyc/index.html Lexer - flex flex 文件格式: https://pandolia.net/tinyc/ch8_flex.html 1 2 3 4 5 6 7 8 9 10 11 %{ Declarations %} Definitions %% Rules %% User subroutines Declarations:声明,会被原样复制入 lex.yy.c 。一般用于声明全局变量和函数。 Definitions:定义,可以定义正则表达式的名字,用于 Rules 中使用,通过名字直接使用预定义的正则表达式。 Rules:规则,每一行都是一条规则,由匹配模式 pattern (正则表达式)和事件 action (C 代码)组成。 User subroutines:用户定义过程,会被原样复制到 lex.yy.c 的最末尾。 yywrap() 用于把多个输入文件打包成一个输入,当 yylex 将一个文件读入到结尾 EOF 时,会向 yywrap 询问是否继续。若需连续解析多个文件,需要在 yywrap 中打开文件,并返回 0。返回 1 则表示后面没有文件可以读取了,使得 yylex 函数结束。 yytext:刚刚匹配到的字符串的值。 yyleng:刚刚匹配到的字符串的长度。 如一个简单的计算器实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 %{ #include "y.tab.h" %} %% [0-9]+ { yylval = atoi(yytext); return T_NUM; } [-/+*()\n] { return yytext[0]; } . { return 0; /* end when meet everything else */ } %% int yywrap(void) { return 1; } Parser - Yacc/Bison bison 可以认为是 yacc 的开源实现。 Bison 文件的格式: https://pandolia.net/tinyc/ch13_bison.html 1 2 3 4 5 6 7 8 9 10 11 %{ Declarations %} Definitions %% Productions %% User subroutines Declarations:声明,会被原样复制入 y.tab.c 。一般用于声明全局变量和函数。 Definitions:定义,可以定义 bison 专有的变量。 %token:单字符 token (token type 值与字符的 ASCII 码相同)不需要使用 %token 进行预定义,其他类型的 token 都需要使用 %token 进行预定义。bison 会自动为 token 分配一个编号,并写入 y.tab.h 中,因此在 flex 文件中是可以直接调用的。 %left 、%right:表示符号是左(左向右)、右(右向左)结合的。 %nonassoc :表示符号是不可结合的,如 x op y op z 是非法的。 %prec:上下文依赖的优先级,如「负号」就是一个很典型的例子,见 Context-Dependent Precedence。 更多定义可见 bison Declarations。 Productions: : 代表 ->,或 EBNF 式中的 =。 | 用于分隔同一个非终结符的不同产生式。 /* empty */ ,若产生式右边为 $\epsilon$ 时,不需要写任何符号,可写为注释 /* empty */。 ; 表示结束一个非终结符的产生式。 每个产生式后面花括号内,都是一段 C 代码,可在产生式被应用时执行。 例: 1 2 3 s : S E '\n' { printf("ans = %d\n", $2); } | /* empty */ { /* empty */} ; User subroutines:用户定义过程,会被原样复制到 y.tab.c 的最末尾。 bison 会将语法产生式以及符号优先级转换成一个 C 语言的 LALR(1) 动作表,输出到 y.tab.c 中。并会将这个动作表转换为可读形式输出至 y.output 中。 bison 会根据自定义语法文件在 y.tab.c 中生成一个函数 int yyparse(void) 。这个函数按照 LR(1) 解析流程,对词法分析中得到的 token 流进行解析。每当读取下一个符号时,就会执行一次 x = yylex() 。每当要执行一个折叠动作(reduce)时,相应的产生式后的 C 代码将被执行,执行完后将相应的状态出栈。 若 token 流不合法,yyparse 会在第一次出错的地方终止,并调用 yyerror 函数,最后返回 1。 在 reduce 动作时,可用 $1, $2 … $n 来引用属性栈的属性(可以认为是产生式中的第 n 个属性内容,并在最后将这个状态下的属性出栈。其中,$$ 代表产生式左侧的终结符,可在 reduce 动作设置 $$ 的值,最后将 $$ 入栈。 如一个简单的计算器实现: 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 %{ #include <stdio.h> void yyerror(const char* msg) {} int yylex(); %} %token T_NUM %left '+' '-' %left '*' '/' %% S : S E '\n' { printf("ans = %d\n", $2); } | /* empty */ { /* empty */ } ; E : E '+' E { $$ = $1 + $3; } | E '-' E { $$ = $1 - $3; } | E '*' E { $$ = $1 * $3; } | E '/' E { $$ = $1 / $3; } | T_NUM { $$ = $1; } | '(' E ')' { $$ = $2; } ; %% int main() { return yyparse(); }

2022/1/25
articleCard.readMore

grub rescue>, Oops!

教你如何修复丢失的 Linux GRUB 启动引导。 起因 为了再将神船 Z7 用起来,笔者决定扩展其仅有 200GB 的游戏分区空间。然而由于可用空间的位置并不连续,Windows 自带磁盘工具无法处理这种情况,且笔者并不希望将分区转为动态分区,因此使用了一个第三方软件 EaseUS Partition Master Free 来完成磁盘数据的迁移和分区重分配工作。就在设置好分区,重启了之后,屏幕上赫然显示着一个命令行界面: 1 2 3 error: unknown filesystem. Entering rescue mode... grub rescue> Oops!难道重新分个区就把系统搞挂了吗?!grub rescue> 这个命令行界面在我心中一直是噩梦般的存在,之前遇到都只能选择计算机三大法宝之一:重装来解决问题。 既然双系统里的 Linux 进不去了,那我试试直接进 Windows?于是重启了一下机器,通过 BIOS 直接进入了系统盘位于 SSD 中的 Windows。好家伙,这能正常启动,那问题就不大了。 Windows 磁盘分区的重分配工作得到了验证,看上去非常完美。笔者用于存储游戏的专用分区已经扩容至 ~450GB ,那接下来还是修复下 Linux 启动引导的问题吧。 经过一番简单的搜索,笔者摸清了其中大概的原因:在进行 Windows 磁盘操作时,对整个 HDD 做了分区、合并等处理,导致 GPT 元数据发生了改变,Linux 的引导程序 GRUB 自然就无法找到之前的 /boot 启动分区了,这也是报出 error: unknown filesystem 的原因。 在 grub rescue 中修复启动项 由于是硬盘元数据发生了改变导致 GRUB 失效,那我们的思路就是更新 GRUB 中的配置。 首先要找到目前 Linux 的 /boot 启动分区的位置,在 grub rescue 中,可以用 ls 来寻找: 1 2 grub rescue> ls (hd0) (hd0,gpt7) (hd0,gpt6) (hd0,gpt5) (hd0,gpt4) (hd0,gpt3) (hd0,gpt2) (hd0,gpt1) (hd1) (hd1,gpt3) (hd1,gpt2) (hd1,gpt1) 直接输入 ls ,即可得到所有的分区位置。但是启动分区到底是哪个呢?我们可以继续用 ls 来查找,先试试 (hd0,gpt1) 这个分区吧。 此处要注意的是,我们要查找的是 /boot/grub 这个文件夹目录存在的分区。由于笔者的 /boot 启动分区是单独挂载的,所以查找的路径是 <partition>/grub 。若安装的 Linux 并没有单独挂载 /boot 启动分区,则查找路径应为 <partition>/boot/grub。 1 2 grub rescue> ls (hd0,gpt1)/grub error: unknown filesystem. 输出为 error: unknown filesystem. ,说明这个分区并不是我们想要的启动分区,那再试试下一个分区。 1 2 grub rescue> ls (hd0,gpt2)/grub ./ ../ x86_64-efi/ grubenv themes/ fonts/ grub.cfg Bingo!这就是我们要找的东西丫!好,记下这个分区 (hd0,gpt2),它就是我们宝贵的启动分区。 接下来就是给 GRUB 修改启动配置,只要改两个参数即可: 1 2 grub rescue> set root=(hd0,gpt2) grub rescue> set prefix=(hd0,gpt2)/grub 将 root 设置为启动分区。 将 prefix 设置为 grub 安装文件夹。若 /boot 未挂载单独分区,也可能为 <partition>/boot/grub。 配置也修改好了之后,重新使 GRUB 进入普通模式就可以找回我们的启动项了! 1 2 grub rescue> insmod normal grub rescue> normal 执行完后,应该能看到我们熟悉的 GRUB 引导界面了。 持久化 事实上,这样的修复只是暂时的,我们还需要对 GRUB 配置进行持久化的更新,否则每次开机都要这样操作一遍。 进入 Linux,打开终端,输入如下命令,GRUB 就会自动更新配置。 1 2 $ sudo update-grub $ sudo grub-install /dev/sda /dev/sda 为 Linux 的安装磁盘。 可以再重启一次计算机,验证 Linux 启动引导是否修复成功。 References Repair Linux boot failures in GRUB 2 rescue mode https://www.howtoforge.com/tutorial/repair-linux-boot-with-grub-rescue/ grub rescue 救援模式的处理 http://xstarcd.github.io/wiki/Linux/grub_rescue.html Linux 系统引导失败,出现 grub rescue 恢复界面 - 少数派 https://sspai.com/post/55875

2021/11/28
articleCard.readMore

浅谈 beancount 借款还款交易记录方法

本文基于的假设是:友人 A 需要购买产品 B,但需要你来代他购买。探讨以下几种事件发生顺序的记账方法。 除了自己已有的 Assets:Bank:Z 外,需要额外建立这些账户: Assets:Receivables:A 先转账,再交易 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ; 转账 0000-00-00 * "A transfer money" Assets:Receivables:A -1000.00 CNY Assets:Bank:Z 1000.00 CNY ; 此时 Assets:Bank:Z 账户起着代持资金的作用。 ; 交易 0000-00-01 * "Buy B for A" Assets:Bank:Z -1000.00 CNY Assets:Receivables:A 1000.00 CNY ; 整个过程结束后各账户余额: ; Assets:Bank:Z 0.00 CNY ; Assets:Receivables:A 0.00 CNY 先交易,再转账(垫付) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ; 交易(垫付) 0000-00-00 * "Buy B for A" Assets:Receivables:A 1000.00 CNY Assets:Bank:Z -1000.00 CNY ; 此时 Assets:Bank:Z 账户起着垫付资金的作用, ; Assets:Receivables:A 用于应收来自 A 的款项。 ; 转账(还款) 0000-00-01 * "A transfer money" Assets:Bank:Z 1000.00 CNY Assets:Receivables:A -1000.00 CNY ; 交易结束后各账户余额: ; Assets:Bank:Z 0.00 CNY ; Assets:Receivables:A 0.00 CNY 转账和交易同步 1 2 3 4 5 6 7 8 9 10 ; 转账+交易 0000-00-00 * "A transfer money to buy B" Assets:Receivables:A -1000.00 CNY Assets:Bank:Z 1000.00 CNY Assets:Bank:Z -1000.00 CNY Assets:Receivables:A 1000.00 CNY ; 交易结束后各账户余额: ; Assets:Bank:Z 0.00 CNY ; Assets:Receivables:A 0.00 CNY

2021/8/10
articleCard.readMore

月度记账工作流

准备工作 一部装有 beancount 、fava 以及 double-entry-generator 的 Mac 或 PC。 一部 iPhone 或 Android 手机。 本地存储个人 my-bookkeepings 账本最新内容。 开始工作! Balance CheatSheet 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ; wechat pay balance 0000-00-00 document Assets:Digital:Wechat:Cash "./path/to/your_wechat_bills.csv" 0000-00-01 balance Assets:Digital:Wechat:Cash <balance_value> ~ 0.00 CNY ; alipay balance 0000-00-00 document Assets:Digital:Alipay:Cash "./path/to/your_alipay_bills.csv" 0000-00-01 balance Assets:Digital:Alipay:Cash <balance_value> ~ 0.00 CNY ; bank balance 0000-00-01 balance Assets:Bank:CN:ICBC:Savings <balance_value> ~ 0.00 CNY 0000-00-01 balance Assets:Bank:CN:ICBC:SocialSecurity <balance_value> ~ 0.00 CNY 0000-00-01 balance Assets:Bank:CN:BOCOM:Savings <balance_value> ~ 0.00 CNY 0000-00-01 balance Assets:Bank:CN:BOC:Savings <balance_value> ~ 0.00 CNY 0000-00-01 balance Liabilities:CreditCard:CN:ICBC <balance_value> ~ 0.00 CNY 1111-11-19 balance Liabilities:CreditCard:CN:CMB <balance_value> ~ 0.00 CNY 1111-11-20 balance Liabilities:CreditCard:CN:BOCOM <balance_value> ~ 0.00 CNY 导入微信支付账单 先查看上月微信 beancount 账单,确定最后导入日。 在「微信」中导出从最后导入日至今日的微信支付账单。 使用如下命令生成账单: 1 2 3 4 5 double-entry-generator translate \ --config ./config/double-entry-generator/wechat.yaml \ --provider wechat \ --output tmp-wechat.beancount \ your_wechat_bills.csv 修改相关 FIXME 账户交易(posting)。 在当月 index.beancount 中添加微信支付的相关 balance 和 document 语句,例: 1 2 0000-00-00 document Assets:Digital:Wechat:Cash "./path/to/your_wechat_bills.csv" 0000-00-01 balance Assets:Digital:Wechat:Cash <balance_value> ~ 0.00 CNY balance 日期必须为 T+1,否则语句会忽略今日交易,导致对账失败。 若对账失败,特别是小额差异,极有可能是「零钱通」发放利息所致。值得注意的是,微信「零钱通」的利息发放并不包括在微信导出的账单当中。 值得注意的是,从「理财通」转入至「银行卡」的交易,微信支付是没有记录的,见这里。 导入支付宝账单 先查看上月支付宝 beancount 账单,确定最后导入日。 在「支付宝」导出从最后导入日至今日的支付宝账单。 使用如下命令生成账单: 1 2 3 4 5 double-entry-generator translate \ --config ./config/double-entry-generator/alipay.yaml \ --provider alipay \ --output tmp-alipay.beancount \ your_alipay_bills.csv 修改相关 FIXME 账户交易(posting)。 在当月 index.beancount 中添加支付宝的相关 balance 和 document 语句,例: 1 2 0000-00-00 document Assets:Digital:Alipay:Cash "./path/to/your_alipay_bills.csv" 0000-00-01 balance Assets:Digital:Alipay:Cash <balance_value> ~ 0.00 CNY balance 日期必须为 T+1,否则语句会忽略今日交易,导致对账失败。 导入火币账单 (可选,依据月度是否有火币交易) 先查看上月火币 beancount 账单,确定最后导入日。 在「火币」导出从最后导入日至今日的火币账单。 使用如下命令生成账单: 1 2 3 4 5 double-entry-generator translate \ --config ./config/double-entry-generator/huobi.yaml \ --provider huobi \ --output tmp-huobi.beancount \ your_huobi_bills.csv 修改相关 FIXME 账户交易(posting)。 导入中国工商银行账单 先查看上月工商银行 beancount 账单,确定最后导入日。 在「中国工商银行」导出从最后导入日至今日中国工商银行的借记卡和信用卡账单。 使用如下命令生成账单: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 生成贷记卡账单 double-entry-generator translate \ --config ./config/double-entry-generator/icbc-1120-5595.yaml \ --provider icbc \ --output tmp-icbc-1120-5595.beancount \ your_icbc_1120_5595_bills.csv # 生成借记卡账单 double-entry-generator translate \ --config ./config/double-entry-generator/icbc-9855.yaml \ --provider icbc \ --output tmp-icbc-9855.beancount \ your_icbc_9855_bills.csv double-entry-generator translate \ --config ./config/double-entry-generator/icbc-5868.yaml \ --provider icbc \ --output tmp-icbc-5868.beancount \ your_icbc_5868_bills.csv 修改相关 FIXME 账户交易(posting)。 在当月 index.beancount 中添加中国工商银行的相关 balance 和 document 语句,例: 1 2 3 4 0000-00-00 document Assets:Bank:CN:ICBC:Savings "./path/to/your_icbc_debit_bills.csv" 0000-00-00 document Liabilities:CreditCard:CN:ICBC "./path/to/your_icbc_credit_bills.csv" 0000-00-01 balance Assets:Bank:CN:ICBC:Savings <balance_value> ~ 0.00 CNY 0000-00-01 balance Liabilities:CreditCard:CN:ICBC <balance_value> ~ 0.00 CNY 导入其他银行卡账单 double-entry-generator 目前仅支持中国工商银行账单的转换,鉴于个人使用银行卡的交易较少(<10 笔/月),可以直接使用手动记账。 打开: 中国工商银行 招商银行 中国银行 交通银行 先写一个 T+1 的 balance 对账语句,对相关的账户进行断言。 1 2 3 4 5 6 7 0000-00-01 balance Assets:Bank:CN:ICBC:Savings <balance_value> ~ 0.00 CNY 0000-00-01 balance Assets:Bank:CN:ICBC:SocialSecurity <balance_value> ~ 0.00 CNY 0000-00-01 balance Assets:Bank:CN:BOCOM:Savings <balance_value> ~ 0.00 CNY 0000-00-01 balance Assets:Bank:CN:BOC:Savings <balance_value> ~ 0.00 CNY 0000-00-01 balance Liabilities:CreditCard:CN:ICBC <balance_value> ~ 0.00 CNY 1111-11-19 balance Liabilities:CreditCard:CN:CMB <balance_value> ~ 0.00 CNY 1111-11-20 balance Liabilities:CreditCard:CN:BOCOM <balance_value> ~ 0.00 CNY 笔者的信用卡账单分别是每月 1 日和 19 日出账,故将信用卡断言的日期设为当月 1 日以及 19 日。 工商银行信用卡可以看到实时余额,而招行和交行的只能在出账当天才可获得余额。因此工行信用卡可以直接断言当日余额,而招行、交行银行卡需要断言出账日与余额。 一般 0000-00-01 为记账当日 T+1 ,而 1111-11 一般为记账当月的月份。 2023-01-25 ,则 0000-00-01 应改为 2023-01-26 ,1111-11 应改为 2023-01 。 查找当月相关银行卡账单,手动记录在当月的 index.beancount 中。 总结 以月为粒度,微信、支付宝账单基本都能够正常转换,整个工作流下来需要花费的时间成本在二十分钟以内。 使用 double-entry-generator 能够基本实现个人记账期望,可以考虑固化该工作流,以长期使用。 相关链接 beancount/beancount https://github.com/beancount/beancount beancount/fava https://github.com/beancount/fava deb-sig/double-entry-generator https://github.com/deb-sig/double-entry-generator 多种支付账户的账单导出及查看方法汇总 https://blog.triplez.cn/posts/bills-export-methods/

2021/6/27
articleCard.readMore

多种支付账户的账单导出及查看方法汇总

第三方电子支付 微信支付 打开“微信”,点击“我的”,点击“支付”,进入如下界面: 点击“钱包”,点击右上角“账单”,点击右上角的“常见问题”。 点击“下载账单”。 选择“用于个人对账”。 自行选择需要导出的日期范围,填写自己的邮箱即可。 支付宝 打开「支付宝」APP,在菜单栏中点击「我的」。 进入「账单」,点击右上角「…」。 选择「开具交易流水证明」。 在「选择申请用途」中选择「用于个人对账」。 选择需要的交易时间范围。 填写电子邮箱后,即可获取账单。 余额宝 打开余额宝网站,选择日期,点击“下载”即可。 银行 借记卡 中国工商银行 登录中国工商银行个人网上银行,在「我的卡包」中选择某个银行卡,点击「明细」按钮,进入「明细查询」。 macOS 需要使用 Safari 并安装工行插件才可正常登录。 选择需要的时间区间,在「下载明细格式」选择 「EXCEL 格式(.csv)」,点击「下载」后,提示“明细正在下载中,请稍后回到本交易页面查看下载结果”。稍等片刻,页面提示“您的历史明细下载已经处理成功,请您再次进入明细查询页面进行下载!”。此时重新点击「查询」,在「下载」按钮旁边会出现可下载结果的链接,点击即可下载真正的 csv 账单文件(不要忘记重新将下载格式改为 csv)。 交通银行 手机「交通银行 APP」->查找“交易明细”->最下面“是否需要开立交易明细证明?”点击「立即申请」->「交易明细清单」->「电子版」-> 选择日期->填写电子邮箱->「确认开立」->输入银行卡密码->开立成功 PDF 为加密格式,加密密码为身份证后六位。 如何开立 CSV 格式的账单? 中国银行 手机「中国银行 APP」->「首页」->「更多」->「助手」->「交易流水打印」->「立即申请」-> 选择日期 -> 选择发送邮箱 -> 填写电子邮箱 -> 点击「申请」-> 申请成功 PDF 为加密格式,加密密码在「交易流水打印」的「申请记录」中可以找到。 网上银行: 需要安装安全输入插件,用 Chrome 登录中国银行网上银行。 找不到登录账号了。 信用卡 招商银行 “电邮账单”获取方式: 掌上生活->金融->查账单->右上角…->账单服务->账单补寄->(寄送方式改为「电邮账单」)->申请补寄 – zsxsoft/my-beancount-scripts 如何获取 CSV 账单? 登录网银大众版或专业版后,点【信用卡→账户管理→未出账单查询/已出账单查询】查看/下载/打印一年内的账单,带有业务受理专用章。 已出账单:点击【账单明细→点击下载财务明细】打印/下载,格式为【PDF】。 未出账单:点击页面右侧的下载或打印,格式为【Excel】。 社保(深圳) 深圳五险缴费明细查询: 登录深圳社保服务网站,进入“查询服务”,选择“缴费信息查询”,点击“五险缴费明细查询”即可。 深圳医保、养老个人账户余额查询: 登录深圳社保服务网站,进入“查询服务”,选择“参保信息查询”,点击“基本医疗保险个人账户查询”即可。 公积金(深圳) 「i 深圳 APP」,在首页点击「公积金」,进入「深圳市公积金」页面,即可看到余额。点击「账户明细」即可看到缴费情况。 加密货币 火币 Global 登录火币 Global 网站,进入币币订单的成交明细页面,选择合适的时间区间后,点击成交明细右上角的导出按钮即可。

2021/6/19
articleCard.readMore

欢迎 Macat !

今天我家来了一位新主子,在此我隆重的向大家介绍下:他名字叫 Mac,是一只 Cat,所以也叫 Macat ~ 中文名叫麦麦 我才不会说是因为我喜欢吃 McDonald 的 Big Mac 哈哈哈。 最后放一个照片给大家欣赏欣赏。

2021/4/11
articleCard.readMore