引子
最近在用 gitlab 的 CI/CD 实现 Keil 和 IAR 工程的自动构建,在最后一步遇到了 gitlab-runner 的编码问题引发的 bug,在此记录分享一下。
项目结构
gitlab 安装在 A 服务器上,在 B 服务器上安装了 gitlab-runner v15.1.0。
参考 gitlab 官方文档(virtualbox),在 B 服务器安装了 virtualbox,并在 virtualbox 内安装了 Win10 - 21H2,再在 Win10 内安装 pwsh、msys64、Keil、IAR 等工具。详细结构如下图:
问题描述
在 gitlab 网站触发 CI/CD 后,pwsh 报语法错误,提示双引号不成对:
ParserError:
Line |
211 | $CI_DISPOSABLE_ENVIRONMENT="true"
| ~~~~~
| Unexpected token 'true" $env:CI_DISPOSABLE_ENVIRONMENT=$CI_DISPOSABLE_ENVIRONMENT
| $CI_RUNNER_VERSION="15.1.0" $env:CI_RUNNER_VERSION=$CI_RUNNER_VERSION $CI_RUNNER_REVISION="76984217"
| $env:CI_RUNNER_REVISION=$CI_RUNNER_REVISION $CI_RUNNER_EXECUTABLE_ARCH="linux/amd64"
| $env:CI_RUNNER_EXECUTABLE_ARCH=$CI_RUNNER_EXECUTABLE_ARCH $GIT_LFS_SKIP_SMUDGE="1"
| $env:GIT_LFS_SKIP_SMUDGE=$GIT_LFS_SKIP_SMUDGE echo "n expression or statement.
问题分析
打印完整 pwsh 命令
要分析问题原因,首先要拿到原始 pwsh 命令,并粘贴命令确认问题可以复现,修改命令直至 pwsh 运行不报语法错误,再分析为何此修改可解决问题,进而反推出问题原因。
自己修改添加 log 并编译gitlab-runner
15.1.0,替换 B 服务器的gitlab-runner
后再次触发 CI/CD 即可看到完整 pwsh 命令。
以下是在本地 PC
(Win10) 编译 gitlab-runner
源码的步骤:
- 下载 gitlab-runner 15.1.0 的源码并解压
- 安装 golang,然后安装 gox:
go install github.com/mitchellh/gox@v1.0.1
- 在源码根目录下新建 build.bat 并填入以下内容
- 双击
build.bat
编译gitlab-runner
,编译完成后,会在源码根目录生成名为gitlab-runner
的二进制文件 - 将生成的
gitlab-runner
上传到B 服务器
替换原来的程序/usr/bin/gitlab-runner
,并重启服务:sudo gitlab-runner restart
rem build.bat
set GOARCH=amd64
set GOOS=linux
go build -buildvcs=false
pause
经过我对源码的分析,executors/virtualbox/virtualbox.go
文件Run
函数里的cmd.Script
就是pwsh
要执行的命令,而s.BuildShell.CmdLine
则是 ssh 连接 win10 的命令:ssh -p port username@localhost command
中的command
,在 s.sshCommand.Run
语句之前加入两句 log:
func (s *executor) Run(cmd common.ExecutorCommand) error {
s.Println("s.BuildShell.CmdLine: " + s.BuildShell.CmdLine)
s.Println("cmd.Script: " + cmd.Script)
err := s.sshCommand.Run(cmd.Context, ssh.Command{
Command: s.BuildShell.CmdLine,
Stdin: cmd.Script,
})
if exitError, ok := err.(*ssh.ExitError); ok {
exitCode := exitError.ExitCode()
err = &common.BuildError{Inner: err, ExitCode: exitCode}
}
return err
}
重新编译gitlab-runner
并重启服务,再次触发 CI/CD,可看到以下 log:
......
s.BuildShell.CmdLine: pwsh -NoProfile -NoLogo -InputFormat text -OutputFormat text -NonInteractive -ExecutionPolicy Bypass -Command -
cmd.Script: #!/usr/bin/env pwsh
& {
$ErrorActionPreference = "Stop"
$FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION="false"
$env:FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION=$FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION
$FF_NETWORK_PER_BUILD="false"
$env:FF_NETWORK_PER_BUILD=$FF_NETWORK_PER_BUILD
$FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY="false"
$env:FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY=$FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY
$FF_USE_DIRECT_DOWNLOAD="true"
$env:FF_USE_DIRECT_DOWNLOAD=$FF_USE_DIRECT_DOWNLOAD
......
$env:GITLAB_USER_LOGIN=$GITLAB_USER_LOGIN
$GITLAB_USER_NAME="xx勇"
$env:GITLAB_USER_NAME=$GITLAB_USER_NAME
$CI_DISPOSABLE_ENVIRONMENT="true"
$env:CI_DISPOSABLE_ENVIRONMENT=$CI_DISPOSABLE_ENVIRONMENT
$CI_RUNNER_VERSION="development version"
$env:CI_RUNNER_VERSION=$CI_RUNNER_VERSION
$CI_RUNNER_REVISION="HEAD"
......
if(!$cmdErr) {
& "git" "lfs" "pull"
if(!$?) { Exit &{if($LASTEXITCODE) {$LASTEXITCODE} else {1}} }
echo ""
}
echo "Skipping Git submodules setup"
}
ParserError:
Line |
211 | $CI_DISPOSABLE_ENVIRONMENT="true"
| ~~~~~
| Unexpected token 'true" $env:CI_DISPOSABLE_ENVIRONMENT=$CI_DISPOSABLE_ENVIRONMENT
| $CI_RUNNER_VERSION="development' in expression or statement.
第 2 行就是 ssh 的command
,第 3 行至倒数第 7 行就是 pwsh 详细命令。
手工复现问题
在 B 服务器
手动开启 virtualbox 虚拟机,确保 ssh 可以正常连接虚拟机里的 win10,在B 服务器
输入以下指令,通过 ssh 打开虚拟机中的 pwsh
:
ssh -p 22 test@192.168.1.148 pwsh -NoProfile -NoLogo -InputFormat text -OutputFormat text -NonInteractive -ExecutionPolicy Bypass -Command -
其中,pwsh -NoProfile -NoLogo -InputFormat text -OutputFormat text -NonInteractive -ExecutionPolicy Bypass -Command -
就是上面打印的s.BuildShell.CmdLine
,输入密码成功登陆后是不会有任何提示的,因为前面这段参数就是为了禁止pwsh
输出无关的东西,以方便gitlab-runner
解析命令执行结果。
直接复制上面打印的 pwsh 详细命令(cmd.Script
后的内容),粘贴到这个没有任何提示的窗口,并按多次回车:
#!/usr/bin/env pwsh
& {
$ErrorActionPreference = "Stop"
$FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION="false"
$env:FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION=$FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION
$FF_NETWORK_PER_BUILD="false"
$env:FF_NETWORK_PER_BUILD=$FF_NETWORK_PER_BUILD
$FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY="false"
$env:FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY=$FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY
$FF_USE_DIRECT_DOWNLOAD="true"
$env:FF_USE_DIRECT_DOWNLOAD=$FF_USE_DIRECT_DOWNLOAD
......
$env:GITLAB_USER_LOGIN=$GITLAB_USER_LOGIN
$GITLAB_USER_NAME="xx勇"
$env:GITLAB_USER_NAME=$GITLAB_USER_NAME
$CI_DISPOSABLE_ENVIRONMENT="true"
$env:CI_DISPOSABLE_ENVIRONMENT=$CI_DISPOSABLE_ENVIRONMENT
$CI_RUNNER_VERSION="development version"
$env:CI_RUNNER_VERSION=$CI_RUNNER_VERSION
$CI_RUNNER_REVISION="HEAD"
......
if(!$cmdErr) {
& "git" "lfs" "pull"
if(!$?) { Exit &{if($LASTEXITCODE) {$LASTEXITCODE} else {1}} }
echo ""
}
echo "Skipping Git submodules setup"
}
会得到 gitlab CI/CD 网页相同的错误提示:
ParserError:
Line |
211 | $CI_DISPOSABLE_ENVIRONMENT="true"
| ~~~~~
| Unexpected token 'true" $env:CI_DISPOSABLE_ENVIRONMENT=$CI_DISPOSABLE_ENVIRONMENT
| $CI_RUNNER_VERSION="development' in expression or statement.
说明已经复现问题,将 pwsh 详细命令复制到文本编辑器,在$CI_DISPOSABLE_ENVIRONMENT="true"
之前增加一个双引号"
,再次粘贴到那个没有任何提示的窗口,可以看到不再报pwsh
语法错误:
分析原因
仔细检查 pwsh 详细命令后,发现所有双引号都是成对出现的,将 pwsh 详细命令保存成 ps1 文件放到 Win10 中执行,也不会报语法错误,这说明 pwsh 详细命令本身并没有问题。想到平时用 notepad++ 编写 bat 脚本时经常出现中文乱码,要将 bat 文件格式转换为 GB2312 (也就是 GBK 格式)才能正常显示中文,那么,这里会不会也是这个问题呢?
上网搜索发现,有人说 go 默认是 UTF-8 格式,而我知道 Win10 中文版的命令行用的是 GBK 格式,再结合前面检查所有双引号"
都是成对的,说明极有可能是编码问题,某个中文的 UTF-8 编码在 GBK 下被解析成双引号"
了。
修复并验证
参考这篇文章,使用mahonia
工具,在 pwsh 命令执行前,将cmd.Script
转换为 GBK 格式。
- 下载
mahonia
工具,因code.google.com/p/mahonia
地址需要登录才能使用,故而改用 github 中的同名仓库github.com/NuoMinMin/mahonia
,执行语句下载mahonia
工具:go get github.com/NuoMinMin/mahonia
- 修改
executors/virtualbox/virtualbox.go
源码,将cmd.Script
转换为 GBK 格式 - 重新编译
gitlab-runner
并更新到B 服务器
,然后重启服务 - 再次触发 CI/CD,自动构建通过了!
package virtualbox
import (
"errors"
"fmt"
"time"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/executors"
"gitlab.com/gitlab-org/gitlab-runner/executors/vm"
"gitlab.com/gitlab-org/gitlab-runner/helpers/ssh"
vbox "gitlab.com/gitlab-org/gitlab-runner/helpers/virtualbox"
"github.com/NuoMinMin/mahonia" // UTF-8 转 GBK
)
......
func (s *executor) Run(cmd common.ExecutorCommand) error {
// s.Println("s.BuildShell.CmdLine: " + s.BuildShell.CmdLine)
// s.Println("cmd.Script: " + cmd.Script)
// 将 UTF-8 转换成 gbk, 因为 pwsh 用的是 gbk 编码, 不这么修改的话, 有些字会被解析成 " 导致执行 pwsh 报错
cmd.Script, _ = mahonia.NewEncoder("gbk").ConvertStringOK(cmd.Script)
err := s.sshCommand.Run(cmd.Context, ssh.Command{
Command: s.BuildShell.CmdLine,
Stdin: cmd.Script,
})
if exitError, ok := err.(*ssh.ExitError); ok {
exitCode := exitError.ExitCode()
err = &common.BuildError{Inner: err, ExitCode: exitCode}
}
return err
}
......
后记
修复代码不应该写在executors/virtualbox/virtualbox.go
中,这个问题是pwsh
编码格式不是 UTF-8 引发的,修复代码应当加在shells/powershell.go
里,以减小virtualbox.go
与具体 shell 的耦合,否则,当 virtualbox 里装的不是 Windows10,而是 Linux 或者 macOS,这个修复方案反而会出问题(需要 UTF-8 编码却提供了 GBK 编码),鄙人不才,没有完全读透gitlab-runner
的源码,不知道要如何修改shells/powershell.go
,所以没有给 gitlab 官方提交修复 patch,有能力的同学可以自行修复并给 gitlab 官方提交合并请求。
更新
gitlab 官方已修复此 bug,使用 15.6.1 版实测,问题不再出现。