404E Blog

持续记录技术折腾和可复用经验。

docker+mc的运维管理方案

之前在 一篇文章 中提到过docker部署minecraft服务器,后来发现并不好用,因为portainer免费版并没有很好的控制台管理方式,没有用户组之类的精确权限控制,同时也没有好用的文件管理方案,最终选择使用code-server来管理,在容器中安装tmux用于后台运行并随时打开控制台,同时还有vscode的好用文件管理

Dockerfile

# ubuntu 作为基础镜像。
FROM ubuntu:24.04

# HTTP 代理
ENV HTTP_PROXY="http://172.17.0.1:7890"
# HTTPS 代理
ENV HTTPS_PROXY="http://172.17.0.1:7890"
# 针对 code-server/npm/git 等的全局代理(通常是小写的)
ENV http_proxy="http://172.17.0.1:7890"
ENV https_proxy="http://172.17.0.1:7890"
ENV NO_PROXY="localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# 时区
ENV TZ="Asia/Shanghai"

# 配置 APT 清华源
COPY aliyun-ubuntu.sources /etc/apt/sources.list.d/
RUN apt-get update

# 安装依赖
RUN DEBIAN_FRONTEND=noninteractive \
    apt-get install -y --no-install-recommends \
    curl \
    gosu \
    tmux \
    ca-certificates \
    iputils-ping \
    wget \
    zip \
    unzip \
    locales \
    && rm -rf /var/lib/apt/lists/*

# 设置编码
RUN locale-gen en_US.UTF-8 && update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

# code-server
RUN curl -fsSL https://code-server.dev/install.sh | sh

COPY start.sh /
RUN chmod +x /start.sh

RUN useradd --shell /bin/bash -u 1001 -m mc
WORKDIR /home/mc

# 暴露端口
EXPOSE 8080
EXPOSE 25565

CMD ["/start.sh"]

start.sh

#!/bin/bash
set -e

# 检查环境变量是否已设置
if [ -z "$SERVER_NAME" ]; then
    echo "Error: SERVER_NAME environment variable is not set correctly."
    exit 1
fi

# 修正文件归属
chown -R mc /home/mc

# ===============================================================
# 信号捕获函数:用于优雅关闭 MC Server
# ===============================================================
graceful_shutdown() {
    echo "Caught signal. Performing graceful shutdown..."
    
    # 检查 tmux session 是否存在
    if tmux has-session -t mc 2>/dev/null; then
        echo "Sending 'stop' command to Minecraft server via tmux..."
        
        # 使用 tmux send-keys 向 'mc' 会话发送 'stop' 命令和 Enter 键
        # -t mc: 指定目标会话
        # C-m: 相当于按下 Enter 键
        tmux send-keys -t mc 'stop' C-m
        
        # 等待 MC Server 进程退出。mc-server-runner 会处理 Java 的关闭
        # 我们可以等待 tmux session 消失,表示 mc-server-runner 已退出
        TIMEOUT=60
        COUNT=0
        while tmux has-session -t mc 2>/dev/null && [ $COUNT -lt $TIMEOUT ]; do
            echo "Waiting for Minecraft server to stop... (Max $TIMEOUT seconds)"
            sleep 1
            COUNT=$((COUNT + 1))
        done
        
        if [ $COUNT -eq $TIMEOUT ]; then
            echo "WARNING: Minecraft server did not stop gracefully within $TIMEOUT seconds. Killing tmux session."
            tmux kill-session -t mc 2>/dev/null
        else
            echo "Minecraft server stopped successfully."
        fi
    else
        echo "Minecraft server tmux session not found or already stopped."
    fi

    # 停止 code-server (exec 后的 code-server 已经是主进程,收到信号后会自动退出)
    # 我们这里不需要手动杀死 code-server,因为 Tini 会转发信号给它。
    # 退出脚本,允许 Tini 干净地清理进程
    exit 0
}

# 捕获 SIGINT (Ctrl+C) 和 SIGTERM (Docker Stop) 信号
trap 'graceful_shutdown' SIGINT SIGTERM

echo Starting Server...
cd /home/mc/$SERVER_NAME
gosu mc tmux new -d -s $SERVER_NAME
gosu mc tmux send-keys -t $SERVER_NAME:0 "$START_CMD" C-m

echo Starting Code...
gosu mc sed -i "s#^password: .*#password: $CODE_PASSWORD#" /home/mc/.config/code-server/config.yaml
gosu mc code-server --bind-addr 0.0.0.0:8080 /home/mc/$SERVER_NAME &
CODE_SERVER_PID=$!
echo "code-server started with PID $CODE_SERVER_PID."

# 等待 code-server 进程或收到的信号。
# 这一行是保持 start.sh 脚本存活的关键,以监听信号。
wait $CODE_SERVER_PID

# 如果 code-server 意外退出,则执行优雅关闭流程
graceful_shutdown

aliyun-ubuntu.sources

Types: deb
URIs: http://mirrors.aliyun.com/ubuntu
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

docker-compose.yml 使用示例(此处的镜像是本地构建的)

services:
  velocity:
    image: minecraft-universal:1.5
    container_name: velocity
    hostname: velocity
    environment:
      SERVER_NAME: "velocity"
      START_CMD: "./start.sh"
      CODE_PASSWORD: "<your-password>" 
    ports:
      - "25565:25565"
    volumes:
      - /opt/java:/java:ro
      - /opt/mc-velocity:/home/mc
    restart: unless-stopped
    networks:
      - mc
      - web

使用时需要先在映射的目录下创建目录,目录名字和环境变量中的SERVER_NAME一致,并在目录下添加 start.sh 以及其他服务器文件,对应compose中定义的启动指令

同事误删mysql库补救办法

发现问题

今天下午同事突然跑来找我说不小心把库删了,问我会不会恢复,我从来没有这种删库恢复的经验,自然是要学习一下的,直接开始帮他恢复

查找解决办法

被删库的机器,这儿就叫他243,243的应用和数据库在同一个服务器上

  1. 首先先停止了应用防止继续写入或者丢失服务数据
  2. 检查mysql的binlog是否开启 SHOW VARIABLES LIKE 'log_bin%';,发现是开着的
  3. 检查binlog是否完整 ls /opt/mysql/data/ 发现缺少了几乎一半的binlog,推测是开启了过期清理
  4. 这个时候了解服务器运维的同事提出可以问机房的人恢复硬盘备份,并联系了相关同事
  5. 等待了一段时间,运维同事恢复了前一天的一个备份到一台新机器上,这儿叫他229

解决过程

在等待的时间里,确定了解决方案

  1. 检查两个服务器的binlog id差异
  2. 在243上找到最新的删库的binlog id
  3. 在229上找到最新的binlog id
  4. 在243生成两个binlog id中间差异的sql文件
  5. 将sql文件scp到229服务器上
  6. 229服务器的mysql执行sql补全缺失的数据

其中第二步可以在等待的时候完成

检查binlog的脚本 - 243

mysqlbinlog --no-defaults -v --base64-output=DECODE-ROWS  --stop-position=287225397  binlog.000075 | tail -n 500 > ~/tail.log
# at 287225397
#260227 15:46:29 server id 1  end_log_pos 287225556 CRC32 0x97e6fb47 	Query	thread_id=663510	exec_time=1	error_code=0	Xid = 583720608
use `laboratory`/*!*/;
SET TIMESTAMP=1772178389/*!*/;
SET @@session.pseudo_thread_id=663510/*!*/;
SET @@session.foreign_key_checks=0/*!*/;
DROP TABLE IF EXISTS `assay_report` /* generated by server */

