作者: Jim Wang 公众号: 巴博萨船长
摘要:Python 是如何访问和控制系统的环境变量的,Python中的父进程与子进程间的环境变量关系是何?能不能在运行时修改环境变量?子进程中对环境变量修改能不能传递给对父进程?如果能,怎么样实现这样的变化?如何使用subprocess.Popen运行vcvars.bat,然后在持续集成时,完成devenv清理和创建的前期准备。
Abstract: How does Python access and control the system‘s environment variables? What is the relationship between the environment variables of the parent process and the child process in Python? Can environment variables be modified at runtime? Can the modification of environment variables in the child process be passed to the parent process? If so, how to achieve such a change? How to use subprocess.Popen to run vcvars.bat, and then complete the preliminary preparations for the Clean and Build of devenv during CI .
作者: Jim Wang 公众号: 巴博萨船长
任务背景 该任务是:当前所用的完成持续集成的脚本是Python语言实现的。在现有的脚本的核心内容是,使用subprocess.Popen方法来调用devenv命令,从而实现项目的清理clean和构建build。
今年伊始,项目中引用了.NET组件。一部分代码使用.NET完成了一个交互界面窗口。由于使用了.NET,编译的时候就需要对.NET的程序集进行强名称签名等一系列操作。例如,在导出类型库时会使用到的类型库导出工具Tlbexp.exe(一般在Visual Studio项目属性中**[生成]**部分中的“为COM互操作注册”被勾选时),以及程序集注册工具Regasm.exe等。此类工具的路径为:C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\。理论上,当在命令行中使用devenv命令工具时,该工具就会自动查找上述的工具目录,并保证在编译解决方案的时候能够访问到这些工具。实践得知,devenv不能完成诸如此类环境变量的设置。进而在使用devenv时,因为项目配置中的所用到的工具因为无法找到会发生编译错误。
既然devenv不能实现理论结果,那就需要用到vsvars32.bat个batch文件了了。该文件是visual studio自带的文件,本质就是配置环境变量,工作目录的一些批处理命令。任务中遇到的问题是Tlbexp.exe无法找到,问题的本质是工具的路径不在系统环境中:
1 VS140COMNTOOLS=C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\
初始代码 话不多少,直接上代码,项目一开始我们使用的是如下的代码,代码的主要内容是使用subprocess模块下的Popen方法来调用cmd命令devenv完成代码的清除clean和构建build,部分代码如下:
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 import osimport subprocess as spdef mCompile (slnPath, compilArgs ): """[summary] Args: slnPath ([type]): [description] compilArgs ([type]): [description] Raises: RuntimeError: [description] ValueError: [description] """ compilerExe = "C://Porgamm Files (x86)//Microsoft Visual Studio XX.X/CommonX//IDE//devenv" startupinfo = sp.STARTUPINFO() startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW logClean = "clean.txt" stepClean = sp.Popen([compilerExe, "/clean" , compilArgs, slnPath, "/OUT" , logClean ], stdout=sp.PIPE, stderr=sp.STDOUT, startupinfo=startupinfo) stdoutdata, stderrdata = stepClean .communicate() if stepClean.returncode != 0 : msg = "Error: Failed to clean the solution " + slnPath if stdoutdata != None : msg = msg + "\nError Message: " + stdoutdata raise RuntimeError(msg) logBuild = "build.txt" stepCompile = sp.Popen([compilerExe, "/rebuild" , compilArgs, slnPath, "/OUT" , logBuild], stdout=sp.PIPE, stderr=sp.STDOUT, startupinfo=startupinfo) stdoutdata, stderrdata = stepCompile.communicate() if stepCompile.returncode != 0 : msg = "Error: Failed to compile the solution " + slnPath if stdoutdata != None : msg = msg + "\nError Message: " + stdoutdata raise RuntimeError(msg)
上述代码中有STARTUPINFO相关内容,为了方便代码解释在此列出如下清单,清单中对每个结构体每个成员都通过注释的方式对成员的功能一一进行了解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 typedef struct _STARTUPINFO { DWORD cb; PSTR lpReserved; PSTR lpDesktop; PSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; PBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;
在原始代码中使用上述结构体的dwFlags 成员,该成员的值被设定为STARTF_USESHOWWINDOW ,意指使用wShowWindow这个成员属性,其他相关值,请参阅下列清单。这样做的目的是,当需要隐藏窗口时,结合示例代码,仅需要在代码中添加startupinfo.wShowWindow = sp.SW_HIDE即可,但因devenv这样的命令不包含窗体,为了保持简约,所以就没有写入这些代码。
1 2 3 4 5 6 7 8 9 10 表4-7 dwFlags 使用标志及含义 标志 含义 STARTF_USESIZE // 使用dwXSize 和dwYSize 成员 STARTF_USESHOWWINDOW //使用wShowWindow 成员 STARTF_USEPOSITION //使用dwX 和dwY 成员 STARTF_USECOUNTCHARS //使用dwXCountChars 和dwYCount Chars 成员 STARTF_USEFILLATTRIBUTE //使用dwFillAttribute 成员 STARTF_USESTDHANDLES //使用hStdInput 、hStdOutput 和hStdError 成员 STARTF_RUN_FULLSCREEN //强制在x86 计算机上运行的控制台应用程序以全屏幕方式启动运行
初步修改 我们要解决的“工具Tlbexp.exe无法找到”这个问题,就需要运行vsvars32.bat这batch文件,完成系统环境变量的更新。理论上仅需要在mComple这个函数中,即clean步骤前再次使用subprocess模块下的Popen方法来运行vsvars32.bat即可,相应代码应为:
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 import osimport subprocess as sp def mCompile (slnPath, compilArgs ): """[summary] Args: slnPath ([type]): [description] compilArgs ([type]): [description] Raises: RuntimeError: [description] ValueError: [description] """ vsvarsbatch = "C://Porgamm Files (x86)//Microsoft Visual Studio XX.X/CommonX//Tools//vsvars32.bat" compilerExe = "C://Porgamm Files (x86)//Microsoft Visual Studio XX.X/CommonX//IDE//devenv" startupinfo = sp.STARTUPINFO() startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW if vsvarsbatch != "" : cmd = [vsvarsbatch] stepVSARS = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.STDOUT, startupinfo=startupinfo) stdoutdata, stderrdata = stepVSARS.communicate() if stepVSARS.returncode != 0 : msg = "Error: Failed to raun vsvars32.bat" if stdoutdata != None : msg = msg + "\nError Message: " + stdoutdata raise RuntimeError(msg) if stepVSARS.wait() != 0 : raise ValueError(stderrdata.decode("mbcs" )) logClean = "clean.txt" stepClean = sp.Popen([compilerExe, "/clean" , compilArgs, slnPath, "/OUT" , logClean ], stdout=sp.PIPE, stderr=sp.STDOUT, startupinfo=startupinfo) stdoutdata, stderrdata = stepClean .communicate()
可现实是这样的代码达不到预期的结果,根本原因在于:程序中,进程之间的数据是隔离的,内存空间是不能共享的,进程之间要进行数据通信必须借助其他手段。子进程的结果父进程获取不到,其他的子进程当然也无法得到。
还会有疑问的地方可能是,可我们的问题焦点是系统的环境变量? 程序中,每个进程都有自己的运行环境,这样的环境只能从父进程被继承到子进程中,而不能反向地从子进程传递到父进程。实际上,默认情况下subprocess模块下的Popen方法一直践行着这种规则,Popen方法有env这个参数,该参数用于指定子进程的环境变量,如果 env = None,子进程的环境变量将从父进程中继承。也就是说,如果环境变量不加修改,那么子进程的环境变量与父进程一致,与其他子进程也一致。由于环境变量传递的单向性,我们上述的初始修改代码是不能完成任务要求的。
既然常规方法不行,那就使用一个非常规 的方式,我们可以使用被称为“外带数据 ”(out-of-band)的进程通讯方式,将子进程中环境变量的变化传递给父进程。
最终修改 针对初始修改中依旧存在的问题,我们再次对代码进行了修改。最终版本如下:
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 import osimport subprocess as spdef mCompile (slnPath, compilArgs ): """[summary] Args: slnPath ([type]): [description] compilArgs ([type]): [description] Raises: RuntimeError: [description] ValueError: [description] """ vvsvarsbatch = "C://Porgamm Files (x86)//Microsoft Visual Studio XX.X/CommonX//Tools//vsvars32.bat" compilerExe = "C://Porgamm Files (x86)//Microsoft Visual Studio XX.X/CommonX//IDE//devenv" startupinfo = sp.STARTUPINFO() startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW if vsvarsbatch != "" : cmd = [vsvarsbatch, '&&' , 'set' ] stepVSARS = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.STDOUT, startupinfo=startupinfo) stdoutdata, stderrdata = stepVSARS.communicate() if stepVSARS.returncode != 0 : msg = "Error: Failed to raun vsvars32.bat" if stdoutdata != None : msg = msg + "\nError Message: " + stdoutdata raise RuntimeError(msg) if stepVSARS.wait() != 0 : raise ValueError(stderrdata.decode("mbcs" )) output = stdoutdata.decode("mbcs" ).split("\r\n" ) dict_new_env = dict ((e[0 ].upper(), e[1 ]) for e in [p.rstrip().split("=" , 1 ) for p in output] if len (e) == 2 ) os.environ.update(dict_new_env) logClean = "clean.txt" stepClean = sp.Popen([compilerExe, "/clean" , compilArgs, slnPath, "/OUT" , logClean ], stdout=sp.PIPE, stderr=sp.STDOUT, startupinfo=startupinfo) stdoutdata, stderrdata = stepClean .communicate()
在这次修改中,代码cmd = [vsvarsbatch] 变成了cmd = [vsvarsbatch, ‘&&’, ‘set’]**,因为vsvars32.bat脚本对环境变量的更新,是没有 标准输出(stdout)的,即直接是看不到变化的,所以这里使用 set,将子进程所有环境变量都打印到 标准输出中,当 组合命令**运行结束之后,在检查命令是否异常结束之后,通过stdoutdata获取set命令的标准输出,然后在将得到的标准输出转换为字典格式,再然后更新当前的环境变量,这样父进程中的环境变量得到了更新。之后代码中的子进程都会使用更新之后的环境变量,这样就达到了任务要求,解决了任务难题。
小结 个人认为,项目发生变化会带来新的问题,这并不意外。解决问题时,初始思考的不全面也是正常的,遇见问题认真分析仔细研究终究会找到解决问题的方法的。当然,解决一个问题的可以有很多不同的方法,上述方法也仅仅是一家之言,并非最优,若读者有别的思路也欢迎关注我的个人微信公众号,一起讨论学习。