在 Linux 虚拟机中使用 PyAutoGUI 做自动化

PyAutoGUI 是 GUI 功能强大自动化方案,但 UI 程序的运行环境选择与配置也是一大难题。 系统选择 为了让环境可迁移,免维护,资源消耗少,必须使用虚拟化的方案。 虽然 PyAutoGUI 支持 Windows/macOS/Linux 三个平台。但是各有各的弊端。 Windows 的系统臃肿且开发环境不友好 macOS 的权限管理过于严格且虚拟化难度大,高dpi的屏幕也不适合自动化 Linux 就是最好的选择了,但执行的应用可能不支持 Linux,必须引入 Wine 综合以上情况考虑,系统环境选择如下 pve: 宿主机系统,也可以选择别的环境 debian: 客户机系统,目前是 debian 12, 方便使用 deepin 的 wine 程序 mate: 桌面环境,比较轻量,实测兼容性比 xfce 好 星火应用商店: 方便安装各种国内软件和 Wine 程序 统信 Windows 应用兼容引擎: 在星火应用商店中安装,可以用于安装和打包商店中没有的程序 虚拟机配置 步骤 创建好 pve 虚拟机, 安装 debian,选择 mate 桌面 设置分辨率:控制中心 -> 显示器 -> 设置为 1920x1080 关闭自动睡眠:控制中心 -> 电源管理 -> 动作和显示修改为从不 关闭屏幕保护:控制中心 -> 屏幕保护程序 -> 取消勾选所有选项 安装gnome-screenshot: 用于 PyAutoGUI 内部调用截图 安装星火应用商店(可选) 安装统信 Windows 应用兼容引擎(可选):在星火应用商店中安装 火焰截图(可选):在星火应用商店中安装,更方便对应用的按钮和文字截图。 其他说明: 如果不关闭自动睡眠和屏幕保护,PyAutoGUI 就无法截取到应用的图像,也就无法操作了。 图形显示协议默认选 x11,wayland 对显卡有要求,虚拟机不方便处理。 睡眠后时间不对 虚拟机睡眠唤醒后时间没有更新,不知道为什么没有触发时间更新。 目前只能通过强行同步来解决 在脚本里执行同步命令 1 2 # 和阿里云ntp服务器同步 sudo ntpdig -S ntp.aliyun.com 或者,理论上可以通过虚拟机中的 systemd-suspend hook,或者 pve hookscript 来解决(没试成功) PyAutoGUI 使用技巧 远程登录调试 pve 默认图片控制台不太方便,不能复制粘贴,所以使用远程登录调试 由于系统使用 x11 环境,这里使用 xrdp 作为远程服务端。 如果以后更新到 wayland,可以使用 gnome-remote-desktop。 1 2 sudo apt update sudo apt install xorgxrdp xrdp 远程登录时找不到显示器 xrdp 或者 ssh 远程登录执行脚本时,窗口相关命令可能会提示找不到显示器。 可以在自动化脚本中设置以下环境变量 1 2 os.environ['DISPLAY'] = ':0' os.environ['XAUTHORITY'] = '/home/<your-name>/.Xauthority' 过滤没必要的截图日志 gnome-screenshot 截图时会产生一些无用日志 1 2 ** Message: 17:36:31.888: Unable to use GNOME Shell's builtin screenshot interface, resorting to fallback X11. 新建一个 gnome-screenshot 文件,赋予执行权限,放到 PATH 环境变量中比 /usr/bin 靠前的路径 1 2 #!/bin/bash exec /usr/bin/gnome-screenshot "$@" >> /tmp/gnome-screenshot.log 2>&1 验证码识别 pytesseract 常见的方案是 pytesseract, 但是效果不好,识别率比较一般。 安装 1 2 sudo apt install tesseract-ocr pip install pytesseract 使用 1 pytesseract.image_to_string(image, config='--psm 8 -c tessedit_char_whitelist=0123456789') ddddocr ddddocr 是基于机器学习的验证码识别库,识别效果比较好。 这里使用 docker 安装 fastapi接口 1 docker run -d -p 8000:8000 oozzbb/ddddocr-fastapi:latest 使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 def ocr_image(file='region.png'): url = 'http://your-address:8000/ocr' with open(file, 'rb') as fp: files = { 'file': fp, } data = { 'probability': 'false', 'png_fix': 'false', 'charsets': '0123456789' # 验证码可能的字符列表 } response = requests.post(url, files=files, data=data) assert response.ok return response.json()['data'] 高效执行自动化脚本 可以在宿主机远程调用自动化脚本,并且脚本执行完成后挂起虚拟机 1 2 3 4 5 6 7 8 9 10 HOST=your-name@vm-ip-address VMID=109 # pve 虚拟机id qm resume $VMID # 唤醒虚拟机 sleep 2 ssh $HOST "SOME_VAR=foobar python your-script.py" # 执行脚本 sleep 1 qm suspend $VMID # 关闭虚拟机 其他有用的函数 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 # 切换窗口到前台 def wm_to_front(title: str): if sys.platform == 'linux': return subprocess.check_call(['wmctrl', '-a', title]) raise NotImplementedError # 点击或者移到到图片位置 def click_image(image, confidence=0.9, grayscale=True, delay=1.5, duration=0.6, offset=(1, 0), abort=True, click=True): try: loc = pyautogui.locateOnScreen(image, confidence=confidence, grayscale=grayscale) except pyautogui.ImageNotFoundException: loc = None if loc: x, y = pyautogui.center(loc) if click: pyautogui.click(x+offset[0], y+offset[1], duration=duration) else: pyautogui.moveTo(x+offset[0], y+offset[1], duration=duration) time.sleep(delay) return loc else: if abort: raise RuntimeError(f"can not find {image}!") else: return None # 等待某个图片出现 def wait_button(image, timeout, interval=10): for _ in range(timeout//interval): time.sleep(interval) loc = click_image(image, abort=False) if loc: break else: raise RuntimeError(f'wait {image} failed') time.sleep(interval/2) 使用感受 PyAutoGUI 是基于图像而不是图型控件来识别目标,没有确认反馈。 相比于网页自动化的 Selenium 就感觉落后些了。 当然,Windows 平台有 pywinauto 支持控件识别,但我不咋熟悉。 总之,PyAutoGUI 就像手枪,虽然比较简陋,但还是很直观的,执行简单的任务也够用了。 注意事项 自动化代码中不要编码用户名密码相关信息,建议用环境变量传入 不要轻易调整系统的 dpi(建议设为1.0) 和分辨率,可能导致图片无法识别 xfce 对 wine 应用的兼容性似乎不好,wine 文件保存对话框被可能被反复触发,原因不明。

2025/5/27
articleCard.readMore

语言的力量

语言的力量远比想象的强大,某种程度上是有虚空造物的能力。 你可以凭空创造一个概念,并植入你的听众脑子,不论好还是坏。诈骗分子、PUA 大师、营销大师无不精通此等技艺。 起因是一个综艺节目,有个导演对哈妮克孜说了句话,让我惊为天人,让我惊叹“你他娘真是个人才”。 它在线下拍戏的时候见到了哈妮克孜,哈当时戴了副大镜框眼镜,于是它说道“我当时看到你的时候,感觉好失望,你和我银幕上看到的哈妮克孜完全不一样”。 它凭空创造了一个观念(事实),哈妮克孜颜值不行。怎么创造的呢?和哈妮克孜荧幕形象相比。这显然是不客观的,但很难瞬间就察觉到。 这样,好与坏、美与丑一切观念都是可以被重新定义的,只要你选择合适的目标和对照标准,甚至是你脑海里的标准。 你是丑的和你漂亮的时候比,你是蠢的和聪明的时候比,我对你是失望的和对你的期望相比。 PUA 大师就是用这种手法实现打压,新闻学就是这么颠倒黑白,当然也可以反向操作,那么你就成为知心姐姐鼓励大师。 所以,神话中的言出法随也不难理解,毕竟人是观念的动物,你改变/植入了某人的某个观念,那么你就改变了他眼中的世界。 这也就是《乔布斯传》中乔布斯的扭曲现实力场。

2025/5/27
articleCard.readMore

联想笔记本 BIOS 跳过检测强制降级

联想某些版本的 bios 似乎会禁止降级,即使打开 bios 设置里的允许降级选项,依然会提示 “this platform does not support IHISI interface” 的错误,导致降级失败。 降级方法 经过一些尝试,找到了方法绕过,以 ThinkBook 14 G3 ACL 机型为例 打开驱动下载页面 打开 BIOS 页面, 下载以下安装文件 当前版本 BIOS 的安装包(NEW),版本 GQCN41WW_HFCN36WW 以及想要降级的 BIOS (OLD),版本 GQCN36WW_HFCN31WW 运行 NEW,选择仅解压。找到解压目录,应该只有一个 exe 程序,使用 7-zip 或者 bandizip 打开并提取内部文件。 对 OLD 也执行如上操作,如果没有解压选项,可以尝试直接提取。 将 OLD 提取目录中的 .bin 格式固件(GLV3A036.bin、GLV4D031.bin)复制到 NEW 提取目录中。 修改 NEW 提取目录中的 platform.ini 文件,找到如下内容。将其中的 .bin 文件名,修改为 OLD 中的 .bin 文件名,保存。有两个 .bin 文件,文件名是和版本中的数字对应的,不要修改错误。 1 2 3 4 5 6 7 8 9 10 11 ; 修改前 [MULTI_FD] Flag=1 FD#01=ID,GLV4D,GLV4D036.bin FD#02=PCI,0,1,1,0,FFFFFFFF,FFFFFFFF,GLV3A041.bin ; 修改后 [MULTI_FD] Flag=1 FD#01=ID,GLV4D,GLV4D031.bin FD#02=PCI,0,1,1,0,FFFFFFFF,FFFFFFFF,GLV3A036.bin 运行 NEW 提取目录中的 H2OFFT-Wx64.exe,等待降级完成。 注意:有风险,请谨慎操作。不要跨太多版本,可能有不可预料的问题 参考 https://www.geektech.co.nz/lenovo-y700-how-to-enable-bios-downgrade

2025/5/15
articleCard.readMore

redroid “设备未获得play保护机制认证” 问题

使用 redroid 等安卓虚拟环境,可能会发现 google play 用不了的问题。 虽然系统集成了 gapps,但系统提示 “设备未获得play保护机制认证”,无法登录 play 商店。 可能的原因比较多,这里大概是因为虚拟机的型号没在google的数据库里。 解决方案就是,获取 GSF ID 注册到 Google。 步骤 安装 device id 打开应用,复制 GSF id,假设为ffffffff 终端运行printf "%d\n" 0xffffffff,转换换成10进制数字。 打开 https://www.google.com/android/uncertified/ 输入转换后的结果并提交