检查binlog的脚本 - 229

mysqlbinlog --no-defaults --database=laboratory ./binlog.000074 > ~/74.sql
# at 1074009846
#260227 10:39:55 server id 1  end_log_pos 1074009890 CRC32 0x5b7eb4ae   Rotate to binlog.000075  pos: 4
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

在243生成差异sql

mysqlbinlog --no-defaults --database=laboratory  --skip-gtids  --start-position=256983036 binlog.000074 --stop-position=287225397 binlog.000075 > all.sql

恢复sql

# 登录sql
mysql -u root -p
# 执行文件 (1.5G的all.sql最终执行了十几分钟,服务器是固态硬盘)
source /home/all.sql

最终效果

在下班前恢复完成数据库并准时下班🎉🎉🎉

ps:本次行动由gemini提供技术支持😂

优化超大单体Spring Boot项目开发环境启动速度

之前维护的一个项目,项目启动需要12分钟,开发环境需要频繁启停,极大影响开发效率

分析

使用IDEA profiler收集启动阶段的数据,并用idea打开火焰图分析

发现启动时间大部分都消耗在注入Autowired资源以及创建Aop切面

同时整理了项目的模块发现开发环境下有一部分是不常用的

优化方案

首先是修改pom使开发环境不加载不常用的模块,启动速度快了60秒左右

