gitlab-runner + virtualbox + pwsh 编码问题引发 CI/CD 报错

引子

最近在用 gitlab 的 CI/CD 实现 Keil 和 IAR 工程的自动构建,在最后一步遇到了 gitlab-runner 的编码问题引发的 bug,在此记录分享一下。

项目结构

gitlab 安装在 A 服务器上,在 B 服务器上安装了 gitlab-runner v15.1.0。
参考 gitlab 官方文档(virtualbox),在 B 服务器安装了 virtualbox,并在 virtualbox 内安装了 Win10 - 21H2,再在 Win10 内安装 pwshmsys64、Keil、IAR 等工具。详细结构如下图:

B 服务器 - ubuntu
A 服务器 - ubuntu
本地 PC
virtualbox
Windows10
msys64
push
CI/CD
ssh
gitlab-runner 15.1.0
sshd
pwsh
git
gitlab-runner 15.0.0
Keil
IAR
make
gitlab 网站
git

问题描述

在 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 命令,并粘贴命令确认问题可以复现,修改命令直至 pwsh 运行不报语法错误,再分析为何此修改可解决问题,进而反推出问题原因。
自己修改添加 log 并编译gitlab-runner15.1.0,替换 B 服务器的gitlab-runner后再次触发 CI/CD 即可看到完整 pwsh 命令。
以下是在本地 PC(Win10) 编译 gitlab-runner源码的步骤:

  1. 下载 gitlab-runner 15.1.0 的源码并解压
  2. 安装 golang,然后安装 gox: go install github.com/mitchellh/gox@v1.0.1
  3. 在源码根目录下新建 build.bat 并填入以下内容
  4. 双击build.bat编译gitlab-runner,编译完成后,会在源码根目录生成名为 gitlab-runner的二进制文件
  5. 将生成的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 详细命令后,发现所有双引号都是成对出现的,将 pwsh 详细命令保存成 ps1 文件放到 Win10 中执行,也不会报语法错误,这说明 pwsh 详细命令本身并没有问题。想到平时用 notepad++ 编写 bat 脚本时经常出现中文乱码,要将 bat 文件格式转换为 GB2312 (也就是 GBK 格式)才能正常显示中文,那么,这里会不会也是这个问题呢?
上网搜索发现,有人说 go 默认是 UTF-8 格式,而我知道 Win10 中文版的命令行用的是 GBK 格式,再结合前面检查所有双引号"都是成对的,说明极有可能是编码问题,某个中文的 UTF-8 编码在 GBK 下被解析成双引号"了。

修复并验证

参考这篇文章,使用mahonia工具,在 pwsh 命令执行前,将cmd.Script转换为 GBK 格式。

  1. 下载mahonia工具,因 code.google.com/p/mahonia地址需要登录才能使用,故而改用 github 中的同名仓库github.com/NuoMinMin/mahonia,执行语句下载mahonia工具:go get github.com/NuoMinMin/mahonia
  2. 修改executors/virtualbox/virtualbox.go源码,将cmd.Script转换为 GBK 格式
  3. 重新编译gitlab-runner并更新到B 服务器,然后重启服务
  4. 再次触发 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 版实测,问题不再出现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值