2025/5/7
articleCard.readMore

在 iOS 上访问安卓应用

有些应用在安卓上是独占的,iOS 上又没有比较好的替代品,而且 iOS 上没有能用安卓模拟器。 如果使用多个设备,维护的心智成本又高,被这个问题困扰了许久。 最近碰巧了解了 scrcpy, 用于远程控制安卓,终于解决了这个问题。 需要的工具 安卓环境:虚拟环境或者物理设备均可,这里使用虚拟环境。以下是可选的虚拟环境 PVE 的 PCT容器 (Proxmox Container Toolkit),需要打补丁 docker,基于Redroid 需要调整内核参数 WSA (Windows Subsystem for Android),与 scrcpy 配合有点问题,窗口有黑边。 VMware、VirtualBox等虚拟化平台,需要解决虚拟显卡。 使用 scrcpy-mobile 远程控制安卓环境, 当然 scrcpy 也支持 Windows、Linux、macOS PCT 安卓容器 由于已有PVE环境,这里选用PCT,主要步骤 根据 PCT-patches 文档,给 PCT 打好补丁 根据 lineageOS 模板,新建安卓容器,注意去除Unprivileged container的勾选。 修改 lxc.init.cmd 选项,在后面增加以下参数,调整分辨率和iPad一致。 1 androidboot.redroid_width=1668 androidboot.redroid_height=2388 androidboot.redroid_fps=60 ps: 安卓容器对宿主机性能似乎有一定要求,J4125 只是勉强够用,流畅度一般。 macOS 中使用 scrcpy 安装 scrcpy ,连接命令 1 2 3 4 5 6 scrcpy --audio-codec=aac \ --video-codec=h264 \ --video-bit-rate=16M \ --max-fps=60 \ --tcpip=192.168.10.181:5555 \ --start-app=io.legado.app.release 参数解释 --audio-codec=aac 同步声音,使用 AAC 编码,默认参数可能导致没有声音 --video-codec=h264 使用 H.264 视频压缩编码 --video-bit-rate=16M 16Mbps 码率,高画质 --max-fps=60 最大帧率 60FPS,画面流畅 --tcpip=192.168.10.181:5555 Wi-Fi adb 连接设备 --start-app=io.legado.app.release (可选)连接后直接启动 Legado App,应用列表可使用adb shell pm list packages -3查看 iOS 中使用 scrcpy-mobile appstore中安装 scrcpy-mobile 由于该应用的用户界面易用性比较差,表单也不支持有些 scrcpy 参数,这里直接使用快捷指令打开应用。 设置打开 scrcpy-mobile 后,开启引导式访问,防止误触。同时可以在 iOS 设置中开启引导式访问的面容id,防止频繁输入密码。 scrcpy-mobile 的 url schema 1 scrcpy2://192.168.10.181:5555?enable-audio=true&audio-codec=aac&video-bit-rate=16M&video-codec=h264&max-fps=60 最后,这也未尝不算一种 NTR 安卓环境设置(可选) 关闭导航栏:系统 > 手势 > 系统导航 > 手势导航 加快或者关闭系统动画:开发者选项 > 窗口动画缩放、过渡动画缩放、Animator 时长比例 > 关闭或者0.5x

2025/4/26
articleCard.readMore

在 VSCode 中用 Rust 刷LeetCode

本文介绍在 VSCode 中配置和使用插件来高效地解决 LeetCode 问题,并使用 Rust 语言编写和测试代码。 vscode 插件 LeetCode.vscode-leetcode pucelle.run-on-save rust-lang.rust-analyzer 项目结构 cargo new vscode-leetcode-rust 1 2 3 4 5 6 7 8 9 10 // tree -I target --dirsfirst . ├── Cargo.lock ├── Cargo.toml └── src ├── lib.rs ├── main.rs └── solutions ├── 1_two_sum.rs ... vscode 全局设置 1 2 3 4 5 6 7 8 "leetcode.useEndpointTranslation": false, // for english filename "leetcode.workspaceFolder": "/Users/<your_name>/projects/vscode-leetcode-cn-rust", "leetcode.filePath": { "default": { "folder": "src/solutions", "filename": "${id}_${snake_case_name}.${ext}" } }, 用 automod 宏添加新回答到模块 cargo add automod 1 2 3 4 5 6 7 // src/lib.rs const CURRENT: &str = "sdfsdfsd.rs"; // trigger rust-analyser recheck pub mod solutions { automod::dir!("src/solutions"); } 触发 rust-analyzer 用 run-on-save 插件,保存回答时更新 lib.rs,触发 rust-analyzer 重新分析项目,开启新回答的代码补全。 vscode 项目配置 1 2 3 4 5 6 7 8 "runOnSave.commands": [ { // use gnu sed update lib.rs when add solutions (macOS) "command": "sh onsave.sh ${fileBasename}", "runIn": "backend", "finishStatusMessage": "touched ${workspaceFolderBasename}" }, ] onsave.sh脚本,macOS使用 gnused,linux 使用默认 sed 就好。 通过切换模块是否为pub来触发 rust-analyzer 识别新回答 1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash FILE=$1 if [[ `grep "$FILE" src/lib.rs | wc -l` -eq 0 ]]; then gsed -i -E "/CURRENT/c\const CURRENT: &str = \"$FILE\";" src/lib.rs if [[ `grep '::dir!(pub' src/lib.rs | wc -l` -eq 1 ]]; then gsed -i "s|::dir!(pub |::dir!(|" src/lib.rs else gsed -i "s|::dir!(|::dir!(pub |" src/lib.rs fi fi 编写本地测试用例 把测试代码写在 "// @lc code=end" 后面,需要定义 Solution 结构体,可能还需要定义参数的结构体。 1 2 3 4 5 6 7 8 // @lc code=end struct Solution; #[test] fn test_a() { // let res = Solution::is_valid(String::from("()[]{}")); // println!("RESUTL\t{:?}", res); }

2025/3/8
articleCard.readMore

跨域的那些事

什么是跨域? 就是当前域访问了非本域的资源。对于http来说,url代表资源,也就是访问了非本域的url。 域的定义是啥? 这里的域,也就是同源策略(Same-origin policy)中的源。 如果两个 URL 的协议、端口和主机都相同的话,则这两个 URL 是同源的。 当前域:当前网页的域;目标域:访问的资源所在的域 为什么要限制跨域请求?信息泄露 js直接读取浏览器的目标域的sessionid、cookie,伪造请求。 比如,当前站点是恶意站点,它用js请求银行站点,盗取信息。 CSRF,如img标签的src会被访问,利用受害者已认证的身份,在不知情的情况下向受害者认证的站点发起恶意请求。 通过在src里构造url,恶意请求目标域 1 2 <img src="https://bank.com/transfer?amount=1000&to=attackerAccount" style="display:none;"> XSS跨站脚本(Cross-site scripting)注入,导致用户在信息泄露 一般是接受了用户构造的输入,输入里包含恶意脚本内容,内容未被转义,又输出在页面上,从而被执行 其他用户查看了该页面,注入的脚本被执行 怎么限制跨域?浏览器的同源策略 禁止的 DOM 同源策略: 不同源的dom之间不能相互操作,多个iframe的情况 XMLHttpRequest 同源策略: 禁止请求不同源url Cookie、LocalStorage、IndexedDB 等存储性内容同源策略 允许的 页面中的链接,重定向以及表单提交 <script>、<img>、<link>这些包含 src 属性的标签可以加载跨域资源。(只能GET) 限制跨域导致哪些不便? 前后端分离开发时,localhost不能正常访问后端资源 一些公共的api不能被访问 https的页面的http的静态资源,不能加载 如何绕过同源策略? 浏览器启动参数(在用户端操作) 反向代理(在当前域操作) JSONP(在目标域操作) 利用<script>允许跨域的特点,设置标签的src为目标域,动态生成需要的javascript内容 跨源资源共享(CORS)(目标域操作) 设置相应的reponse header 1 2 3 4 Access-Control-Allow-Origin: https://foo.example // 所允许的来源域 Access-Control-Allow-Methods: POST, GET, OPTIONS // 所允许的请求方法 Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 所允许的请求header Access-Control-Max-Age: 86400 参考 https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS https://developer.mozilla.org/zh-CN/docs/Glossary/CSRF https://juejin.cn/post/6879360544323665928 https://juejin.cn/post/6867096987804794888

2023/8/15
articleCard.readMore

HomeBrew 与无 root 权限 Linux 环境包管理

一些公用的 Linux 服务器,处于维护以及安全考虑,一般只会提供普通权限用户给使用者。 普通用户的权限满足日常使用是够了,但是难以配置自己的开发环境,安装一些自己需要的包。 如果都从源码编译安装软件,依赖的维护过于复杂,初始编译工具链的版本可能也不满足需求,如 gcc 版本过低。 如果申请 sudo 权限或者请求更新系统或安装 docker,后期责任难以界定,运维和管理员一般也不会同意。 所以,最优方案还是有需求的用户在个人目录维护自己的工具链和环境。下文方案为围绕 HomeBrew 构建。 安装 miniconda 解决前置依赖 如果你的系统比较新,可以直接尝试安装 HomeBrew。 基于上面讨论的内容,公用服务器一般存在系统版本低的问题,是 centos7 或者 centos6 也毫不稀奇,而且如 glibc 等库的版本也非常低。 安装 HomeBrew 有两个强依赖,git 及 curl,而且依赖的版本都比较高,centos7 的版本也不能满足。 另外,由于 Brew 不少软件都需要从源码编译,gcc 和良好的网络环境也不可缺少。 幸好 miniconda 能够解决以上几点问题。miniconda 只是提供 HomeBrew 安装的依赖,后续可以删除。 配置 conda 源(可选): 新建 .condarc,包含以下内容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 channels: - defaults show_channel_urls: true default_channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2 custom_channels: conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud pytorch-lts: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud 下载及安装 miniconda 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 下载 # 如果服务器的内置证书已过期, 增加 --no-check-certificate 条件跳过证书验证 wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && chmod +x Miniconda3-latest-Linux-x86_64.sh # 安装 ./Miniconda3-latest-Linux-x86_64.sh -b -p ~/miniconda3 source ~/miniconda3/etc/profile.d/conda.sh # 安装所需包 conda install -y gcc_linux-64 gxx_linux-64 curl git # 链接 gcc,等 HomeBrew 安装完成这些链接可以删掉 cd ~/miniconda3/bin/ ln -s x86_64-conda_cos6-linux-gnu-gcc gcc ln -s x86_64-conda_cos6-linux-gnu-cpp c++ ln -s gcc cc 配置安装环境变量 这些环境变量也可以配置到 bashrc 等文件,使之永久生效 1 2 3 4 5 6 7 8 9 10 # 设置 curl 和 git,可选 export HOMEBREW_CURL_PATH=~/miniconda3/bin/curl export HOMEBREW_GIT_PATH=~/miniconda3/bin/git # 设置安装源为清华源,如果网络畅通可忽略,清华源也可能403 export HOMEBREW_INSTALL_FROM_API=1 export HOMEBREW_API_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api" export HOMEBREW_BOTTLE_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles" export HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git" export HOMEBREW_CORE_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git" 安装 HomeBrew 由于没有 root 权限,HomeBrew 需要手动安装。 由于是手动安装,位置与默认安装位置不同,很多预编译的包就不能用了,都得从源码编译,所以网络和机器性能以及耐心很重要。 1 2 3 4 5 6 7 8 9 10 # 下载,也可使用清华源 https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git git clone https://github.com/Homebrew/brew ~/.homebrew # 安装 eval "$(~/.homebrew/bin/brew shellenv)" brew update --force --quiet chmod -R go-w "$(brew --prefix)/share/zsh" # 自动加载 brew echo 'eval "$(~/.homebrew/bin/brew shellenv)"' >> ~/.bashrc 参考 https://docs.brew.sh/Installation https://mirrors.tuna.tsinghua.edu.cn/help/anaconda/ https://mirrors.tuna.tsinghua.edu.cn/help/homebrew/ https://github.com/tuna/issues/issues/1353