然后编写了脚本静态分析源码,收集了所有没有使用的@Autowired和@Resource,并手动处理(项目代码规范不是很好, 不确定是否有特殊的引用, 虽然最后没发现这样的引用),启动速度快了30秒左右

import java.io.File

val dir = File("/path/to/a/project")
val unused = mutableListOf<Pair<String, String>>()
val autowiredPattern = Regex("(private|public|protected)\\s+\\w+\\s+(?<varName>\\w+)\\s*;")

fun collect(file: File) {
    if (file.isDirectory) {
        // 忽略编译产物
        file.name == "target" && file.parentFile.resolve("pom.xml").exists() && return
        file.listFiles()?.forEach { collect(it) }
        return
    }
    if (!file.name.endsWith(".java")) {
        return
    }
    // println("scan ${file.name}")
    // 找到所有autowired
    val autowiredList = mutableListOf<String>()
    var nextIsAutowired = false
    val lines = file.readLines().filter { it.startsWith("//") }
    for (line in lines) {
        if (line.trim().run{ startsWith("@Autowired") || startsWith("@Resource") }) {
            nextIsAutowired = true
        }
        if (nextIsAutowired) {
            val result = autowiredPattern.find(line)
            if (result != null) autowiredList.add(result.groups["varName"]!!.value)
            nextIsAutowired = false
            continue
        }
    }
    if (autowiredList.isEmpty()) return
    // 查询没有使用的autowired
    val filePath = file.absolutePath
        .replace("\\", "/")
        .substringAfter("src/main/java/")
        .replace("/", ".")
        .removeSuffix(".java")
    autowiredList.filter { varName ->
        lines.all { line -> varName !in line || line.trim().matches(autowiredPattern) }
    }.let {
        unused += it.map { v -> filePath to v }
    }
}

collect(dir)
File("unused-autowired-${dir.name}.txt").writeText(unused.joinToString("\n") { (k, v) -> "$k: $v" })

此时启动速度依然很慢,于是使用 lazy-initialization 的方案,环境变量中添加 spring.main.lazy-initialization=true

但是所有bean都懒加载会导致一部分模块出错最终导致启动失败,于是添加filter

@Bean
public LazyInitializationExcludeFilter filter() {
    return (beanName, beanDefinition, beanType) -> {
        String className = beanType.getName();
        return className.startsWith("com.example.module.plugin.important")
                || className.startsWith("com.example.module.common")
                ;
    };
}

最终启动速度从12分钟优化到最快4分钟(不启用可选的模块)

