分类 编程相关 下的文章

部署过才发现,使用Docker部署PHP网站,没想象中的简单。顺便总结一下使用Docker Compose部署Typecho的经验。

1. 概述

重点注意:

  • 一般PHP网站的Docker镜像,只提供了网站的PHP源码和php-fpm服务,在生产环境不能单独部署。
  • 生产环境需要前端搭配Nginx或Apache等接收请求,再交给php-fpm服务处理。一般还需要后端连上MySQL、MariaDB、PostgreSQL等数据库。
  • 建议使用Docker Compose部署。比Docker run更好管理,比K8S更省资源。

2. 建立网络

建议先建立一个Docker网络,把相关服务部署到该网络。

  • 所有服务使用service名称作为host名称,配置相互访问时不用填IP。
  • 同一网络内的服务,不用在宿主机暴露端口,各个服务之间也相互访问。一般只需暴露Nginx的端口。

建立Docker网络的参考命令如下。其中网络名称为docker-net

docker network create --driver "bridge" docker-net

3. Nginx服务

Nginx的docker-compose.yaml,路径在/opt/docker_deploy/nginx/,内容如下:

services:
  nginx:
    image: nginx:1.27.0-alpine
    restart: always
    ports:
      - 80:80
      - 443:443
    environment:
      - NGINX_PORT=80
    volumes:
      - ./etc/nginx.conf/:/etc/nginx/nginx.conf:ro
      - ./etc/conf.d:/etc/nginx/conf.d
      - /etc/letsencrypt/archive:/etc/nginx/cert:ro
      - ./logs:/var/log/nginx
      - ./www:/var/www
      - /opt/docker_deploy/typecho/app:/var/www/typecho:ro
    networks:
      - docker-net

networks:
  docker-net:
    external: true

Type的Nginx配置文件,在/opt/docker_deploy/nginx/etc/conf.d/typecho.conf,内容如下:

server {
  server_name blog.xxxx.com;
  listen 80;

  rewrite ^(.*)$  https://$host$1 permanent;
}

server {
  server_name blog.xxxx.com;
  listen 443 ssl;
  http2 on;
  index index.php index.htm index.html;
  root /var/www/typecho;

  ssl_certificate /etc/nginx/cert/blog.xxxx.com/fullchain8.pem;
  ssl_certificate_key /etc/nginx/cert/blog.xxxx.com/privkey8.pem;

  add_header X-Frame-Options SAMEORIGIN;
  add_header Content-Security-Policy "frame-ancestors 'self';";

  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    include fastcgi_params;
    #fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param SCRIPT_FILENAME /app$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_pass typecho:9000;
    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
  }

  location / {
    try_files $uri $uri/ /index.php$is_args$query_string;
  }
}

4. Typecho

部署要点:

  • Typecho的Docker部署说明,可参考:https://github.com/typecho/Dockerfile
  • 由于服务器上还有其它网站或系统,需要Nginx做反向代理,所以Typecho的Docker镜像选择了fpm版本。
  • 需要把PHP源码文件映射出来,提供给Nginx访问。

Typecho的docker-compose.yaml,路径在/opt/docker_deploy/typecho/,内容如下:

ervices:
  typecho:
    image: joyqi/typecho:1.2.1-php8.0-fpm-alpine
    restart: always
    #ports:
    #  - "9000:9000"
    environment:
      - "TYPECHO_SITE_URL=https://blog.xxxx.com"
      - "TYPECHO_DB_ADAPTER=Pdo_Mysql"
      - "TYPECHO_DB_HOST=mariadb"
      - "TYPECHO_DB_PORT=3306"
      - "TYPECHO_DB_USER=typecho"
      - "TYPECHO_DB_PASSWORD=123456"
      - "TYPECHO_DB_DATABASE=typecho"
    volumes:
      - ./app:/app
    networks:
      - docker-net

networks:
  docker-net:
    external: true

要注意,如果Nginx的Typecho没有开启SSL,但需要全局https访问Typecho,可以在配置文件config.inc.php添加以下配置:

/** 全站开启https */
define('__TYPECHO_SECURE__', true);

5. MariaDB

MariaDB的docker-compose.yaml,路径在/opt/docker_deploy/mariadb/,内容如下:

ervices:
  mariadb:
    image: mariadb:11.4.2
    restart: always
    #ports:
    #  - 3306:3306
    volumes:
      - ./dbdata:/var/lib/mysql
    environment:
      MARIADB_ROOT_PASSWORD: root123456
      #MYSQL_USER: ${DB_USER}
      #MYSQL_PASSWORD: ${DB_PASSWORD}
      #MYSQL_DATABASE: ${DB_DATABASE}
    networks:
      - docker-net

networks:
  docker-net:
    external: true

虽然家里的迷你服务器(锐角云三角主机)跑了N年,但是一直没有搞定USB硬盘在开机后自动挂载。每次断电重启后,都需要手工执行挂载。终于,通过SystemD的配置,解决了这个问题。

Debian 12上实现开机自动挂载USB磁盘,一般可以在系统启动过程或者系统启动后执行。如果在系统启动过程执行挂载,挂载过程出错,有可能导致系统启动失败(导致不能进入系统)。所以系统启动后执行挂载,相对稳妥。其方案很多,我选择了在fstab文件里通过SystemD的配置实现。

修改/etc/fstab文件,添加以下配置即可。

UUID=7d010a88-bc3c-7a2c-0270-3eae70edd016 /media/ud1 ext4 defaults,auto,nofail,x-systemd.automount,x-systemd.device-timeout=10s 0 0