2023/5/28
articleCard.readMore

给 macOS 词典增加生词本功能

macOS 系统的自带词典应用非常强大,与其他应用整合很好,快捷取词很方便(command+control+d)。 但是美中不足的是缺少生词本功能,查了单词又很容易忘记,对语言学习者来说就有些不便了。 经过本强迫症的探索,终于找到基于 Karabiner-Elements + Automator + Logseq 的完美生词本方案。 最后的效果是,快捷键取词的同时记录单词卡片到Logseq对应的笔记。 词典词库扩充 参考知乎文章安装好《朗道英汉字典5.0》 这是为了有个释义简洁的词典,方便后续生成生词本词条 编写workflow 使用 macOS 自带应用 Automator(自动操作)编写workflow,将当前鼠标所在位置的文本提取并保存制卡。 首先打开 Automator.app 新建一个 Quick Aciont(快速操作) 然后依次拖入“获得词语定义”,“运行Shell脚本”等步骤,并调整如下几个位置的选项。 修改脚本里的代码为如下内容,生词本路径相应替换,并相应位置新建好生词本文件。 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 # -*- coding:utf-8 -*- from __future__ import unicode_literals, print_function import sys, os, io, subprocess FILE=os.path.expanduser("~/weiyun_sync/!sync/logseq-note/pages/生词本.md") output = [] text = sys.argv[1].decode('utf8') if sys.version_info.major == 2 else sys.argv[1] lines = [i.strip() for i in text.splitlines() if i.strip()] if len(lines) < 2: exit(0) word = lines[0] if lines[1][0] == '*': output.append('- {}\t{} [[card]]'.format(word, lines[1])) lines = lines[2:] else: output.append('- {}\t [[card]]'.format(word)) lines = lines[1:] output.append('\t- {}'.format(lines[0])) for line in lines[1:]: output.append('\t ' + line) old_words = set() with io.open(FILE, 'r', encoding='utf8') as fp: for line in fp: parts = line.split() if line.startswith('-') and len(parts) > 1: old_words.add(parts[1]) if word not in old_words: with io.open(FILE, 'a', encoding='utf8') as fp: fp.write('\n') fp.write('\n'.join(output)) fp.write('\n') subprocess.check_call(['osascript', '-e', u'display notification "添加 {}" with title "生词本"'.format(word)]) else: subprocess.check_call(['osascript', '-e', u'display notification "跳过 {}" with title "生词本"'.format(word)]) 选择路径保存好 workflow,然后在 键盘 - 快捷键 - 服务 中能看到新建的workflow。 为它设置快捷键 command + shift + alt + 1 Karabiner-Elements Karabiner-Elements 是 macOS 平台的一个重新映射快捷键的软件。 这里我们使用它将“查询单词”和“触发workflow”整合在一起,当然它还支持很多用途,这里就不赘述了。 注意确保Karabiner相关权限,并且设置中下图相关设备是勾选状态 安装好Karabiner-Elements后,打开它的配置文件 路径在 /Users/<用户名>/.config/karabiner/karabiner.json 在 profiles -> complex_modifications -> rules 列表中增加一项配置,内容如下。 然后保存,Karabiner会自动加载新的配置。 这里是将鼠标的侧键(靠前的)映射为查单词的快捷键,实现一键查词。 也可以根据需要更改按键,通过EventViewer可以查看按键代码,配置文件格式可参考官方文档 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 { "description": "Mouse", "manipulators": [ { "from": { "pointing_button": "button5" }, "to": [ { "pointing_button": "button1" }, { "pointing_button": "button1" }, { "key_code": "d", "modifiers": [ "left_command", "left_control" ] }, { "key_code": "1", "modifiers": [ "left_option", "left_shift", "left_command" ] } ], "type": "basic" } ] } 效果 可以看到 Logseq 中卡片生成的效果 参考 https://karabiner-elements.pqrs.org/docs/ https://zhuanlan.zhihu.com/p/433646737 https://hectorguo.com/zh/save-words-in-dictionary/ https://github.com/jjgod/mac-dictionary-kit https://lightcss.com/mac-dictionary/

2022/11/12
articleCard.readMore

关闭子进程打开的文件描述符

我们在测试代码时,由于需要经常重启服务,经常会发现服务端口被占用。 一般kill掉后台进程就ok了,但是如果服务有启动一些常驻的后台程序,可能也会导致端口不能释放。 在类UNIX系统中,一切被打开的文件、端口被抽象为文件描述符(file descriptor) 从python3.4开始,文件描述符默认是non-inheritable,也就是子进程不会共享文件描述符。 问题 一般为了实现多进程、多线程的webserver,服务端口fd必须设置为继承(set_inheritable),这样才能多进程监听一个端口(配合SO_REUSEPORT) 典型的是使用flask的测试服务器的场景,这里我们写一段代码模拟。 1 2 3 4 5 6 import socket, os server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.1', 22222)) server.set_inheritable(True) os.system("python -c 'import time;time.sleep(1000)' ") 我们通过lsof -p {pid}可以看到这两个进程的所有文件描述符 server进程, 可以看到服务端口的fd是4 1 2 3 4 5 6 7 8 9 10 11 COMMAND PID FD TYPE DEVICE SIZE/OFF NODE NAME ptpython 6214 cwd DIR 253,0 4096 872946898 / ... ptpython 6214 0u CHR 136,13 0t0 16 /dev/pts/13 ptpython 6214 1u CHR 136,13 0t0 16 /dev/pts/13 ptpython 6214 2u CHR 136,13 0t0 16 /dev/pts/13 ptpython 6214 3r CHR 1,9 0t0 2057 /dev/urandom ptpython 6214 4u sock 0,7 0t0 58345077 protocol: TCP ptpython 6214 5u a_inode 0,10 0 8627 [eventpoll] ptpython 6214 6u unix 0x0000000000000000 0t0 58368029 socket ptpython 6214 7u unix 0x0000000000000000 0t0 58368030 socket sleep子进程,也拥有fd=4的文件描述符 1 2 3 4 5 6 7 COMMAND PID FD TYPE DEVICE SIZE/OFF NODE NAME python 18022 cwd DIR 253,0 4096 872946898 / ... python 18022 0u CHR 136,13 0t0 16 /dev/pts/13 python 18022 1u CHR 136,13 0t0 16 /dev/pts/13 python 18022 2u CHR 136,13 0t0 16 /dev/pts/13 python 18022 4u sock 0,7 0t0 58345077 protocol: TCP 如果server进程退出时,sleep进程没有退出,fd=4对应的端口就被占用了,服务也就不能正常启动了。 解决方法 手动清理 1 2 3 4 5 6 7 8 import os import time os.system(f'lsof -p {os.getpid()}') os.closerange(3, 100) # 这里假定打开文件描述符不会超过100 time.sleep(5) os.system(f'lsof -p {os.getpid()}') # 后面执行需要的业务代码 使用close_fds 使用subprocess库而不是os来启动子程序, 通过close_fds参数关闭多余的文件描述符 1 2 import subprocess subprocess.call("python -c 'import time;time.sleep(1000)'", shell=True, close_fds=True) 参考 https://docs.python.org/3/library/os.html#inheritance-of-file-descriptors https://docs.python.org/3/library/subprocess.html#subprocess.Popen https://stackoverflow.com/questions/2023608/check-what-files-are-open-in-python#answer-25069136

2022/8/30
articleCard.readMore

容器内进程优雅退出

在使用 docker 时,常常会碰到进程退出时资源清理的问题,比如保证当前请求处理完成,再退出程序。 当执行 docker stop xxx 时,docker会向主进程(pid=1)发送 SIGTERM 信号 如果在一定时间(默认为10s)内进程没有退出,会进一步发送 SIGKILL 直接杀死程序,该信号既不能被捕捉也不能被忽略。 一般的web框架或者rpc框架都集成了 SIGTERM 信号处理程序, 一般不用担心优雅退出的问题。 但是如果你的容器内有多个程序(称为胖容器,一般不推荐),那么就需要做一些操作保证所有程序优雅退出。 signals 信号是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令(即信号)。 应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉。 常见信号: 信号名称信号数描述默认操作 SIGHUP1当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。终止进程 SIGINT2程序终止(interrupt)信号,在用户键入 Ctrl+C 时发出。终止进程 SIGQUIT3和SIGINT类似,但由QUIT字符(通常是Ctrl /)来控制。终止进程并dump core SIGFPE8在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术错误。终止进程并dump core SIGKILL9用来立即结束程序的运行。本信号不能被阻塞,处理和忽略。终止进程 SIGALRM14时钟定时信号,计算的是实际的时间或时钟时间。alarm 函数使用该信号。终止进程 SIGTERM15通常用来要求程序自己正常退出;kill 命令缺省产生这个信号。终止进程 Dockerfile 下面以 supervisor 为例,Dockerfile 如下 1 2 3 4 5 6 7 8 FROM centos:centos7 ENV PYTHONUNBUFFERED=1 TZ=Asia/Shanghai RUN yum -y install epel-release && \ yum -y install supervisor && \ yum -y clean all && rm -rf /var/cache COPY ./ /root/ ENTRYPOINT [ "/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf" ] trap 正常情况,容器退出时supervisor启动的其他程序并不会收到 SIGTERM 信号,导致子程序直接退出了。 这里使用 trap 对程序的异常处理进行包装 1 trap <siginal handler> <signal 1> <signal 2> ... 新建一个初始化脚本,init.sh 1 2 3 4 5 6 7 #!/bin/sh /usr/bin/supervisord -n -c /etc/supervisord.conf & trap "supervisorctl stop all && sleep 3" TERM INT wait 修改 ENTRYPOINT 为如下 1 ENTRYPOINT ["sh", "/root/init.sh"] 参考 https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/ https://www.cnblogs.com/taobataoma/archive/2007/08/30/875743.html https://wangchujiang.com/linux-command/c/trap.html

2022/7/10
articleCard.readMore

Python 循环变量泄露与延迟绑定

循环变量泄露与延迟绑定叠加在一起,会产生一些让人迷惑的结果。 梦开始的地方 先看看一开始的问题,可以看到这里lambda函数的返回值一直在变。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 xx = [] for i in [1,2,3]: xx.append(lambda: i) print('a:', xx[0]()) for j in xx: print(j()) print('b:', xx[0]()) for i in xx: print(i, i()) print('c:', xx[0], xx[0]()) for i in [4, 5, 6]: print(i) print('d:', xx[0], xx[0]()) 输出如下 1 2 3 4 5 6 7 8 9 10 11 12 13 a: 3 3 3 3 b: 3 <function main2.<locals>.<lambda> at 0x10ca30310> <function main2.<locals>.<lambda> at 0x10ca30310> <function main2.<locals>.<lambda> at 0x10ca303a0> <function main2.<locals>.<lambda> at 0x10ca303a0> <function main2.<locals>.<lambda> at 0x10ca30430> <function main2.<locals>.<lambda> at 0x10ca30430> c: <function main2.<locals>.<lambda> at 0x10ca30310> <function main2.<locals>.<lambda> at 0x10ca30430> 4 5 6 d: <function main2.<locals>.<lambda> at 0x10ca30310> 6 循环变量泄露 由于Python没有块级作用域,所以循环会改变当前作用域变量的值,也就是循环变量泄露。 注意:Python3中列表推导式循环变量不会泄露,Python2中和常规循环一样泄露。 1 2 3 4 5 x = -1 for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global') 输出如下 1 2 6 : for x inside loop 6 : x in global 闭包与延迟绑定 再讲一下闭包,在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包。 这里所谓的引用可以也就是内部函数记住了变量的名称(而不是值,这个从ast语法树可以看出),而变量对应的值是会变化的。 如果在循环中定义闭包,引用的变量的值在循环结束才统一确定为最后一次循环时的值,也就是延迟绑定(lazy binding)。 所以下面的例子,xx的所有匿名函数的返回值均为3 1 2 3 xx = [] for i in [1,2,3]: xx.append(lambda: i) 最后 再分析一开始的问题,这里的匿名函数引用了变量i,而i是全局变量,所以再次使用i作为循环变量时,列表中的匿名函数引用的值就被覆盖了。 正确做法: 在独立的函数中定义闭包 闭包引用的变量应该是其他函数不可修改的 优先使用列表推导式 参考 https://stackoverflow.com/questions/3611760/scoping-in-python-for-loops https://www.educative.io/courses/python-ftw-under-the-hood/N8RW8508RkL https://mail.python.org/pipermail/python-ideas/2008-October/002109.html

2022/3/4
articleCard.readMore

bash 语法备忘

bash 语法作为程序员好像都了解一些,但又缺少体系化学习,需要使用到某些功能时又经常手忙脚乱地查。 本文主要参考阮一峰的bash教程,对bash的知识点进行了梳理。 本文目的是作为bash的语法备忘录、语法速查表。 模式扩展 模式扩展(globbing),类似C语言中的宏展开,我们通常使用的通配符*就是其中之一。 Bash 一共提供八种扩展,前4种为文件扩展,只有文件路径确实存在才会扩展。 ~ 波浪线扩展 ? 问号扩展 * 星号扩展 [] 方括号扩展 {} 大括号扩展 $var 变量扩展 $(date) 命令扩展 $((1 + 1)) 算术扩展 波浪线扩展 波浪线~会自动扩展成当前用户的主目录。 ~user表示扩展成用户user的主目录。如果用户不存在,则波浪号扩展不起作用。 1 2 3 4 5 6 7 8 bash-5.1$ echo ~/projects/ /Users/ruan/projects/ bash-5.1$ echo ~root/.ssh /var/root/.ssh bash-5.1$ echo ~aaa/.ssh ~aaa/.ssh 问号扩展 ?字符代表文件路径里面的任意单个字符,不包括空字符。 只有文件确实存在的前提下,才会发生扩展。 1 2 3 4 5 6 7 bash-5.1$ touch {a,b}.txt ab.txt bash-5.1$ ls ?.txt a.txt b.txt bash-5.1$ ls ??.txt ab.txt 星号扩展 *字符代表文件路径里面的任意数量的任意字符,包括零个字符。 1 2 3 4 5 6 7 8 9 bash-5.1$ ls *.txt a.txt ab.txt b.txt bash-5.1$ ls /usr/local/Cellar/*/*/bin/z* /usr/local/Cellar/ffmpeg/4.4_2/bin/zmqsend /usr/local/Cellar/mysql-client/8.0.26/bin/zlib_decompress /usr/local/Cellar/netpbm/10.86.24/bin/zeisstopnm /usr/local/Cellar/perl/5.34.0/bin/zipdetails /usr/local/Cellar/zstd/1.5.0/bin/zstd 方括号扩展 方括号扩展的形式是[...],只有文件确实存在的前提下才会扩展。 [^...]和[!...]。它们表示匹配不在方括号里面的字符 方括号扩展有一个简写形式[start-end],表示匹配一个连续的范围 1 2 3 4 5 6 7 8 bash-5.1$ ls [ab].txt a.txt b.txt bash-5.1$ ls [^b]b.txt ab.txt bash-5.1$ ls [a-b].txt a.txt b.txt 大括号扩展 大括号扩展{...}表示分别扩展成大括号里面的所有值 大括号也可以与其他模式联用,并且总是先于其他模式进行扩展。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bash-5.1$ echo {1,2,3} 1 2 3 bash-5.1$ echo a{1,2,3}b a1b a2b a3b bash-5.1$ echo --exclude={a,b,c} --exclude=a --exclude=b --exclude=c bash-5.1$ echo foo{1,2{1,2}0,3}bar foo1bar foo210bar foo220bar foo3bar bash-5.1$ echo {1..3} 1 2 3 bash-5.1$ echo {1..10..2} 1 3 5 7 9 变量扩展 Bash 将美元符号$开头的词元视为变量,将其扩展成变量值 1 2 bash-5.1$ echo $HOME /Users/ruan 命令扩展 $(...)可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。 1 2 3 4 5 bash-5.1$ echo $(date) 日 11 28 16:22:09 CST 2021 bash-5.1$ echo `date` 日 11 28 16:22:24 CST 2021 算术扩展 $((...))可以扩展成整数运算的结果 1 2 bash-5.1$ echo $((1+1)) 2 引号使用 单引号 单引号用于保留字符的字面含义,在单引号里转义字符和模式扩展都会失效。 1 2 3 4 5 bash-5.1$ ls '[ab].txt' ls: cannot access '[ab].txt': No such file or directory bash-5.1$ ls '*' ls: cannot access '*': No such file or directory 双引号 双引号比单引号宽松,三个特殊字符除外:美元符号($)、反引号(`)和反斜杠(\)。这三个字符,会被 Bash 自动扩展。 也就是说,相比单引号在双引号中变量扩展,命令扩展,算术扩展以及转义字符是有效的。 1 2 3 4 5 6 7 8 9 10 11 bash-5.1$ echo "$((1+1))" 2 bash-5.1$ echo "$HOME" /Users/ruan bash-5.1$ echo "$(date)" 日 11 28 16:35:27 CST 2021 bash-5.1$ echo -e "1\t2" 12 引号嵌套 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 双引号中使用单引号 bash-5.1$ echo '"' " bash-5.1$ echo '"${HOME}"' "${HOME}" # 单引号中使用双引号 bash-5.1$ echo "'${HOME}'" '/Users/ruan' bash-5.1$ echo "'\"${HOME}\"'" '"/Users/ruan"' # 引号嵌套中使用模式扩展,将需要扩展的字符放在单引号中;典型的有json变量填充 bash-5.1$ echo '{"user": "'${USER}'"}' {"user": "ruan"} here doc Here 文档(here document)是一种输入多行字符串的方法,格式如下。 它的格式分成开始标记(<< token)和结束标记(token), 一般用字符串EOF作为token 1 2 3 << token text token 例如 1 2 3 4 5 6 7 8 bash-5.1$ cat << EOF > 11 > 22 > 33 > EOF 11 22 33 here string Here 文档还有一个变体,叫做 Here 字符串(Here string),使用三个小于号(<<<)表示。 它的作用是将字符串通过标准输入,传递给命令。 1 2 3 4 bash-5.1$ cat <<< foobar foobar bash-5.1$ echo foobar | cat foobar 变量 bash 是基于标准输入在不同进程间交互数据的,大部分功能都是在操作字符串,所以变量的默认类型也是字符串。 声明变量和读取变量 声明时等号两边不能有空格。 Bash 变量名区分大小写,HOME和home是两个不同的变量。 1 2 3 bash-5.1$ foo=1 bash-5.1$ echo $foo 1 变量查看和删除 1 2 3 4 5 6 7 8 9 10 11 12 # 查看所有变量, 其中包含父进程export的变量 bash-5.1$ set UID=501 USER=ruan bar=2 foo=1 bash-5.1$ unset foo bash-5.1$ set UID=501 USER=ruan bar=2 变量输出 用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。 如果希望子进程能够读到这个变量,需要使用export命令。 1 2 3 4 bash-5.1$ bash -c set | grep foo bash-5.1$ export foo=1 bash-5.1$ bash -c set | grep foo foo=1 环境变量 平时所说的环境变量,就是init进程export输出的。子进程对变量的修改不会影响父进程,也就是说变量不是共享的。 1 2 3 4 5 # 查看环境变量 bash-5.1$ env SHELL=/bin/zsh LSCOLORS=Gxfxcxdxbxegedabagacad ITERM_PROFILE=Default 下面是一些常见的环境变量。 BASHPID:Bash 进程的进程 ID。 BASHOPTS:当前 Shell 的参数,可以用shopt命令修改。 DISPLAY:图形环境的显示器名字,通常是:0,表示 X Server 的第一个显示器。 EDITOR:默认的文本编辑器。 HOME:用户的主目录。 HOST:当前主机的名称。 IFS:词与词之间的分隔符,默认为空格。 LANG:字符集以及语言编码,比如zh_CN.UTF-8。 PATH:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。 PS1:Shell 提示符。 PS2: 输入多行命令时,次要的 Shell 提示符。 PWD:当前工作目录。 RANDOM:返回一个0到32767之间的随机数。 SHELL:Shell 的名字。 SHELLOPTS:启动当前 Shell 的set命令的参数 TERM:终端类型名,即终端仿真器所用的协议。 UID:当前用户的 ID 编号。 USER:当前用户的用户名。 特殊变量 Bash 提供一些特殊变量。这些变量的值由 Shell 提供,用户不能进行赋值。 $?: 上一个命令的退出码, 0为成功,其他为失败 $$: 当前进程的pid $_: 为上一个命令的最后一个参数 $!: 为最近一个后台执行的异步命令的进程 ID。 $0: bash脚本的参数列表,0是脚本文件路径,1到n是第1到第n个参数 变量默认值 ${varname:-word}: 如果变量varname存在且不为空,则返回它的值,否则返回word ${varname:=word}: 如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word。 ${varname:+word}: 如果变量名存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在。 ${varname:?message}: 如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。 declare 命令 declare命令的主要参数(OPTION)如下。 -a:声明数组变量。 -A:声明关联数组变量。 -f:输出所有函数定义。 -F:输出所有函数名。 -i:声明整数变量。 -p:查看变量信息。 -r:声明只读变量。 -x:该变量输出为环境变量。 数据类型 bash 有字符串,数字,数字,关联数组四种数据类型,默认是字符串,其他类型需要手动声明。 字符串 定义 语法 varname=value 1 2 3 bash-5.1$ s1=abcdefg bash-5.1$ echo $s1 abcdefg 获取长度(length) 语法 ${#varname} 1 2 bash-5.1$ echo ${#s1} 7 子字符串(substr) 语法 ${varname:offset:length}, offset为负数的时候,前面要加空格,防止与默认值语法冲突。 1 2 3 4 5 6 7 bash-5.1$ s1=abcdefg bash-5.1$ echo ${s1:1:3} bcd bash-5.1$ echo ${s1: -6:2} bc bash-5.1$ echo ${s1: -6:3} bcd 替换 (replace) 字符串头部的模式匹配 ${variable#pattern}: 删除最短匹配(非贪婪匹配)的部分,返回剩余部分 ${variable##pattern}: 删除最长匹配(贪婪匹配)的部分,返回剩余部分 匹配模式pattern可以使用*、?、[]等通配符。 1 2 3 4 5 6 7 $ myPath=/home/cam/book/long.file.name $ echo ${myPath#/*/} cam/book/long.file.name $ echo ${myPath##/*/} long.file.name 字符串尾部的模式匹配 ${variable%pattern}: 删除最短匹配(非贪婪匹配)的部分,返回剩余部分 ${variable%%pattern}: 删除最长匹配(贪婪匹配)的部分,返回剩余部分 1 2 3 4 5 6 7 $ path=/home/cam/book/long.file.name $ echo ${path%.*} /home/cam/book/long.file $ echo ${path%%.*} /home/cam/book/long 任意位置的模式匹配 如果匹配pattern则用replace替换匹配的内容 ${variable/pattern/replace}: 替换第一个匹配 ${variable//pattern/replace}: 替换所有匹配 1 2 3 4 5 6 7 $ path=/home/cam/foo/foo.name $ echo ${path/foo/bar} /home/cam/bar/foo.name $ echo ${path//foo/bar} /home/cam/bar/bar.name 数字 使用 declare -i声明整数变量。 1 2 3 4 5 6 7 8 9 10 11 12 # 声明为整数,可以直接计算,不需要使用$符号 bash-5.1$ declare -i val1=12 val2=5 bash-5.1$ echo $val1 12 bash-5.1$ val1+=val2 bash-5.1$ echo $val1 17 # 一个变量声明为整数以后,依然可以被改写为字符串。Bash 不会报错,但会赋以不确定的值 bash-5.1$ val1=aaa bash-5.1$ echo $val1 0 数值的进制 Bash 的数值默认都是十进制,但是在算术表达式中,也可以使用其他进制。 number:没有任何特殊表示法的数字是十进制数(以10为底)。 0number:八进制数。 0xnumber:十六进制数。 base#number:base进制的数。 1 2 3 4 5 6 7 8 9 10 11 bash-5.1$ declare -i a=0x77 bash-5.1$ echo $a 119 bash-5.1$ declare -i a=0xfe bash-5.1$ echo $a 254 bash-5.1$ declare -i a=2#111 bash-5.1$ echo $a 7 算术表达式 ((...))语法可以进行整数的算术运算。 支持的算术运算符如下。 +:加法 -:减法 *:乘法 /:除法(整除) %:余数 **:指数 ++:自增运算(前缀或后缀) --:自减运算(前缀或后缀) 如果要读取算术运算的结果,需要在((...))前面加上美元符号$((...)),使其变成算术表达式,返回算术运算的值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bash-5.1$ echo $((1+1)) 2 bash-5.1$ echo $((1-1)) 0 bash-5.1$ echo $((1*2)) 2 bash-5.1$ echo $((1/2)) 0 bash-5.1$ echo $((5%2)) 1 bash-5.1$ a=1 bash-5.1$ echo $((a++)) 1 bash-5.1$ echo $((a++)) 2 bash-5.1$ echo $((++a)) 4 数组 创建数组 array=(item1 item2) 语法可初始化数组,括号内可以换行,多行初始化可以用#注释。 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 # 直接初始化数组 bash-5.1$ a=(1 2 3) bash-5.1$ echo ${a[@]} 1 2 3 # 多行初始化数组 bash-5.1$ a=( > 1 > 2 > 3 > #4 > ) bash-5.1$ echo ${a[@]} 1 2 3 # 模式扩展初始化 bash-5.1$ a=({1..3}) bash-5.1$ echo ${a[@]} 1 2 3 # declare -a命令声明一个数组,也是可以的。 bash-5.1$ declare -a b bash-5.1$ b+=(1) bash-5.1$ echo ${b[@]} 1 #read -a命令则是将用户的命令行输入,存入一个数组。 bash-5.1$ read -a c 11 22 33 bash-5.1$ echo ${c[@]} 11 22 33 访问数组元素 array[index] 语法可访问数组元素,不带index访问则是访问数组首个元素。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bash-5.1$ a=(1 2 3) # 查看元素 bash-5.1$ echo ${a[1]} 2 # 元素赋值 bash-5.1$ a[1]+=1 bash-5.1$ echo ${a[1]} 21 bash-5.1$ a[1]=22 bash-5.1$ echo ${a[1]} 22 # bash-5.1$ a[0]=1111 bash-5.1$ echo ${a} 1111 bash-5.1$ echo ${a[0]} 1111 数组长度 ${#array[@]} 和 ${#array[*]} 可访问获得数组长度 1 2 3 4 5 bash-5.1$ a=(1 2 3) bash-5.1$ echo ${#a[*]} 3 bash-5.1$ echo ${#a[@]} 3 获取非空元素下标 ${!array[@]} 或 ${!array[*]}, 可以获得非空元素的下标 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bash-5.1$ a=(1 2 3) bash-5.1$ a[6]=6 bash-5.1$ echo ${a[0]} 1 bash-5.1$ echo ${a[@]} 1 2 3 6 bash-5.1$ echo ${a[3]} bash-5.1$ echo ${a[4]} bash-5.1$ echo ${!a[@]} 0 1 2 6 # 注意此时数组长度为4,并不是7 bash-5.1$ echo ${#a[@]} 4 数组元素切片 ${array[@]:position:length}的语法可以提取数组成员。 1 2 3 4 5 bash-5.1$ a=({1..10}) bash-5.1$ echo ${a[@]} 1 2 3 4 5 6 7 8 9 10 bash-5.1$ echo ${a[@]:1:3} 2 3 4 数组追加元素 数组末尾追加元素,可以使用+=赋值运算符。 1 2 3 4 5 6 bash-5.1$ a=({1..10}) bash-5.1$ echo ${a[@]} 1 2 3 4 5 6 7 8 9 10 bash-5.1$ a+=(11 12) bash-5.1$ echo ${a[@]} 1 2 3 4 5 6 7 8 9 10 11 12 删除元素 删除一个数组成员,使用unset命令。 1 2 3 4 5 6 7 8 bash-5.1$ a=(1 2 3) # 删除单个元素 bash-5.1$ unset a[1] bash-5.1$ echo ${a[@]} 1 3 # 删除整个数组 bash-5.1$ unset a bash-5.1$ echo ${a[@]} 关联数组 declare -A可以声明关联数组,关联数组使用字符串而不是整数作为数组索引。 除了初始化外,使用方法和数组基本相同 1 2 3 4 5 6 7 bash-5.1$ declare -A a bash-5.1$ a['red']=1 bash-5.1$ a['blue']=2 bash-5.1$ echo ${a[@]} 2 1 bash-5.1$ echo ${a[@]:0:1} 2 控制流 注释 #表示注释,每行从#开始之后的内容代表注释,会被bash忽略. 1 2 bash-5.1$ echo 1111 # 222 1111 条件判断 bash 和常规编程语言一样使用if作为分支条件的关键字, fi作为结束的关键字,else和 elif子句是可选的 其中if和elif的condition所判断的内容是命令的状态码是否为0,为0则执行关联的语句。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 因为bash中分号(;)和换行是等价的,所以有下面两种风格,其他多行语句也是类似的 # 本人偏好风格1 # 风格1 if condition; then command elif condition; then command else command fi # 风格2 if condition then command elif condition then command else command fi 这里的condition可以是多个命令,如command1 && command2,或者command1 || command2,则if判断的是这两个命令的状态码的逻辑计算结果。 condition也是可以是command1; command2, 则则if判断的是最后一个命令的状态码。 这里最常用的condition是test命令, 也就是[[]]和[]. test是bash的内置命令,会执行给定的表达式,结果为真满足则返回状态码0, 否则返回状态码1. 下文循环语言的condition也是相同的,就不赘述了 1 2 3 4 5 6 bash-5.1$ test 1 -eq 1 bash-5.1$ echo $? 0 bash-5.1$ test 1 -eq 2 bash-5.1$ echo $? 1 [[]]和[]的区别是[[]]内部支持&&,||逻辑判断,所以以下三种写法是等价的。 由于[和]是命令, 所以两侧一定要有空格,也是就是[ 1 -eq 1 ],否则bash会认为命令找不到。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # test if test 1 -eq 2 || test 1 -eq 1; then echo True fi # [ ] if [ 1 -eq 2 ] || [ 1 -eq 1 ]; then echo True fi # [[ ]] if [[ 1 -eq 2 || 1 -eq 1 ]]; then echo True fi 逻辑操作符 判断条件支持且(&&)或(||)非(!) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # not if [[ ! 'aa' == 'bb' ]]; then echo True fi # or if [[ 1 -eq 2 || 1 -eq 1 ]]; then echo True fi # and if [[ 1 -ne 2 && 1 -eq 1 ]]; then echo True fi 判断时引号使用(quote) 使用[和test时,变量引用注意加双引号,否则得不到正确的结果,[[则不需要。 1 2 3 4 5 6 7 8 bash-3.2$ echo "$SSH_CLIENT" bash-3.2$ if [ -n $SSH_CLIENT ]; then echo 1; else echo 0; fi 1 bash-3.2$ if [ -n "$SSH_CLIENT" ]; then echo 1; else echo 0; fi 0 bash-3.2$ if [[ -n $SSH_CLIENT ]]; then echo 1; else echo 0; fi 0 字符串判断 bash默认数据类型为字符串,所以常见的 >, <是用于字符串判断。 注意:字符串判断不支持>=和<=, 得使用逻辑组合来替代 -z string:字符串串长度为0 -n string: 字符串长度大于0 string1 == string2: string1 等于 string2 string1 = string2: string1 等于 string2 string1 > string2: 如果按照字典顺序string1排列在string2之后 string1 < string2: 如果按照字典顺序string1排列在string2之前 数字(整数)判断 下面的表达式用于判断整数。 [ integer1 -eq integer2 ]:如果integer1等于integer2,则为true。 [ integer1 -ne integer2 ]:如果integer1不等于integer2,则为true。 [ integer1 -le integer2 ]:如果integer1小于或等于integer2,则为true。 [ integer1 -lt integer2 ]:如果integer1小于integer2,则为true。 [ integer1 -ge integer2 ]:如果integer1大于或等于integer2,则为true。 [ integer1 -gt integer2 ]:如果integer1大于integer2,则为true。 文件判断 以下表达式用来判断文件状态。仅列举常用判断,详细支持列表参考 https://tldp.org/LDP/abs/html/fto.html [ -a file ]:如果 file 存在,则为true。 [ -d file ]:如果 file 存在并且是一个目录,则为true。 [ -e file ]:如果 file 存在,则为true, 同-a。 [ -f file ]:如果 file 存在并且是一个普通文件,则为true。 [ -h file ]:如果 file 存在并且是符号链接,则为true。 [ -L file ]:如果 file 存在并且是符号链接,则为true, 同-h。 [ -p file ]:如果 file 存在并且是一个命名管道,则为true。 [ -r file ]:如果 file 存在并且可读(当前用户有可读权限),则为true。 [ -s file ]:如果 file 存在且其长度大于零,则为true。 [ -w file ]:如果 file 存在并且可写(当前用户拥有可写权限),则为true。 [ -x file ]:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true。 switch case bash也支持,switch case,语法如下。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 case EXPRESSION in PATTERN_1) STATEMENTS ;; PATTERN_2) STATEMENTS ;; PATTERN_N) STATEMENTS ;; *) STATEMENTS ;; esac 例如 1 2 3 4 5 6 7 8 9 10 11 a=2 case $a in 1) echo 11 ;; 2) echo 22 ;; *) ;; esac 循环 while 循环 while循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。 condition与if语句的相同,就不赘述了。 1 2 3 while condition; do command done unitl 循环 until循环与while循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。 1 2 3 until condition; do command done for-in 循环 for...in循环用于遍历列表的每一项。 1 2 3 for variable in list; do commands done 常见的几种用法 1 2 3 4 5 6 7 8 9 10 11 12 for i in 1 2 3; do echo $i done for i in {1..3}; do echo $i done list=(1 2 3) for i in ${list[@]}; do echo $i done for 循环 for循环还支持 C 语言的循环语法。 1 2 3 for (( expression1; expression2; expression3 )); do commands done 上面代码中,expression1用来初始化循环条件,expression2用来决定循环结束的条件,expression3在每次循环迭代的末尾执行,用于更新值。 注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$。 例如 1 2 3 for ((i=1; i<=3; i++)); do echo $i done 跳出循环 Bash 提供了两个内部命令break和continue,用来在循环内部跳出循环。 break命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。 continue命令立即终止本轮循环,开始执行下一轮循环。 函数 函数定义 Bash 函数定义的语法有两种,其中fn为定义的函数名称。 1 2 3 4 5 6 7 8 9 # 第一种 fn() { # codes } # 第二种 function fn() { # codes } 函数参数 函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。 ${N}:函数的第一个到第N个的参数。 $0:函数所在的脚本名。 $#:函数的参数总数。 $@:函数的全部参数,参数之间使用空格分隔。 $*:函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。 函数调用 funcname arg1 arg ... argN 的语法进行函数调用。主要函数的返回值和输出值(标准输出)的区别,这和主流编程语言不同 1 2 3 4 5 6 7 8 9 10 11 add() { declare -i res res=0 for i in $@; do res+=$i done echo $res } # 结果为10 add 1 2 3 4 函数返回值 return命令用于从函数返回一个值。返回值和命令的状态码一样,可以用$?拿到值。 return也可以不接具体的值,则返回值是return命令的上一条命令的状态码。 如果不加return,则返回值是函数体最后一条命令的状态码。 1 2 3 function func_return_value { return 10 } 关键概念 shebang Shebang(也称为Hashbang)是一个由井号和叹号构成的字符序列#!, 其出现在可执行文本文件的第一行的前两个字符。 在文件中存在Shebang的情况下,类Unix操作系统的程序加载器会分析Shebang后的内容,将这些内容作为解释器指令,并调用该指令. 例如,shell脚本 1 2 3 #!/bin/bash echo Hello, world! python 脚本 1 2 3 #!/usr/bin/env python -u print("Hello, world!") 状态码 每个命令都会返回一个退出状态码(有时候也被称为返回状态)。 成功的命令返回 0,不成功的命令返回非零值,非零值通常都被解释成一个错误码。行为良好的 UNIX 命令、程序和工具都会返回 0 作为退出码来表示成功,虽然偶尔也会有例外。 状态码一般是程序的main函数的返回码,如c,c++。 如果是bash脚本,状态码的值则是 exit 命令的参数值。 当脚本以不带参数的 exit 命令来结束时,脚本的退出状态码就由脚本中最后执行的命令来决定,这与函数的 return 行为是一致的。 特殊变量$?可以查看上个命令的退出状态码 文件描述符 文件描述符在形式上是一个非负整数。指向内核为每一个进程所维护的该进程打开文件的记录表。 当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。 标准输入输出 每个Unix进程(除了可能的守护进程)应均有三个标准的POSIX文件描述符,对应于三个标准流: 0:标准输入 1:标准输出 2:错误输出 打开新的文件描述符 手动指定描述符 1 2 3 exec 3<> /tmp/foo #open fd 3. echo "test" >&3 exec 3>&- #close fd 3. 系统自动分配描述符,bash4.1开始支持(在macos报错,原因不明) 1 2 3 4 5 6 7 8 #!/bin/bash FILENAME=abc.txt exec {FD}<>"$FILENAME" echo 11 >&FD echo 22 >&FD $FD>&- 描述符重定向 command > file: 将输出重定向到 file。 command < file: 将输入重定向到 file。 command >> file: 将输出以追加的方式重定向到 file。 n > file: 将文件描述符为 n 的文件重定向到 file。 n >> file: 将文件描述符为 n 的文件以追加的方式重定向到 file。 n >& m: 将输出文件 m 和 n 合并。 n <& m: 将输入文件 m 和 n 合并。 所以命令中常见的ls -al > output.txt 2>&1, 就是将标准输出和错误输出都重定向到一个文件。 等价于ls -al &>output.txt,本人偏好这种写法,比较简洁。 IFS (Input Field Separators) IFS决定了bash在处理字符串的时候是如何进行单词切分。 IFS的默认值是空格,TAB,换行符,即\t\n 1 2 3 $ echo "$IFS" | cat -et ^I$ $ 例如,在for循环的时候,如何区分每个item 1 2 3 for i in `echo -e "foo bar\tfoobar\nfoofoo"`; do echo "'$i' is the substring"; done 也可以自定义 1 2 3 4 5 6 7 OLD_IFS="$IFS" IFS=":" string="1:2:3" for i in $string; do echo "'$i' is the substring"; done IFS=$OLD_IFS 任务管理 linux进程分前台(fg)和后台(bg)。 在命令的末尾添加&可以将命令后台执行,一般配合输出重定向使用。 jobs可以查看当前bash进程的子进程,并通过fg和bg进行前台和后台切换。 %1代表后台的第一个进程,以此类推%N代表第n个. control + Z可以将当前前台程序暂停,配合bg可以将其转后台。 wait [pid]可以等待子进程结束,如果不带pid参数则等待所有子进程结束。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 bash-5.1$ sleep 100 ^Z [1]+ 已停止 sleep 100 bash-5.1$ jobs [1]+ 已停止 sleep 100 bash-5.1$ bg %1 [1]+ sleep 100 & bash-5.1$ sleep 200 & [2] 63603 bash-5.1$ jobs [1]- 运行中 sleep 100 & [2]+ 运行中 sleep 200 & bash-5.1$ wait %1 [1]- 已完成 sleep 100 后台进程并发控制 可以利用jobs对后台进程并发数目进行控制 1 2 3 4 5 6 7 8 for i in {1..30}; do sleep $((30+i)) & if [[ $(jobs | wc -l ) -gt 10 ]]; then jobs wait fi done wait 参考 https://wangdoc.com/bash/ https://tldp.org/LDP/abs/html/fto.html https://zh.wikipedia.org/wiki/Shebang https://en.wikipedia.org/wiki/File_descriptor

2021/11/28
articleCard.readMore

MySQL 自定义数据库路径

最近的一些文章是整理以前的笔记 MySQL 是最常用的数据,有时希望将数据库文件存放在自定义路径,或者在系统中启动多个 MySQL服务。 当然,如果条件允许,建议直接使用 docker 创建 my.cnf 配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [mysqld] datadir=/home/ruan/data/mysql_data socket=/home/ruan/data/mysql.sock user=ruan # Disabling symbolic-links is recommended to prevent assorted security risks symbolic-links=0 bind-address=127.0.0.1 port = 12345 character-set-server=utf8 collation-server=utf8_general_ci [mysqld_safe] log-error=/home/ruan/data/mysqld.log pid-file=/home/ruan/data/mysqld.pid 启动和初始化 1 2 3 4 # 启动MySQL mysqld_safe --defaults-file=my.cnf --user=ruan # 初始化数据库 mysql_install_db --defaults-file=my.cnf --user=ruan 目录结构 1 2 3 4 5 6 7 8 9 10 $ tree data data ├── my.cnf ├── mysql_data │   ├── ibdata1 │   ├── ib_logfile0 │   ├── ib_logfile1 ├── mysqld.log ├── mysqld.pid └── mysql.sock 设置密码 1 mysql> use mysql; update user set password=password('m654321') where user='root'; flush privileges;

2021/10/14
articleCard.readMore

hexo 站内搜索内容不完全问题修复

在使用 Hexo 的站内搜索时,发现搜索的内容不全。单步调试发现xml解析不完整,有部分内容被截断了。 在浏览器中打开/search.xml发现以下错误。显然xml中有非法字符,xml解析产生了错误。 将search.xml文件保存,并用python打开,找到具体出错的位置。 utf8解码之后可以发现\x10非法字符,将其删除,重新生成文章问题解决。 1 2 3 4 5 6 7 >>> xxx = open('./tmp.xml', 'rb').read() >>> xxx.index(b'\x10\xE7\x84\xB6') 923278 >>> xxx[923278:923278+31] b'\x10\xe7\x84\xb6\xe5\x88\x99\xef\xbc\x8c\xe4\xbb\x8a\xe4\xb9\x8b\xe4\xb8\x96\xe4\xba\xba\xef\xbc\x8c\xe7\xb1\xbb\xe6\xad\xa4' >>> xxx[923278:923278+31].decode() '\x10然则,今之世人,类此'

2021/10/11
articleCard.readMore

macOS 使用技巧

对程序员来说,macOS 就是一个桌面支持比较好的 Linux/Unix,给日常开发带来了许多便利 本文记录一些日常使用中的小技巧。 macOS mojave 密码位数限制 苹果在10.14的系统上增加了密码最小长度的限制 可以通过这个命令去除 1 pwpolicy -clearaccountpolicies 注意:去除限制之后,通过time machine备份的系统,还原到有限制的机器上,可能会导致一些bug。如卡死在登陆页面,部分应用的资源库损坏等等。 重置账户、配置 如果出现上面的问题,可以尝试重置下系统设置(选择任意一种,建议选1或2) 新建管理员账号,在新机器上去除密码限制。 进入恢复模式(开机按command+r),删除对应用户账户配置文件(恢复模式下文件挂载路径可能不同), 重新开机 1 2 mv ~/Library/Preferences ~/Library/Preferences.bak mv ~/Library/PreferencePanes ~/Library/PreferencePanes.bak 清除机器配置状态,重新设置一个新账户(开机按住command+s;) 1 2 3 /sbin/mount -uaw rm var/db/.applesetupdone reboot Mac 开启任何来源选项 1 sudo spctl --master-disable 让系统更流畅 macOS 10.14 以后系统动画越来越复杂,对显卡要求比较高,导致配置比较老的设备运行起来很卡顿。 特别是big sur系统,使用intel核显的机器都不建议安装。 通过关闭一些动画和特效可以让系统更流畅些 键位修改 使用 karabiner 可以对系统全局快捷键和应用快捷键进行修改。 通过配置 karabiner 将 command,control,option 按键进行重映射,可以让 macOS 的键位 和 Windows/Linux 接近。 使 macOS 的命令风格与Linux相同 macOS 的命令应该是 Unix 风格的,和 Linux 有些不同,可选参数必须放在前面,会有些不方便。 比如 1 2 3 # mac ➜ ~ /bin/ls /tmp -al ls: -al: No such file or directory 可以安装 gnu 工具,覆盖这些命令 1 2 3 brew install coreutils findutils gnu-tar htop export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH" export MANPATH="/usr/local/opt/coreutils/libexec/gnuman:$MANPATH" 清除一些 macOS 系统默认快捷键 创建文件 ~/Library/KeyBindings/DefaultKeyBinding.dict 这里是清除了默认的 Control + Command + 方向键的行为 修改完成后必须重新打开现有app才能生效 1 2 3 4 5 { "^@\UF701" = ("noop:"); "^@\UF702" = ("noop:"); "^@\UF703" = ("noop:"); } 字符含义 PrefixMeaning ~⌥ Option key $⇧ Shift key ^^ Control key @⌘ Command key #keys on number pad 参考: http://xahlee.info/kbd/osx_keybinding.html https://blog.victormendonca.com/2020/04/27/how-to-change-macos-key-bindings/ 关闭启动声音 1 2 3 4 5 # 关闭提示音 sudo nvram StartupMute=%01 # 打开duang sudo nvram StartupMute=%00 crontab 文件路径 用户执行crontab -e之后定时任务存储位置/private/var/at/tabs/<username> hostname 更改 macos 默认没有设置hostname,导致终端PS1显示时会用网卡的mac地址作为主机名,看起来比较别扭。 可以通过sudo scutil --set HostName <name>设置想要的主机名 1 2 3 4 5 6 7 8 ➜ ~ hostname a8a159010000 ➜ ~ scutil --get HostName HostName: not set ➜ ~ sudo scutil --set HostName Mac-mini Password: ➜ ~ hostname ruandeMac-mini catalina 禁用系统更新提示 在终端中输入 1 defaults write com.apple.systempreferences AttentionPrefBundleIDs 0 ; killall Dock 在 /etc/hosts 中加入 1 2 3 # disable mac update 0.0.0.0 swdist.apple.com.edgekey.net 0.0.0.0 swdist.apple.com.akadns.net 固定dock位置,防止在不同屏幕间移动 不是100%有效 1 defaults write com.apple.Dock position-immutable -bool yes; killall Dock 不生成 ._ 开头的隐藏文件 1 defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true

2021/10/10
articleCard.readMore

MySQL 表分区使用

使用MySQL数据库时,当表的数据条数比较大时(1000w以上),数据查询会很慢,索引的效果也不好。 这时我们可以把表的数据分区存储,安装数据值的前缀或者时间字段来分区。 建表 1 2 3 4 5 6 7 8 9 CREATE TABLE test_part ( appid int(11), val int(11), username VARCHAR(25) NOT NULL, start_time DATETIME ) PARTITION BY RANGE (TO_DAYS(start_time) )( PARTITION p20190305 VALUES LESS THAN (TO_DAYS('2019-03-06 00:00:00') ) ) 删除分区 alter table test_part drop partition p1; 不可以删除hash或者key分区。 一次性删除多个分区,alter table test_part drop partition p1,p2; 增加分区 ALTER TABLE test_part ADD partition (partition p20190306 VALUES LESS THAN (TO_DAYS(‘2019-03-07 00:00:00’))); 生成测试数据 创建储存过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 DROP PROCEDURE IF EXISTS proc1; DELIMITER $$ SET AUTOCOMMIT = 0$$ CREATE PROCEDURE proc1() BEGIN DECLARE v_cnt DECIMAL (10) DEFAULT 0 ; dd:LOOP INSERT INTO test_part VALUES ( FLOOR(RAND()*100), FLOOR(RAND()*1000), UUID(), DATE_ADD('2019-03-04 00:00:00', INTERVAL FLOOR(v_cnt / 5000) MINUTE) ); COMMIT; SET v_cnt = v_cnt+1 ; IF v_cnt = 10000000 THEN LEAVE dd; END IF; END LOOP dd ; END;$$ DELIMITER ; ``` 调用储存过程 call proc1; `

2021/10/10
articleCard.readMore

Windows 网络共享

要在不暴露 client 的情况下共享网络,一般就只能使用 nat(Network Address Translation), linux 下可以使用 iptables 很轻松地搞定。 nat 包含 DNAT 和 SNAT, 要想双向互通,必须两者都实现。 windows下的网络共享只有SNAT那一部分,比如各自免费wifi软件。少了DNAT,外部网络就无法访问内部。 还好windows下可以配置端口转发,实现等效的DNAT 先配置网络共享 前置要求,需要两张网卡,无线或者有线均可。 A: 用于访问外网 B: 共享网络接入点 打开:控制面板 》网络和Internet 》网络和共享中心 》更改适配器设置 右键A网卡 > 属性 > 共享 > 勾选允许 (win10可能有下拉选择,下拉选中B网卡) 端口映射配置 netsh是Windows自带的端口转发/端口映射工具。 支持IPv4和IPv6,命令即时生效,重启系统后配置仍然存在。 常用命令 add - 在一个表格中添加一个配置项。 delete - 从一个表格中删除一个配置项。 dump - 显示一个配置脚本。 help - 显示命令列表。 reset - 重置端口代理配置状态。 set - 设置配置信息。 show - 显示信息。 用法(以v4tov4为例) 1 2 3 4 5 add v4tov4 [listenport=]integer>|servicename> \ [connectaddress=]IPv4 address>|hostname> \ [[connectport=]integer>|servicename>] \ [[listenaddress=]IPv4 address>|hostname>] \ [[protocol=]tcp] 参数说明 listenport - IPv4 侦听端口。 connectaddress - IPv4 连接地址。 connectport - IPv4 连接端口。 listenaddress - IPv4 侦听地址。 protocol - 使用的协议。现在只支持 TCP 案例(ssh端口转发) 将192.168.8.108的22端口映射到本地的2222端口 这样外部就可以通过本地的对外ip来ssh访问192.168.8.108了 1 netsh interface portproxy add v4tov4 listenport=2222 connectaddress=192.168.8.108 connectport=22 显示端口转发 一般情况下使用下列命令进行查看 1 netsh interface portproxy show all

2021/10/10
articleCard.readMore

Docker mtu 引发的加班血案

最近在搞 torch 的工程化,基于 brpc 和 libtorch,将两者编译在一起的过程也是坑深,容下次再表。 为了简化部署,brpc 服务在 Docker 容器中运行。本地测试时功能一切正常,上到预发布环境时请求全部超时。 由于业务代码,brpc,docker环境,机房都是新的,在排查问题的过程中简直一头雾水。(当然根本原因还是水平不足) 使尽浑身解数定位 发现请求超时后,开始用CURL测试接口,用真实数据验证发现请求都耗时1s,这和用c++的预期完全不符。 我代码不应该有bug!! 首先是怀疑业务代码有问题,逐行统计业务代码耗时,发现业务代码仅耗时10+ms。 curl 不应该 Expect 用空数据访问接口,发现耗时也只有20+ms,这时开始怀疑brpc是不是编译得有问题,或者说和libtorch编译到一起不兼容。 这时我请教了一位同事,他对brpc比较熟悉,然后他说是curl实现的问题,和brpc没关系, 参考 brpc issue 1 2 curl传输的时候,会设置 Expect: 100-continue, 这个协议brpc本身没有实现, 所以curl会等待一个超时。 加上 -H 'Expect:' 可以解决这个问题 所以这个1s超时是个烟雾弹,线上client是Python,不会有这个问题。 难道是 lvs ? 接着又是一通疯狂测试,各种角度体位测试。发现本机测试是ok的,透过lvs请求(跨机房)就会卡住直到超时,而且小body请求一切正常,大body请求卡住。 这时又开始怀疑brpc编译的不对,导致这个超时(brpc编译过程比较曲折,导致我不太有信心)。 于是我在这个服务器Docker中运行了另一个brpc服务,发现是一样的问题。 为了确认是否是brpc的问题,又写了个Python 的 echo server 进行测试,发现在docker中是一样的问题,但是不在docker中运行有一切正常。 这是时就可以确定,与代码无关,是docker或者lvs的问题,一度陷入僵局。 docker 才是罪魁祸首 完全没思路,于是找来了运维的同学,这时运维提了一下,这个机房的mtu会小一点,于是一切串起来了。 马上测试,发现机房的mtu只有1450,而docker0网桥的默认mtu是1500,这就很能解释小body没问题,大body卡死。 1 2 3 4 5 6 7 ~# netstat -i Kernel Interface table Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg docker0 1500 0 0 0 0 0 0 0 0 BMU eth0 1450 1023246 0 0 0 110268 0 0 0 BMRU lo 65536 1967 0 0 0 1967 0 0 0 LRU wlan0 1500 0 0 0 0 0 0 0 0 BMU 修改docker0的mtu,重启docker.service,一切问题都解决了。 1 2 3 4 5 6 cat << EOF > /etc/docker/daemon.json { "mtu": 1450 } EOF 事后诸葛 找到问题等于解决了一半 这句话说得并不太懂,应该是“找到问题等于解决了90%”。 在互联网时代,找到了具体问题,在一通Google,基本等于解决了问题。你始终要确信,这个问题不应该只有你遇到。 在这个例子上,curl的误导大概花了一半的时间去定位。所以,定位问题首先得明确问题,如医生看病一样,确认问题发生的现象(卡住),位置(docker容器中)、程度(永久)、触发原因(大请求body)。 要善于运用他人的经验和知识 找专业人士寻求帮助是非常高效的,会大大缩短定位问题的时间,因为他们会运用经验和知识快速排除错误选项。 知识可能会误导你 你所拥有的知识,并不是究竟的知识,也就是它所能应用的范围并不适应当前的场景,还有可能误导你。 你所缺失的知识,让你看不清前方正确的道路。 按我的知识,docker0是网桥,等价于交换机,除了性能问题,不应该导致丢包,就一直没往这个方向考虑(当然这块的知识也不扎实)。 MTU也不应该导致丢包,交换机应该会进行IP分片。 但忽略了一个点,虚拟的网桥并不是硬件网桥,可能并没有实现IP分片的逻辑(仅丢弃),又或者没有实现 PMTU(Path MTU Discovery)。 上面这点存疑,但更直接的原因是服务的IP包的 Don't fragment flag 为1,也就是禁止分片(为什么设置还不清楚)。 参考 https://github.com/apache/incubator-brpc/issues/1075 云网络丢包故障定位全景指南 https://mlohr.com/docker-mtu/ https://github.com/docker/for-win/issues/1144 https://www.cnblogs.com/sammyliu/p/5079898.html https://github.com/moby/moby/issues/12565

2021/9/21
articleCard.readMore

Hyper-V 中安装 openmediavault(OMV) 实现完美 NAS 文件共享

一直在寻找适合自己的 NAS 存储方案,也做过一些尝试,可总有些不完美的点。 最近终于找到一个接近完美的方案:Hyper-V + NFS + openmediavault 思路 我对 NAS 几点硬性要求 能支持 NTFS 文件系统,否则现有数据迁移成本很高 文件共享必须支持回收站,否则数据易丢失 文件检索要方便 所以之前一直用 windows 作为 NAS 系统,最大的问题是不支持共享文件夹的回收站。 现有 NAS 方案的对比 goodbad windows smb性能无法扩展功能(如smb回收站,timemachine) wsl1 samba性能略低于smb,功能可扩展部分共享文件无法打开, 配置略繁琐 wsl2 samba(omv)功能可扩展无法解决桥接网络,需要更好的cpu nfs性能最强无法扩展功能 群晖 / OMV / FreeNAS功能多,功能可扩展无法管理底层文件 windows + nfs + vmware(群晖)可管理底层文件,功能可扩展物理机休眠虚拟机无法唤醒 windows + nfs + hyper-v(omv)功能可扩展,性能接近smb,可休眠需要更好的cpu,hyperv使用略繁琐 本方案的思路受之前文章的启发 在WSL2中安装openmediavault(OMV) WSL2 的便利之处在于让 linux 系统能以一个足够快的速度访问 windows 文件,据了解是使用 9p(Plan 9 9p remote filesystem protocol) 协议进行文件共享。 那么我们只要在 Hyper-V 上运行虚拟机,并通过某种方式(这里选择NFS)共享文件到虚拟机里,就能绕开 WSL2 的缺点。 实现 NAS 的底层文件系统由 windows 管理, 上层文件共享等应用由虚拟机中的 NAS 系统处理,兼顾了稳定性和扩展性。 方案 软件环境选择 windows 10 21H1 haneWIN NFS server 1.2.59 openmediavault Windows 10 安装及配置 设置开机自动登录用户(可选): https://zhuanlan.zhihu.com/p/61262940 禁止自动从睡眠中唤醒(可选): 控制面板 -> 电源选项 -> 更改计划设置 -> 更改高级计划设置 -> 睡眠 -> 允许使用睡眠唤醒定时器(禁用) 启用 Hyper-V:DISM /Online /Enable-Feature /All /FeatureName:Microsoft-Hyper-V Hyper-V 配置桥接网络:虚拟交换机管理器 -> 新建虚拟网络交换机(外部) 安装 haneWIN NFS server 并配置需要共享的硬盘 openmediavault 安装及配置 创建 Hyper-V 虚拟机:选择第一代,选择桥接网卡,虚拟硬盘需挂载到 IDE 控制器 挂载安装镜像,安装 openmediavault 系统 安装 omv-extras 1 wget -O - https://github.com/OpenMediaVault-Plugin-Developers/packages/raw/master/install | bash 安装插件:openmediavault-sharerootfs(共享系统分区的文件夹) openmediavault-remotemount(远程挂载) 挂载 NFS 共享:存储器 -> 远程挂载 建立共享文件夹: 选择系统分区上的 NFS 挂载文件夹 启用smb共享 关闭虚拟机,并配置虚拟机的网卡为静态MAC地址(如果是导入的虚拟机,注意检查MAC地址是否合法) 其他优化配置 samba 性能优化 配置页面:服务 -> SMB/CIFS -> 设置 -> 高级设置 -> 扩展选项 1 2 3 4 5 6 7 8 9 10 11 12 veto files = /.Trashes/$RECYCLE.BIN/System Volume Information/.recycle/ socket options = TCP_NODELAY IPTOS_LOWDELAY SO_KEEPALIVE SO_RCVBUF=98304 SO_SNDBUF=98304 dead time = 30 getwd cache = yes min protocol = SMB2 #max protocol = SMB2 aio read size = 40960 aio write size = 40960 write cache size = 262144 large readwrite = yes fake oplocks = yes oplocks = no samba 回收站优化 默认的回收站不能按天对文件进行分类,不方便进行清理 配置页面:服务 -> SMB/CIFS -> 共享 -> 共享文件夹 启用回收站选项,并在扩展选项中增加 1 recycle:repository = .recycle/%U/today 配置页面:系统 -> 计划任务 增加共享文件夹回收站整理脚本 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 #!/bin/bash set -x -e BASE=/srv/nfs_root TODAY=$(date +'%Y%m%d') USERS=( data ) # read -r -a USERS <<< $(groupmems -g users -l) cd $BASE for share in $(ls); do for user in ${USERS[@]}; do rec="$BASE/${share}/.recycle/$user" if [[ ! -d $rec ]]; then mkdir -p $rec fi cd $rec if [[ ! -d $TODAY ]]; then mkdir $TODAY fi if [[ -L today || -f today ]]; then rm -f today elif [[ -d today ]]; then mv today $TODAY/$(date +'bak.%s') fi ln -s ./$TODAY today done done windows 系统中,增加定时任务,删除NFS共享目录中的回收站文件到系统回收站 Python 代码如下,超过一定时间的文件会被送到 windows 回收站 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 import time import os import logging from datetime import datetime, timedelta from pathlib import Path from send2trash import send2trash os.chdir(os.path.dirname(__file__)) logging.basicConfig( format='[%(levelname)s %(asctime)s %(filename)s:%(lineno)d]\t%(message)s', level='INFO', filename='log/recycle.log', ) EXPORTS_FILE='D:\\nas_file\\nfsd\\exports' KEEP_DAYS = 30 USERS = ['data'] def get_size(path: str) -> int: return sum(p.stat().st_size for p in Path(path).rglob('*')) def main(): exports = [] for line in open(EXPORTS_FILE): parts = line.strip().split() if not parts or parts[0].startswith('#'): continue exports.append(parts[0]) dt = (datetime.now() - timedelta(days=KEEP_DAYS)).strftime('%Y%m%d') for export in exports: for user in USERS: rec = os.path.join(export, '.recycle', user) if os.path.exists(rec): for item in os.listdir(rec): path = os.path.join(rec, item) # print(path, get_size(path)) if os.path.isdir(path) and item.startswith('2') and item < dt: size = get_size(path) if size <= 0: logging.info('rm empyt\t%s', path) os.rmdir(path) else: logging.info('trash\t%s', path) send2trash(path) if __name__ == '__main__': while True: try: logging.info('check') main() time.sleep(3600) except Exception as e: logging.exception(e) 其他 也可考虑将上文 openmediavalut 替换成黑群晖系统,牺牲定制化能力的同时大大增加易用性。 当然,你也可以两者双修 参考 https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v https://nelsonslog.wordpress.com/2019/06/01/wsl-access-to-linux-files-via-plan-9/

2021/9/21
articleCard.readMore