总结

  • 模块拆分解耦是好文明,在这种场景下直接禁用不用的模块也不影响项目启动
  • 在bean初始化里写逻辑是坏文明,遇到一个上游依赖的组件,自己封装了一层XxlJobSpringExecutor,还把class设置成package-private,导致不加载该模块就无法正常初始化xxljob

Arthas

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
通常,本地开发环境无法访问生产环境。如果在生产环境中遇到问题,则无法使用 IDE 远程调试。更糟糕的是,在生产环境中调试是不可接受的,因为它会暂停所有线程,导致服务暂停。
开发人员可以尝试在测试环境或者预发环境中复现生产环境中的问题。但是,某些问题无法在不同的环境中轻松复现,甚至在重新启动后就消失了。
如果您正在考虑在代码中添加一些日志以帮助解决问题,您将必须经历以下阶段:测试、预发,然后生产。这种方法效率低下,更糟糕的是,该问题可能无法解决,因为一旦 JVM 重新启动,它可能无法复现,如上文所述。
Arthas 旨在解决这些问题。开发人员可以在线解决生产问题。无需 JVM 重启,无需代码更改。 Arthas 作为观察者永远不会暂停正在运行的线程。

Arthas官方文档

查cpu高占用

  • dashboard 查看占用最高的线程
  • thread -n 3 查看占用高的线程及其堆栈

查内存高占用

  • oom崩溃的情况无法处理,需要-XX:+HeapDumpOnOutOfMemoryError
    • -XX:HeapDumpPath=/tmp/heapdump.hprof 指定dump文件位置
    • 进程还活着才可以分析
  • vmtool --action forceGc 先fullGc一次
  • dashboard 查看Memory
  • jmap -histo:live <pid> | head -n 20 查看是否有大量自定义对象 一般排名靠前的都是java的基本类型
  • heapdump --live /tmp/dump.hprof 导出文件后拖到idea或者mat软件分析
    • 在合并的路径tab中可以按照实例类型和引用关系找到引用最多对象的实例
    • 在最大对象tab中可以看到对象及其引用的对象的占用并层层展开查看引用关系
    • 保留(Retained)是这个对象及其引用的对象层层递归加起来的总内存占用,主要看这个
      • 计算的是对象的独占对象的内存占用,如果一个object还被其他object引用,则不纳入计算
      • 找到保留大小最大的几个对象,通过引用树找到持有这些对象的实例
    • 浅层(Shallow)是这个对象本身占用的内存,一般只有数组的浅层大小会很大

修改方法逻辑

  • jad --source-only com.example.demo.UserController > /tmp/UserController.java 添加 --lineNumber=false 不显示行号 -d dump文件到目录
  • 编辑补全逻辑
  • mc /tmp/UserController.java -d /tmp 编译成class
  • retransform /tmp/com/example/demo/UserController.class 热重载

查慢方法调用

  • trace com.example.demo.OrderController createOrder -n 1 添加监听
    • -n 1 指定触发次数,方法触发指定次数后自动退出,不添加则会连续监听直到ctrl c
    • 若入参或返回值过长可以输出到文件 文件路径不能使用~ 否则会创建名为~的文件
  • 输出包含方法调用栈以及行号的执行耗时
    • 输出的列表和Map不会展开内容,只会显示size

查方法入参返回值

  • watch com.example.demo.UserService login "{params, returnObj}" -n 1 添加监听
    • -n 1 指定触发次数,方法触发指定次数后自动退出,不添加则会连续监听直到ctrl c
    • 添加 -x 2 展开对象, 展开2刚好够展开Map和List
    • watch com.example.demo.UserService login "{ @com.alibaba.fastjson.JSON@toJSONString(params), @com.alibaba.fastjson.JSON@toJSONString(returnObj) }" -n 1 使用OGNL编写表达式可以输出json格式, 此时不需要-x展开
    • watch com.example.demo.UserService login "{ params, returnObj }" "params[0].name == 'test'" -n 1 使用OGNL编写条件表达式,可以在递归调用等地方过滤不想要收集的方法调用 如果在win等无法输入中文的情况下可以用unicode编码 "'\u6d4b\u8bd5\u540d\u5b57' == params[0]"

远程连接内网路由器

看标题会感觉是一个很简单的操作,但是不是。路由器后台会自动重定向到ip访问,导致常规端口转发不生效。

我尝试过zerotier,但是和公司的局域网网段重叠了,遂放弃。

今天发现ssh可以做socks代理,于是解决了。

ssh指令ssh -D 1080 -N -q user@host -p port打开一个socks代理

然后下一个火狐(可以下便携版)

进入设置拉到最下面找到网络设置,配置socks代理(别写http的)

然后就可以直接访问了

回退vscode版本 & 多版本共存

最近更新了vscode,有一天需要连接远程服务器的时候发现报错说服务器glibc什么的版本过旧不支持了,但是服务器又不是我的,没法更新系统,所以需要回退旧版本。同时我有一个新版本的vscode,因为claude code插件不支持旧版本vscode,所以需要两个vscode互相隔离,于是就有了这个博客。

下载旧版本vscode

因为我需要多版本共存,所以需要一个免安装的版本,避免他和现有版本产生冲突

首先在faq中找到下载地址的格式,然后填入对应的版本

https://update.code.visualstudio.com/{version}/win32-x64-archive/stable

运行

如果下载之后直接运行,会直接使用当前用户目录下的缓存,比如插件啥的,可能会产生冲突,所以需要指定各种目录,我直接给出启动脚本

@echo off

set VSCODE_DIR=C:\Users\Administrator\Downloads\VSCode-win32-x64-1.96.4
set "VSCODE_EXECUTABLE=%VSCODE_DIR%\Code.exe"
set "USER_DATA_DIRECTORY=%VSCODE_DIR%\user-profile"
set "EXTENSIONS_DIR=%VSCODE_DIR%\user-extensions"

start "VSCode Portable" "%VSCODE_EXECUTABLE%" --user-data-dir "%USER_DATA_DIRECTORY%" --extensions-dir "%EXTENSIONS_DIR%"

用这个脚本启动则vscode会有独立的用户数据目录和插件目录,避免和安装版本的产生冲突

minecraft docker运行

今天研究了一下docker中运行minecraft,原因是希望在不给ssh的情况下允许别人进入服务器后台,因为已经部署了portainer,所以希望可以直接通过portainer操作后台

构建镜像

首先需要一个镜像来运行服务端

一开始我选择了Alpine作为底包,然后发现这个包实在是太干净了,甚至用的都不是glibc,下好的预编译的jdk没法跑,于是换了ubuntu

然后希望通过不同的目录来区分各个不同的子服,所以用环境变量+启动脚本动态选择工作目录

Dockerfile

# ubuntu 作为基础镜像。
FROM ubuntu:24.04

ENV TINI_VERSION=v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini

# 设置环境变量的默认值,这些值会被 docker-compose.yml 中的配置覆盖。
ENV SERVER_NAME=minecraft_server
ENV START_CMD="/java/bin/java -Xms1G -Xmx1G -jar server.jar --nogui"

# 设置容器内的通用工作目录。
WORKDIR /${SERVER_NAME}

# 暴露 Minecraft 服务器默认的 TCP 和 UDP 端口。
EXPOSE 25565

# 将启动脚本复制到镜像中。
COPY start.sh /start.sh

# 授权启动脚本可执行权限。
RUN chmod +x /start.sh

# 设置入口点。tini 确保当容器收到停止信号时,能优雅地关闭 Java 进程。
ENTRYPOINT ["/tini", "--"]

# 定义默认的启动命令。
CMD ["/start.sh"]

start.sh

#!/bin/bash

# 检查环境变量是否已设置
if [ -z "$SERVER_NAME" ]; then
    echo "Error: SERVER_NAME environment variable is not set correctly."
    exit 1
fi

cd /$SERVER_NAME
echo "starting..."
exec $START_CMD
# 构建指令
docker build -t minecraft-universal:1.0 .

docker compose

services:
  sc:
    image: minecraft-universal:1.2
    container_name: sc # 容器名字
    # 允许attach
    stdin_open: true
    tty: true
    ports:
      - "35565:25565" # 游戏端口映射,可以修改为其他端口
    environment:
      # 服务器名
      SERVER_NAME: "sc"
      START_CMD: "/java/bin/java -jar fabric-server-mc.1.21.8-loader.0.17.2-launcher.1.1.0.jar nogui"
    volumes:
      # 挂载宿主机的 Java 目录到容器中的 /java
      - /usr/local/jdk/21:/java
      # 挂载宿主机的服务器目录到容器中
      - ./sc:/sc
    restart: unless-stopped

使用如上配置之后可以在attach后正常和服务端控制台交互

重点是 stdin_open: truetty: true

wordpress 非标准端口 https

今天折腾了一天的wordpress,想从http切换到https

首先用acme申请了证书

在wordpress容器到公网之间添加了一个nginx做反代顺便添加https支持

首先遇到的问题是修改站点地址为https后argon的js和css都请求失败,一看发现还都是http

折腾半天找不到配错的地方,最后没写过php也只能硬改代码了,在argon的functions.php里添加了以下代码

function fix_output_urls($buffer) {
    return str_replace('http://e404.top', 'https://e404.top', $buffer);
}

function start_output_buffer() {
    ob_start("fix_output_urls");
}

add_action('wp_loaded', 'start_output_buffer');

然后就返回了正常的https地址

但是打开管理界面的时候一直重定向到自己

又调试半天,找不到问题原因

只能直接改代码看日志debug

半夜把is_ssl函数改了一下发现能跑了

function is_ssl() {
	if ( isset( $_SERVER['HTTPS'] ) ) {
		if ( 'on' === strtolower( $_SERVER['HTTPS'] ) ) {
			return true;
		}

		if ( '1' === (string) $_SERVER['HTTPS'] ) {
			return true;
		}
	} elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' === (string) $_SERVER['SERVER_PORT'] ) ) {
		return true;
	}

	return false;
}

第二天发现是wp-config-docker.php原来是需要替换config.php的

修复误损坏/usr的ubuntu

前情提要

在安装telegraf的时候需要复制文件到/etc /usr /var 等目录下,移动文件的时候错误使用了mv usr/* /usr/*的指令,导致系统损坏,缺少bash无法ssh远程连接

尝试远程修复

ssh连不上的情况下发现跑在docker中的容器居然还在正常运行,于是尝试通过portainer新建容器挂在根目录修复,发现portainer会执行宿主机上的可执行文件调用docker,失败

物理机处理

在无法远程修复的情况下只能直接操作物理机修复/备份数据重装。此时物理机/usr目录损坏已经无法重启正常进入系统,启动时显示run-init: can't execute '/sbin/init': No such file or directory,猜测是因为该文件是从/usr/sbin下软链接过来的

使用安装盘进入系统

我的安装盘是rufus创建的,使用的是之前安装系统时的ubuntu 24.04.1 server

首先进入启动盘选择界面,根据教程需要try ubuntu without install然后进入Live 环境,但是server的启动盘中并不包含这个选项,所以直接进入install界面,然后ctrl + alt + f2进入安装器的 BusyBox / Shell

挂载磁盘

使用lsblk指令列出可挂载的磁盘,我的机器中有一块固态和一块机械,固态的主要数据在 /dev/nvme0n1p2,机械的数据在/dev/sdb1,同时我还插入了移动硬盘以便在系统无法修复的情况下直接备份数据,移动硬盘在 /dev/sdc

首先挂载系统盘检查损坏情况mkdir /nvme && mount /dev/nvme0n1p2 /nvme,然后ls /nvme/usr检查损坏情况

非常神奇的,/nvme/usr/bin/nvme/usr/lib/nvme/usr/src没有被删除,于是我直接从安装器的系统中复制了其余的includelib64libexeclocalsbinshare目录到/nvme/usr下,并重启系统,此时正常进入系统

检查系统损坏情况

虽然正常进入了系统,但是并不是所有问题都解决了,首先检查了一些系统服务是否正常运行

dpkg -V | grep -v '^??'列出所有有缺失的包,apt reinstall重新安装确保丢失文件被重新安装

最后安装mariadb的时候忘了指定版本导致启动失败,回忆起以前安装的时候找的对标mysql8的版本,于是卸载自动安装的最新版本换成了mariadb-server=1:10.11.13-0ubuntu0.24.04.1,重启服务后正常运行

检查docker的时候发现docker compose指令不见了,于是重新安装apt install docker-compose-plugin

一天后发现博客无法访问,博客是部署在docker中的wordpress(也就是你现在看到的博客),显示Error establishing a database connection,检查后发现mariadb的默认绑定地址是127.0.0.1,所以无法从容器中访问

后记

至此,系统已修复完成,所有服务都可以正常运行

米家+小爱音箱+巴法云+termux 远程开关机

场景

以前写过一个安卓app可以远程操作家里旧手机给电脑发wake-on-lan数据包触发远程开机,最近用上了米家的智能家居,想把远程开关机集成到米家里,同时又不想买米家的开机卡

碰壁

首先研究了Home Assistant,部署完了对接米家的时候发现只能单向用ha操作米家设备,这不是和我的需求反了吗

然后看了一下小米iot平台开发者账号,发现要企业资质才能申请😓

问gpt,给了一个用脚本模拟米家设备的方案,会顶替掉一个设备😓

今天下班不死心在网上搜,突然看到一个知乎文章,发现还有这种好东西

折腾

巴法云

先注册巴法云账号,然后点按钮切到mqtt设备云并新建一个设备,此处设备命名后缀代表了设备的类型,参考

创建一个设备,该设备的名字代表了后面mqtt协议的频道,然后可以给设备自定义一个昵称,这个名字是米家关联后的设备名字

在该页面左上角可以看到密钥,点击显示复制出来,可以用mosquitto_sub订阅mqtt,接收消息

termux

然后是termux,研究ha的时候就给旧手机装好了,其实不用proot也可以用,但是之前已经装好了那就拿着用了

我用的脚本如下

#!/bin/bash

ID="{secret}" # 用自己的替换
TOPIC="{topic}"
HOST="bemfa.com"
PORT=9501

mosquitto_sub -h "$HOST" -p "$PORT" -i "$ID" -t "$TOPIC" | while read payload
do
    ts=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$ts] $payload" > $TOPIC.log
    if [[ "$payload" == "on" ]]; then
        wakeonlan 01:02:03:04:05:06
    elif [ "$payload" == "off" ]; then
        ssh device "shutdown /s /t 30 /f" # 需要配置 ssh config
    fi
done

运行脚本后会阻塞,此时如果在巴法云管理页面上可以看到订阅者:在线 1,如果在网页上推送消息,则log文件中会打印推送的消息

米家

在米家app中找到 我的 > 添加其他平台 > 巴法

输一下账号密码,然后会看到设备(需要是巴法云上名字符合后缀要求的设备,否则不会同步到米家),看到后就可以退出来了

使用

触发开关机首先可以用语音操作小爱同学打开电脑,这儿的电脑是巴法云里设置的设备昵称,触发之后会给mqtt推送一条消息on

然后可以使用米家操作按钮开关,但是米家里是看不到这个设备的,所以要用到自动化/手动控制去控制小爱音响,设备选择小爱音响,动作选择自定义,然后写打开电脑,就可以触发巴法云的mqtt推送了,延迟基本上在1秒左右