相关参数解析如下:

  • UUID,填写USB磁盘里需要挂载分区的UUID。可以使用以下其中一条命令查看:

    ls -l /dev/disk/by-uuid/
    lsblk -f
    blkid
  • /media/ud1,设置挂载目录。
  • ext4,分区的文件系统类型。
  • defaults,使用默认选项,即rw,suid,dev,exec,auto,nouser,async

    • rw,挂载该文件系统可读写。
    • suid,从该文件系统执行程序时,遵循用户权限和组权限的设置。
    • dev,解释该文件系统上的字符或块特殊设备。
    • exec,允许执行二进制文件或其它可执行文件。
    • auto,标识mount -a命令可以自动挂载该文件系统,实现开机或重启时自动挂载。
    • nouser,普通用户不能执行挂载。
    • async,该文件系统的所有I/O都应该异步完成。
  • auto,同defaultsauto
  • nofail,设备不存在时,不报告错误信息。
  • x-systemd.automount,为该文件系统创建自动挂载单元(automount unit)。
  • x-systemd.device-timeout=10s,SystemD等待该设备可见(show up)的时间,超过该时间则放弃挂载,避免没有接入该USB磁盘而导致开机等太久。

参考

Windows没有类似Linux的du命令,不能很方便地统计并显示指定路径下各个子项的大小。试过使用CMD的批处理去实现,除了编写代码不方便,实现效也果不好。于是想到使用PowerShell去实现。

具体代码

保存为文件名fold_sum.ps1

param($path)
if ( $path -eq $null ) {
    $path = "."
}
"Path : {0}`n---" -f $path

$totalSize = 0
Get-ChildItem -Force $path | Where-Object { $_.Mode.Substring(5,1) -ne "l" } | ForEach-Object {
    $folderPath = $_.FullName
    if ( $_.PSIsContainer ) {
        $folderSize = (Get-ChildItem -Recurse -Force $folderPath | Select-Object -Property Length | Measure-Object -Property Length -Sum).Sum
    } else {
        $folderSize = (Get-Item -Force $folderPath | Select-Object -Property Length).Length
    }
    $totalSize = $totalSize + $folderSize
    "{0} : {1:N2} MB" -f $_.Name, ($folderSize / 1024 / 1024)
}
# Show total size of current folder
"---`nTotal: {0:N2} MB" -f ($totalSize / 1024 / 1024)

简单说明

  • 使用CMD环境执行时,格式:powershell fold_sum.ps1 d:\path。其中d:\path不填写时默认统计当前路径。
  • 使用PowerShell环境执行时,格式:fold_sum.ps1 d:\path。其中d:\path不填写时默认统计当前路径。
  • Windows 7和Windows 11下都能执行。如果遇到输出结果报错,可以把结果重定向输出到文本文件。
  • PowerShell命令(或者函数)的结果,是基于key-value的格式,能够兼顾人工查看和程序使用。这点,比Linux的Shell要好。
  • 遍历子项时,已经排除了“连接”文件,但仍然会报错某些文件不能访问的问题。需要优化。

参考

1. 背景

由于公司的办公电脑也用于深度学习的模型训练,该电脑就不能同时大型软件(连Chrome也不能多开标签),避免影响训练。于是就想把新手机“红米Note 12 Turbo”利用起来,处理一些“轻办公”的工作。

找到方案是:

  • 方案1:Android系统利用chroot安装完整Linux系统,运行基于X11的图形化软件(例如Chrome),PC端运行X server
  • 方案2:手机运行Android桌面模式,并投屏到PC。

由于方案1运行不流畅、不能方便利用GPU加速、软件兼容不佳等,只能放弃。方案2整理了一下,能达到比较高的实用性。

2. 原理

据说从Android 10开始,Android系统内置了桌面模式。该模式下,App可以自由拖动显示位置,并调整窗口大小(跟PC操作系统一样)。

有趣的是,“开发者选项”里开启了“模拟辅助显示设备”,就可以启用桌面模式,而且基本不影响手机正常显示模式。可以认为同一个Android手机上同时运行“手机模式”和“桌面模式”,App能在这两个模式之间显示。

最后,利用scrcpy把“模拟辅助显示设备”投屏到PC端,就可以使用大屏幕显示“桌面模式”,并且利用鼠标键盘进行输入。

据说手机直连显示器(需要手机直接输出HDMI功能),或者无线连接Miracast,都可以显示“桌面模式”。但是手上没有相关设备,不能验证。

3. 配置

3.1. PC端软件

3.2. Android必要配置

  • 启用“开发者模式”。
  • 进入“设置” -> “系统” -> “开发者选项”,勾选“启用可自由调整的窗口”、“强制使用桌面模式”。

    • 其中“强制使用桌面模式”,就是“模拟辅助显示设备”启用桌面模式。

3.3. 启动命令

在PC端的命令窗口执行以下命令即可启动。Linux Shell脚本参考如下:

#!/bin/bash

# Params
#  $1: SERIAL|HOST[:PORT]
P_SERIAL=$1

PHONE_NAME="My Phone"
CMD_ADB="/opt/android-sdk/platform-tools/adb"
ADB_SERIAL=
CMD_SCRCPY="/opt/scrcpy/scrcpy"
DISPLAY_ID=0
# Log level
declare -A LOG_LEVEL=([i]=INFO [w]=WARN [e]=ERR)

# Log message
# Params
#  $1: log level
#  $2: message
function func_log() {
    lv=${LOG_LEVEL[$1]}
    if [ -z "$lv" ]; then
        lv=NONE
    fi
    echo [$lv] $2 1>&2
    return
}

function func_init() {
    result=n
    
    # Do connect
    is_connected=$(func_connect)
    if [ "$is_connected" == "y" ]; then
        echo y
        return
    fi
    func_log w "Connect failed. SERIAL or HOST is invalid, or need pairing"
    
    # Do pair
    read -p "Need to pair device? Enter "y" for yes, otherwise exit:" need_pair
    if [ "$need_pair" != "y" ]; then
        echo Exit 1>&2
        echo $result
        return
    fi
    is_paired=$(func_pair)
    if [ "$is_paired" != "y" ]; then
        func_log e "Pair failed, exit"
        echo $result
        return
    else
        func_log i "Pair succeed"
    fi
    is_connected=$(func_connect)
    if [ "$is_connected" != "y" ]; then
        func_log e "Connect failed, exit"
        echo $result
        return
    else
        func_log i "Connect succeed"
    fi
    echo y
    return
}

function func_get_serialno() {
    dev_serialno=$($CMD_ADB get-serialno)
    if [ -z "$dev_serialno" ]; then
        return
    fi
    echo $dev_serialno
    return
}

function func_check_serialno() {
    result=n
    dev_serialno=$($CMD_ADB -s "$P_SERIAL" get-serialno)
    if [ "$P_SERIAL" != "$dev_serialno" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_connect() {
    result=n
    
    # Get serialno of default device
    if [ -z $P_SERIAL ]; then
        dev_serialno=$(func_get_serialno)
        if [ -z "$dev_serialno" ]; then
            func_log e "No connected device"
            echo $result
        else
            echo y
            P_SERIAL=$dev_serialno
        fi
        return
    fi
    
    # Check if the device is connected
    is_connected=$(func_check_serialno)
    if [ "$is_connected" == "y" ]; then
        echo y
        return
    fi
    
    # Connect to the device
    dev_connect=$($CMD_ADB connect "$P_SERIAL")
    if [ "${dev_connect:0:9}" != "connected" ] && [ "${dev_connect:0:17}" != "already connected" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_pair() {
    result=n
    echo Enter "HOST[:PORT]" of the paired device:; read pair_host
    echo Enter "PAIRING CODE":; read pair_code
    dev_pair=$($CMD_ADB pair "$pair_host" pair_code)
    if [ "${dev_pair:0:19}" != "Successfully paired" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_tips() {
    echo + Tips ------------------
    echo   - Ctrl + Shift + O, turn ON screen of the connected device
    echo   - Ctrl + O, turn OFF screen of the connected device
    echo + -----------------------
}

function func_start_apps() {
    # --windowingMode <WINDOWING_MODE>: The windowing mode to launch the activity into.
    # --activityType <ACTIVITY_TYPE>: The activity type to launch the activity as.
    $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.LAUNCH -e package cu.axel.smartdock
    # $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.UNFREEZE -e package cu.axel.smartdock
    # sleep 2
    # $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -n cu.axel.smartdock/.activities.MainActivity
}

function func_stop_apps() {
    $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.FREEZE -e package cu.axel.smartdock
}

# main function
function func_main() {
    is_init=$(func_init)
    if [ "$is_init" != "y" ]; then
        return
    fi

    # Turn on auxiliary display device
    $CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices 1920x1080/193
    #$CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices 1920x1008/180

    # Get display-id of Simulate secondary displays
    DISPLAY_ID=$($CMD_SCRCPY $ADB_SERIAL --list-displays | $CMD_ADB $ADB_SERIAL shell "grep -o 'display-id=[1-9][0-9]*' | sed 's/display-id=\([1-9][0-9]*\)/\1/'")
    func_log i "Display id: $DISPLAY_ID"

    # Show tips
    func_tips

    # Run apps
    func_start_apps

    # Run scrcpy
    # $CMD_SCRCPY $ADB_SERIAL --display-id=$DISPLAY_ID --keyboard=uhid --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="$PHONE_NAME - Android Desktop Mode" --window-x=0 --window-y=25
    $CMD_SCRCPY $ADB_SERIAL --display-id=$DISPLAY_ID --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="$PHONE_NAME - Android Desktop Mode" --window-x=0 --window-y=25

    # Stop apps
    func_stop_apps

    # Turn off auxiliary display device
    $CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices null
}

func_main
exit

说明:

  • PHONE_NAME设置手机名称,作为投屏窗口的标题。
  • CMD_ADB设置adb命令所在位置。
  • ADB_SERIAL设置设备序列号,多设备时指定连接的设备。连接无线adb时,可设置IP:端口。可以作为脚本第一个参数传入。
  • CMD_SCRCPY设置scrcpy命令所在位置。
  • DISPLAY_ID是“模拟辅助显示设备”的“display-id”,用于scrcpy投屏。

    • 这里利用了Android自带的grepsed实现字符串匹配提取,避免操作系统缺乏相关命令(例如Windows的CMD)。
    • 如果PC端是Linux设备,可以改为“DISPLAY_ID=$(${CMD_SCRCPY} --list-displays | grep -oP '(?<=display-id=)1-9*')”,简化命令。
  • adb shell settings put global overlay_display_devices用于开启或关闭Android上“模拟辅助显示设备”。

    • 如果在“开发者选项”中开启“模拟辅助显示设备”,没有DPI选项,所以这里通过命令开启。
    • 值为1920x1080/180,设置“模拟辅助显示设备”的分辨率为1920x1080,DPI为180。此值适合针对20吋左右的屏幕。DPI的值,主要影响“模拟辅助显示设备”UI效果,包括字体大小,但不影响手机正常模式。
    • 值为"1920x1080/180\;1920x1080/180",显示两个“模拟辅助显示设备”。多个“模拟辅助显示设备”的配置,需要用双引号(")包裹,并以\;分隔。
    • 值为null,关闭“模拟辅助显示设备”。
    • 对于Windows 11系统,显示器分辨率为1080p时,“模拟辅助显示设备”的分辨率建议为1920x1008。
  • 启动scrcpy

    • 相关参数可以通过scrcpy --help查阅。
  • 关闭“模拟辅助显示设备”。

    • 由于scrcpy运行时,会占着终端,同时暂停了Shell脚本的运行。那么scrcpy停止后,就可以自动执行关闭“模拟辅助显示设备”。

Windows的CMD批处理脚本,参考如下:

@echo off
SETLOCAL ENABLEEXTENSIONS

rem Params
rem  %1: SERIAL|HOST[:PORT]
set P_SERIAL=%1

set PHONE_NAME=My Phone
set CMD_ADB="D:\tools\android-sdk\platform-tools\adb.exe"
set ADB_SERIAL=
set CMD_SCRCPY="D:\tools\scrcpy\scrcpy.exe"
set TEMP_FILE="C:\Users\%username%\AppData\Local\Temp\scrcpy_display_id_%RANDOM%.txt"
set DISPLAY_ID=0

call :func_main
goto end

rem Log message. If error, do exit.
rem Params
rem  %1: log level
rem  %2: message
:func_log
    set lv_index=%~1
    if "%lv_index%" == "i" (
        set lv=INFO
    ) else if "%lv_index%" == "w" (
        set lv=WARN
    ) else if "%lv_index%" == "e" (
        set lv=ERR
    ) else (
        set lv=NONE
    )
    echo [%lv%] %~2
goto :eof

:func_init
    set %~1=n
    
    rem Do connect
    call :func_connect is_connected
    if "%is_connected%" == "y" (
        set %~1=y
        set ADB_SERIAL=-s %P_SERIAL%
        goto :eof
    )
    call :func_log w,"Connect failed. SERIAL or HOST is invalid, or need pairing"
    
    rem Do pair
    set /p need_pair=Need to pair device? Enter "y" for yes, otherwise exit:
    if "%need_pair%" neq "y" (
        echo Exit
        goto :eof
    )
    call :func_pair is_paired
    if "%is_paired%" neq "y" (
        call :func_log e,"Pair failed, exit"
        goto :eof
    ) else (
        call :func_log i,"Pair succeed"
    )
    call :func_connect is_connected
    if "%is_connected%" neq "y" (
        call :func_log e,"Connect failed, exit"
        goto :eof
    ) else (
        call :func_log i,"Connect succeed"
    )
    set ADB_SERIAL=-s %P_SERIAL%
    set %~1=y
goto :eof

:func_get_serialno
    for /f "delims=" %%i in ('%CMD_ADB% get-serialno') do set dev_serialno=%%i
    if "%dev_serialno%" == "" (
        goto :eof
    )
    set %~1=%dev_serialno%
goto :eof

:func_check_serialno
    set %~1=n
    for /f "delims=" %%i in ('%CMD_ADB% -s %P_SERIAL% get-serialno') do set dev_serialno=%%i
    if "%P_SERIAL%" neq "%dev_serialno%" (
        goto :eof
    )
    set %~1=y
goto :eof

:func_connect
    set %~1=n
    
    rem Get serialno of default device
    if "%P_SERIAL%" == "" (
        call :func_get_serialno dev_serialno
    )
    if "%P_SERIAL%%dev_serialno%" == "" (
        call :func_log e,"No connected device"
        goto :eof
    ) else if "%P_SERIAL%%dev_serialno%" == "%dev_serialno%" (
        set %~1=y
        set P_SERIAL=%dev_serialno%
        goto :eof
    )
    
    rem Check if the device is connected
    call :func_check_serialno is_connected
    if "%is_connected%" == "y" (
        set %~1=y
        goto :eof
    )
    
    rem Connect to the device
    for /f "delims=" %%i in ('%CMD_ADB% connect %P_SERIAL%') do set dev_connect=%%i
    if "%dev_connect:~0,9%" == "connected" (
        rem Do nothing
    ) else if "%dev_connect:~0,17%" == "already connected" (
        rem Do nothing
    ) else (
        goto :eof
    )
    set %~1=y
goto :eof

:func_pair
    set %~1=n
    echo Enter "HOST[:PORT]" of the paired device: & set /p pair_host=
    echo Enter "PAIRING CODE": & set /p pair_code=
    for /f "delims=" %%i in ('%CMD_ADB% pair %pair_host% %pair_code%') do set dev_pair=%%i
    if "%dev_pair:~0,19%" neq "Successfully paired" (
        goto :eof
    )
    set %~1=y
goto :eof

:func_tips
    echo + Tips ------------------
    echo   - Ctrl + Shift + O, turn ON screen of the connected device
    echo   - Ctrl + O, turn OFF screen of the connected device
    echo + -----------------------
goto :eof

:func_start_apps
    rem --windowingMode <WINDOWING_MODE>: The windowing mode to launch the activity into.
    rem --activityType <ACTIVITY_TYPE>: The activity type to launch the activity as.
    %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.LAUNCH -e package cu.axel.smartdock
    rem %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.UNFREEZE -e package cu.axel.smartdock
    rem timeout /T 2
    rem %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -n cu.axel.smartdock/.activities.MainActivity
goto :eof

:func_stop_apps
    %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.FREEZE -e package cu.axel.smartdock
goto :eof

rem main function
:func_main
    call :func_init is_init
    if "%is_init%" neq "y" (
        goto :eof
    )
    call :func_log i,"Connected device: %P_SERIAL%"

    rem Turn on auxiliary display device
    rem %CMD_ADB% shell settings put global overlay_display_devices 1920x1080/180
    %CMD_ADB% %ADB_SERIAL% shell settings put global overlay_display_devices 1920x1008/180

    rem Get display id of auxiliary display device
    %CMD_SCRCPY% %ADB_SERIAL% --list-displays | %CMD_ADB% %ADB_SERIAL% shell "grep -o 'display-id=[1-9][0-9]*' | sed 's/display-id=\([1-9][0-9]*\)/\1/'" > %TEMP_FILE%
    set /p DISPLAY_ID=<%TEMP_FILE%
    del %TEMP_FILE%
    call :func_log i,"Display id: %DISPLAY_ID%"

    rem Show tips
    call :func_tips

    rem Run apps
    call :func_start_apps

    rem Run scrcpy
    rem %CMD_SCRCPY% --display-id=%DISPLAY_ID% --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="%PHONE_NAME% - Android Desktop Mode" --window-x=0 --window-y=25
    %CMD_SCRCPY% %ADB_SERIAL% --display-id=%DISPLAY_ID% --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="%PHONE_NAME% - Android Desktop Mode" --window-x=0 --window-y=25

    rem Stop apps
    call :func_stop_apps

    rem Turn off auxiliary display device
    %CMD_ADB% %ADB_SERIAL% shell settings put global overlay_display_devices null
goto :eof

:end

年初给老婆入手了“红米Note 12 Turbo”,整机较轻,运行速度快,最大惊喜是可以运行“yuzu”(Switch模拟器),可以玩“星之卡比:探索发现”。于是上个月也给自己入手一台(终于脱离“红米K30 5G”的苦海…)。

优点:

  • SoC采用高通的骁龙7+Gen2,高性能。
  • SoC能耗比高,OLED屏幕加持,续航很出色。
  • 塑料机身,取消屏幕支架,整机重量不到190g。
  • X轴线性马达,打字的震动反馈舒适。
  • 双扬声器。

缺点:

  • OLED屏幕,会有烧屏风险。
  • 即使国外有销售(国外名称为Poco F5),LineageOS官方没有支持。
  • Android 14的第三方ROM,自动亮度失效。(注:已修复)

1. 刷机

此机型有很多第三方ROM可选,推荐两个:

本人喜欢“Uvite 14”,下面介绍对应的刷机方法:

1.1. 下载文件

  • Recovery(可选)

  • ROM文件

    • Paranoid官方提供了针对Recovery和Fashboot的刷机包。
    • 个人建议下载Fashboot版,后面安装KernelSU会方便一点。

1.2. 刷ROM

注:

  • 刷机前,需要利用小米手机解锁工具进行解锁(此处略)。
  • 由于使用了A/B分区,Recovery集成在boot分区(跟系统Rom集成),所以刷入的第三方Recovery会被系统Rom所覆盖。
  • 由于Recovery集成在boot分区,所以就建议使用Fastboot模式刷系统Rom。
  • 使用Windows系统的,一定要先装好驱动。

方案1,使用Recovery

注:不推荐这个方案。

手机开机进入Fastboot模式(同时按音量减和开机键),刷入Recovery并进入,参考命令如下:

# 刷入Recovery
fastboot devices
fastboot flash recovery twrp-3.7.1_12-v8.6_A14-marble-skkk.img
fastboot reboot recovery

进入Revocery后,就是常规操作了:

  • 格式化data(首次刷这个系统,需要执行此步骤。记得先备份好文件。)
  • Recovery里刷入“Uvite 14”的Recovery版ROM
  • 重启后完成

方案2,使用Fastboot

注:如果使用Windows系统刷机,要先安装好驱动。具体驱动文件,在官方刷机工具里已提供。

Paranoid官方提供了Fastboot的刷机工具:https://github.com/ghostrider-reborn/aospa-flashing-kit/tree/marble

如果手工执行刷机,可参考以下步骤:

  • 手机开机进入Fastboot模式
  • 电脑端执行以下命令

    # Fastboot模式刷ROM
    fastboot update --skip-reboot aospa-uvite-beta-marble-20240617-image.zip
  • 期间手机会重启进入AOSPA Fastbootd,并执行余下处理,这里不要管
  • 直到电脑端提示Finished. Total time: 238.537s,表示完成刷机。此时,手机选Reboot system now,并按电源键,手机重启进入系统。

2. 获取Root权限

推荐使用KernelSU,天然自带隐藏功能,避免App检测Root。

参考资料:

参考步骤:

  • 在github下载最新的KernelSU apk,并在手机安装。
  • 从“Uvite 14”的Fastboot版的刷机包中,提取boot.img文件,并放到手机上。
  • 手机上使用KernelSU apk对boot.img打补丁。
  • 把打补丁后的boot.img刷入手机。

    • 这一步,我的做法是把打补丁后的boot.img,替换“Uvite 14”的Fastboot版的刷机包对应文件,并使用刷ROM的操作刷入。
  • 重启手机后,再打开KernelSU,就提示正常工作了。

3. 刷成砖

值得记录的是,试过使用Recovery刷Rom导致刷成砖。主要是刷机过程中,Recovery自动重启,导致刷机过程意外终止,手机被刷成砖,不能启动,也不能进入Fastboot模式。

由于采用高通的SoC,这个情况,可以进入9008模式,刷入Rom,实现拯救手机。而Android手机进入9008模式的方式,主要有两个,一个是使用刷机线,另一个是开机过程短接主板上的特定触点。查了下,这个手机只能通过“短接触点”的方式进入9008。由于我直接申请售后,就没有继续深入研究了。

关于9008刷机线(以下内容来源网络,没有实践过):

  • 使用Micro USB数据线,即USB 2.0的数据线。
  • 剥开“绿色”和“黑色”的线,露出金属,用于短接。
  • 使用此数据线连上电脑,另一头接上Type C转接头。
  • 手机关机状态下,短接“绿色”和“黑色”线(用手摁住即可),Type C转接头连上手机,几秒后松开两个金属线,在电脑设备管理器(我的电脑-右键-管理-设备管理器-端口COM和LPT)中可以看到9008端口。
  • 如果嫌麻烦,可以直接购买9008刷机线,有个按钮可以实现短接“绿色”和“黑色”的线。

1. 概述

  • 通过部署Docker Registry,可实现自建Docker私有仓库。
  • 自建Docker私有仓库,主要是方便内部分发或部署项目。如果只有一个服务器,可以直接build镜像,且不建私有仓库。
  • 各大厂商有提供免费的私有仓库服务,可以不用自建。

官方相关文档:

2. 部署

  • 假设在IP为192.168.0.1的服务器上部署。
  • Docker Registry采用端口5000docker-registry-ui采用端口80
  • 建议使用Docker Compose方式进行部署。
  • 为了方便管理(主要是执行“删除”操作),同时部署了可视化操作的Web界面。
  • 由于是内部部署,Docker Registry没有配置用户管理(包括用户的身份验证、权限管理之类)。
  • Registry的所有配置项,均可通过环境变量设置。相关的完整配置项说明,详见:Configuring a registry

部署Registry的docker-compose.yaml文件,参考如下:

version: '3.8'

services:
  registry-server:
    image: registry:latest
    #container_name: registry-server
    restart: always
    environment:
      - REGISTRY_STORAGE_DELETE_ENABLED: true
  volumes:
    - ./registry:/var/lib/registry

  registry-ui:
    image: joxit/docker-registry-ui:latest
    #container_name: registry-ui
    restart: always
    ports:
      - 80:80
    environment:
      - SINGLE_REGISTRY=true
      - REGISTRY_TITLE=Docker Registry UI
      - DELETE_IMAGES=true 
      - SHOW_CONTENT_DIGEST=true 
      - NGINX_PROXY_PASS_URL=http://registry-server:5000
      - SHOW_CATALOG_NB_TAGS=true
      - CATALOG_MIN_BRANCHES=1
      - CATALOG_MAX_BRANCHES=1
      - TAGLIST_PAGE_SIZE=100
      - REGISTRY_SECURED=false
      - CATALOG_ELEMENTS_LIMIT=1000

3. 上传镜像

给镜像设置名称和tag,然后执行docker push上传。其中镜像名称需要以Registry地址(IP或域名)+端口作为前缀。

示例命令如下:

# 给镜像设置tag
docker tag <镜像名称>:<tag> 192.168.0.1:5000/<镜像名称>:<tag>

# 上传镜像
docker push 192.168.0.1:5000/<镜像名称>:<tag>

4. 下载镜像

4.1. 修改Docke配置文件。

一般Linux上,Docker服务的配置文件在/etc/docker/daemon.jsoninsecure-registries添加Docker Registry服务,例如:

{
    "insecure-registries":["192.168.0.1:5000"]
}

修改后需要使用配置生效。可重启本机Docker服务,或执行热更新操作。例如:

# 重启本机Docker服务
sudo systemctl restart docker

# Docker配置文件热更新
sudo kill -SIGHUP $(pidof dockerd)

4.2. 从自建私有仓库下载镜像

docker pull 192.168.0.10:5000/<镜像名称>:<tag>

5. 仓库管理

一般通过docker-registry-ui(即http://192.168.0.1/),对私有仓库的镜像进行查看、删除。

Docker Registry本身提供了接口,用于管理其存储的镜像。

最近使用了MySQL 8的主从复制,利用数据同步,实现数据备份。

1. 概述

关于MySQL主从复制的原理和各种部署方式,参考:看完这篇还不懂 MySQL 主从复制,可以回家躺平了~。本文采用“一主一从”方式部署。

参考文章:

官方文档:

注意:从MySQL 8.0.22开始,大量涉及“SLAVE”的配置和命令,都改为“REPLICA”字样,详见官网说明。

2. 主服务器配置

主服务器是安装在Debian 12的MySQL 8,假设其IP地址为192.168.0.100。修改MySQL的配置文件/etc/mysql/conf.d/mysqld.cnf,在[mysqld]下添加以下配置。保存配置文件后重启MySQL服务。

[mysqld]
# 主从复制,设为主服务器
# 服务器ID
server-id = 1
# 开启二进制日志。事务提交时写日志到对应文件
log-bin = mysql-bin
# 日志过期删除的天数,延迟严重的话会导致日志文件占用磁盘
expire_logs_days = 14

利用mysql命令,连上MySQL主服务器,并执行以下语句。注意: MySQL 8 默认身份验证插件是caching_sha2_password,详见变量default_authentication_plugin的配置。

-- 创建用户,提供给从服务器访问
CREATE USER 'repl'@'%' IDENTIFIED WITH caching_sha2_password BY 'password';
-- 授权用户拥有所有数据库表的同步数据权限
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

再执行以下语句,显示主服务器的配置,记下FilePosition的值。

SHOW MASTER STATUS\G;

3. 从服务器配置

从服务是安装在Debian 12的MySQL 8,假设其IP地址为192.168.0.101。修改MySQL配置文件/etc/mysql/conf.d/mysqld.cnf,在[mysqld]下添加以下配置。保存配置文件后重启MySQL服务。

# 主从复制,设为从服务器
# 服务器ID
server-id = 2
# 同步的数据库。多个数据库需配置多行
#replicate-do-db = db1
#replicate-do-db = db2
# 服务重启后自动开始同步数据,默认OFF
#skip-replica-start = false

利用mysql命令,连上MySQL从服务器,并执行以下语句。

  • 由于主服务器的身份验证插件采用caching_sha2_password,从服务器需要配置GET_SOURCE_PUBLIC_KEY=1才能通过身份验证。
  • SOURCE_LOG_FILE对应主服务器的File的值。
  • SOURCE_LOG_POS对应主服务器的Position的值。
-- 从服务器配置
STOP REPLICA;

CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.0.100'
    ,SOURCE_PORT=3306
    ,SOURCE_USER='repl'
    ,SOURCE_PASSWORD='password'
    ,SOURCE_LOG_FILE='mysql-bin.000001'
    ,SOURCE_LOG_POS=123
    ,GET_SOURCE_PUBLIC_KEY=1
    ;

START REPLICA;

执行以下语句,查看从服务器的状态,检查从服务器是否正常运行。如果Replica_IO_RunningReplica_SQL_Running都显示Yes,则表示正常运行。

SHOW REPLICA STATUS\G;

虽然MP3格式存在很多缺点,但是其通用性强、文件大小范围下保证一定的音质,所以存储的音乐文件还是使用MP3格式。

这里记录一下Ubuntu上的相关工具。

1. SoundConverter

获取到的音频文件,其格式可能是整盘录制成FLAC或APE,先转换成MP3格式,再切分。

2. Mp3splt

如果是整盘音频文件,一般会自带CUE文件,描述包含音轨的时间、名称等信息。音频文件转换成MP3后,需要改一下CUE文件里所指定的音频文件名,才能用Mp3splt执行切分。

参考命令格式:

mp3splt -c music.cue music.mp3

3. EasyTAG

用来修正MP3文件的Tag信息,包括内嵌图片。

公司的Web项目,处理图片延迟加载时,遇到一个图片预留占位的显示问题。记录一下相关解决方案。

1. 需求

本来要解决的问题是,图片“延迟加载”。然后图片未加载之前,需要预留占位,避免加载后撑大页面(主要是为了更好看吧)。

2. 解决方案

图片延迟加载,使用<img>loading="lazy"即可。现代浏览器,当前对其支持还不错。另外,最好不用使用CSS的background加载图片,因为没有很好(或者说简单)的延迟加载解决方案。

对于图片预留占位,主要根据界面设计的布局,选择不同的处理方法。比较麻烦的是,需要自适应浏览器窗口大小的情况。如下:

  • 已知图片宽高。可以随便整。
  • 图片框固定大小。使用CSS的object-fit获得最佳显示效果。
  • 固定列数的图片框自适应浏览器窗口。按浏览器窗口自动计算图片框的宽度。
  • 图片框宽度自适应。使用动态的正方形图片框。这是最终采用的方案。

2.1. 已知图片宽高

可以设置图片按比例缩放,或者按最长边等比例缩放。这个不用说了。

2.2. 图片框固定大小

例如图片框固定,宽为300px,高为300px。图片保持比例,并完整显示,使用CSS的object-fit:contain;。代码如下:

<div style="width:300px; height:300px;">
  <img src="..." style="width:100%; height:100%; object-fit:contain;" />
</div>

2.3. 固定列数的图片框自适应浏览器窗口

一般是列表中带图片的情况,可以使用CSS的单位vwvh

  • vw,浏览器窗口宽度的1%
  • vh,浏览器窗口高度的1%

关于CSS的长度单位,详见: - CSS:层叠样式表 | MDN

缺点:

  • 如果图片的宽高,不能根据屏幕的宽高计算出来,此方案不适用。
  • 如果图片宽高不能适应图片框的比例,也是使用object-fit:contain;按比例缩放。

以下示例,按4列显示,图片框是宽高都为窗口宽度24%的正方形:

<style>
body{margin:0; padding:0;}
.ItemList{width:100%; margin:0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; justify-content: space-evenly;}
.ItemWrap{width:24%; margin:1vw 0 0; padding:0; display: block;}
.ItemWrap img{width:24vw; height:24vw; object-fit: contain;}
</style>
<ul class="ItemList">
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
  <li class="ItemWrap"><img src="..." loading="lazy" /></li>
</ul>

2.4. 图片框宽度自适应

动态宽高的正方形图片框,是比较折中和灵活的方案。可以不用知道图片宽高。图片框宽度动态计算,高度设为跟高度一致。示例如下:

<style>
body{margin:0; padding:0;}
.ItemList{width:100%; margin:0; padding:0; list-style:none; display:flex; flex-wrap:wrap; justify-content:space-evenly;}
.ItemWrap{width:24%; margin:1vw 0 0; padding:0; display:block; background-color:#c5a29c;}
.ImgWrap{width:100%; height:0; padding-bottom:100%; overflow:hidden; display:block; position:relative;}
.ImgWrap img{width:100%; height:100%; object-fit:contain; position: absolute;}
</style>
<ul class="ItemList">
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
  <li class="ItemWrap"><a class="ImgWrap"><img src="" loading="lazy"></a></li>
</ul>

解析:

  • ul标签,使用灵活的flex样式显示列表。
  • li标签,决定列数。
  • a标签,作为图片框,限制图片显示的宽高。
  • a标签的height:0; padding-bottom:100%;,通过padding-bottom设置高度与宽度一致,是这个方案最巧妙的地方
  • a标签的overflow:hidden;,避免内嵌的图片溢出。
  • a标签的position:relative;,把宽高传给子级。
  • img标签,通过position: absolute;悬浮显示在图片框上面,其宽高获取了图片框(即父级)的宽高。

3. 后续问题

图片加载时,小图最好加载对应的缩略图。体积小,加载快,体验好。但是要考虑图片框大小,避免小图被拉大而导致模糊,降低用户观感。

1. 概述

由于历史原因,工作上,需要把Microsoft SQL Server指定数据库上定时更新的数据,同步到MySQL 8,大概一周一次。

解决方案有几个:

  • 1)采用现成的工具。

    • 但是,一时间没找到好的工具。
  • 2) 开发个程序,读取SQL Server的相关数据,再插入到MySQL。

    • 需要时间开发,且功能上具有针对性。
    • 作为长期使用的工具,这是最优的方案。
  • 3) 从SQL Server生成MySQL的insert语句,再到MySQL上执行。

    • 很多数据库管理工具都提供了数据迁移功能,例如:MySQL Workbench。
    • 如果数据结构不变,insert语句是稳定的,这方案也不错。
    • 要注意采用批量插入,提高导入性能。
  • 4) 从SQL Server导出格式化数据,例如CSV文件,再导入到MySQL。

    • 由于保存数据的CSV文件比较通用,相关的数据库管理工具都支持导入导出。
    • 但是要注意要处理数据格式、NULL数据等问题。

目前采用了第3个方案,CSV文件比较通用,也不用考虑怎么开发。但是偶尔会出现导入MySQL失败的问题,比如出现了NULL数据。后面应该会写个程序处理,直接生成insert语句。

2. SQL Server导入导出CSV

一般使用BCP命令。即Bulk Copy Program,是一个命令行工具,用于在SQL Server之间批量传输数据。由于微软推出了SQL Server for Linux,所以可以完全在Linux执行导入导出的操作。另外,微软提供了SQL Server for Linux的官方Docker镜像,比Windows上安装SQL Server Express更方便,非常适合开发测试使用(主要应付历史)。

官方介绍及参考文档如下:

2.1. BCP使用说明

直接运行bcp命令,会提示其用,如下:

> C:\Program Files (x86)\Microsoft SQL Server\Client SDK\ODBC\130\Tools\Binn\bcp.exe

用法: bcp {dbtable | query} {in | out | queryout | format} 数据文件
  [-m 最大错误数]             [-f 格式化文件]       [-e 错误文件]
  [-F 首行]                   [-L 末行]             [-b 批大小]
  [-n 本机类型]               [-c 字符类型]         [-w 宽字符类型]
  [-N 将非文本保持为本机类型] [-V 文件格式版本]     [-q 带引号的标识符]
  [-C 代码页说明符]           [-t 字段终止符]       [-r 行终止符]
  [-i 输入文件]               [-o 输出文件]         [-a 数据包大小]
  [-S 服务器名称]             [-U 用户名]           [-P 密码]
  [-T 可信连接]               [-v 版本]             [-R 允许使用区域设置]
  [-k 保留 Null 值]           [-E 保留标识值]
  [-h"加载提示"]              [-x 生成 xml 格式化文件]
  [-d 数据库名称]

2.2. BCP导出CSV文件

CMD批处理命令参考如下:

set CUR_PATH=%~dp0
set BCP="C:\Program Files (x86)\Microsoft SQL Server\Client SDK\ODBC\130\Tools\Binn\bcp.exe"
set BCP_PARAM=-S "IP,端口" -U "用户名" -P "密码" -d "数据库名" -t \t -b 1000 -c -C 65001 -k

rem 导出指定数据库表的数据
%BCP% 数据库名.dbo.表名 out %CUR_PATH%table_export.csv %BCP_PARAM%

rem 导出指定查询语句的数据
%BCP% "select查询语句" queryout %CUR_PATH%query_export.csv %BCP_PARAM%

参数说明:

  • -S 服务器名称,服务器IP与端口之间,使用英文逗号(即“,”)分隔。
  • -t 字段终止符,默认是Tab符号(即“/t”)。
  • -b 批大小,如果导出数据太多,需要分页操作,默认是1000。
  • -c 字符类型,设置导出文件的字符编码为UTF-8时,设置为“-c -C 65001”,要注意大小写。

要注意,导出的CSV文件不带字段名称。需要记录字段名称时,目前只能把字段名称插入到CSV文件的第一行,并且以数据行的分隔符号进行分隔。

2.3. BCP导入CSV文件

要先创建对应的表,才能执行导入。格式如下:

set BCP_PARAM=-S "IP,端口" -U "用户名" -P "密码" -d "数据库名" -t \t -b 1000 -c -C 65001 -k
bcp 数据库名.dbo.表名 in 数据文件.csv %BCP_PARAM%

2.4. SQL语句执行BCP命令

要注意,用户需要授权可执行xp_cmdshell的权限。官方详细说明如下:xp_cmdshell (Transact-SQL) - SQL Server | Microsoft Learn

格式如下:

exec master..xp_cmdshell 'bcp ...'

2.5. SQL Server的其它导入导出方案

3. MySQL导入导出CSV

3.1. 相关参考

参考文章:MySQL导出数据为CSV的方法

MySQL官方文档:

3.2. MySQL导入CSV

假如导入数据到数据表user_table,该表有字段id、name、remark,其中remark数据可能为NULL。

-- 导入CSV文件的SQL语句
load data infile '/var/lib/mysql-files/import_data.csv' into table user_table fields terminated by '\t' escaped by '' optionally enclosed by '"' lines terminated by '\n' ignore 1 lines (id,name,@remark) set remark=nullif(@remark,'');

说明:

  • CSV文件需要放在mysql用户有权限的目录,比如/var/lib/mysql-files/
  • fields terminated by '\t',表示CSV数据以TAB符号分隔。
  • escaped by '',设置转义字符,默认的是反斜杠(backslash:\ ),设置空值('')表示不适用转义。
  • optionally enclosed by '"',以双引号包裹单一字段的数据。
  • lines terminated by '\n',每行数据的结束符号。
  • ignore 1 lines,导入数据时跳过第一行,因为第一行是字段名称的说明。
  • (id,name,@remark),把一行数据关联到对应的字段。其中@remark是把数据赋值到变量,后面有特殊处理。
  • remark=nullif(@remark,''),表示remark字段的数据,根据@remark变量进行处理。这里是NULL数据转为空字符串。

3.3. MySQL导出CSV

-- 导出CSV文件的SQL语句
select * from user_table into outfile '/tmp/expor_data.csv' fields terminated by '\t' escaped by '\\'  optionally enclosed by '"' lines terminated by '\n' ;

说明:

  • fields terminated by "\t",表示CSV数据以TAB符号分隔。
  • escaped by '\\',设置转义字符,默认的是反斜杠(backslash:\ ),设置空值('')表示不适用转义。
  • optionally enclosed by '"',以双引号包裹单一字段的数据。
  • lines terminated by '\n',每行数据的结束符号。

4. SQL语言的concat_ws函数

SQL的select语句可以使用concat_ws函数,可以实现一行数据的所有字段值合并成一个字符串,并指定分隔符号。然后把查询结果保存为文本文件(包括CSV),即实现了数据导出。参考文档如下: