天天看点

记一次当前工作目录问题的排查经历

最近在使用clearcase的时候遇到一个问题,当从命令行里启动版本树,并想给一个节点打上review属性时,经常会出现一个命令窗口一闪而过,刷新版本树之后却没能找到想要打的review属性,只有再次尝试才会正确打上。大家忍受了这个问题很久,但一直都没时间去深入分析它。在连续几次遇到这情况之后,我觉得忍无可忍,下定决心解决它,最终找到了问题的根源并给出了解决方案,在这里详细记录一下这次排查的经历。

clearcase资源管理器,和windows资源管理器很相似

命令行,直接输入<code>clearvtree file_path</code>

因为流程方面的要求,在每次提交一份代码后,必须经过相应的单元测试,由提交者打上<code>unittested</code>属性,然后交给他人review,如果没问题,他会打上<code>reviewed</code>属性,否则提交者需要再重复这一过程只到问题解决为止。只有在这两份属性共同存在的情况下,新版本才能被允许进入build中,这从很大程度上保证了代码的质量。

然后在windows的<code>sendto</code>目录下创建一个快捷方式<code>reviewed</code>,指向这个脚本。于是,要给一个文件创建属性时,只需:

用前面介绍的任一种方式启动版本树

在版本树中对目标节点点击右键,然后在<code>sendto</code>菜单下选择创建的<code>reviewed</code>选项

一切都显得很正确,但用了一段时间之后,很多人发现一个现象,打开版本树后,经常点了<code>reviewed</code>选项,发现一个命令窗口一闪而过,刷新却看不到刚创建的属性,然后只有再试一次才能成功,更为奇怪的是这个问题并不总是出现。

要解决这个问题,这里有三点需要解答:

为什么问题不是每次都出现?

为什么第一次没有成功,出了什么错误?

为什么刷新一次之后就可以了?

这里首先看看第一次到底出了什么错误。由于命令窗口一闪而过,无从知道发生了什么,所以要么重定向脚本,要么让脚本执行完之后停下,而不是关闭退出,这样才能够得到错误信息,这里采用了简单的暂停脚本方案。找到脚本文件,在最后加上<code>pause</code>,再试一次创建属性,得到了错误信息(注:这里假设文件是:<code>q:\project\src\test.cpp</code>,版本是<code>main\7</code>,所以在clearcase里面整个文件的路径为<code>q:\project\src\test.cpp@@\main\7</code>):

找不到文件?在命令行中运行:

明明文件存在,怎么会显示找不到这个文件呢?

想到之前出现过在clearcase中无法找到文件的情况:访问大量文件时偶尔会出现无法找到个别文件的情况,那是由于网络的问题。因为clearcase采用的是集中式的版本控制,我们创建view的方式是<code>dynamic</code>,而不是<code>snapshot</code>,所以所有的数据实际上存在于clearcase服务器上,客户端想要访问一个文件时,会通过网络协议去服务器获取,取回本地之后才能够访问。如果出现大量的文件访问,网络又不是特别好的情况下,就可能会出现传输失败,从而无法访问文件的情况。

于是用<code>ccadminconsole.msc</code>命令去查找clearcase所有的log,结果真在<code>clearcase\my host\server logs\view</code>下找到了一些可疑的log:

难道是这个原因?去网上搜索一阵也无法得到有用的信息。

但是转眼又想,既然刚刚是由于这个文件还没有取回本地导致的,如果我再次打开版本树,并尝试创建<code>review</code>属性,是否就应该没问题了?抱着这种想法又试了一次,结果还是和刚才情况一样,无法找到文件,于是否定了这个猜测。

放弃了上面的猜测之后,又进行另一种猜测,会不会是<code>sendto</code>的机制有些没弄明白的地方?如果不用<code>sendto</code>的这种方式,而直接用前面介绍的命令,会有一样的结果么?于是在命令行中调用:

竟然成功执行,那么问题可能就出现在<code>sendto</code>上。接着猜想,难道是由于<code>sendto</code>的实现机制有问题?如果不用<code>sendto</code>,而是直接在命令行里调用这个脚本可以么?于是把上面的命令保存成一个脚本,放在<code>c:\test\reviewed_by_me.bat</code>,再调用一次:

仍然成功。

等等,这和<code>sendto</code>的方式还有一点区别,<code>sendto</code>是的确是调用了这个脚本,但是它是通过一个快捷方式来调用的,而不是直接运行脚本。为了达到一样的实验条件,我也创建一个快捷方式:<code>reviewed</code>,指向上述脚本。双击之后,发现果然出错了,一样的无法找到文件!

现在的问题就变成了双击以快捷方式打开的脚本和直接从命令行里启动的脚本有什么区别?也许大家看到这里就能猜到原因了,但是当时我还没有立刻意识到,而是同时思考了另外一个问题,为什么把版本树刷新一次又可以了呢?刷新前后都调用的是同样的<code>sendto</code>,这次刷新前后有些什么区别?

为了得到更多的信息,将<code>reviewed_by_me.bat</code>中的命令执行前都打印输出,执行两次之后注意到了问题所在。这是第一次失败时的结果:

这是刷新之后成功执行的结果:

细心的你可能已经发现了这个区别,成功的那一次多了一个q:\,是一个绝对路径,失败的是相对路径,难道问题就出在这里?那为什么前面直接在命令行中用这个相对路径也能正确执行呢?

这个路径是怎样来的?

在前面调用<code>clearvtree</code>时用的是:

难道是这个原因?于是我试了一次输入绝对路径给<code>clearvtree</code>:

然后再创建一次<code>review</code>属性,真的成功了!

通过前面的猜测,现在的问题就定位在相对路径与绝对路径上,文章刚开始的三个问题变成:

为什么相对路径会出错?

为什么同样是相对路径,在命令中的调用不出错,而通过<code>sendto</code>就出错了?

为什么刷新之后相对路径变绝对路径了?

我们知道任意一个进程在处理一个相对路径时,为了正确的访问文件,都需要另一个重要的参数:<code>当前工作目录</code>,进程,更准确的说是操作系统会将相对路径扩展成一个绝对路径再进行处理:

前面都是我们的推测,到了该印证推测的时候了。

从命令行中的调用和<code>sendto</code>的方式结果不一致入手。 由于它们的相对路径一样,那么问题一定是出在<code>当前工作目录</code>上,先来看命令行的方式,从头到尾我只开了一个命令行窗口,它的工作路径是:<code>q:\</code>,因此,在这里面调用的子进程默认都会继承同样的<code>当前工作目录</code>,所以传给它们的相对路径最终都会扩展成为正确的路径:

再来看<code>sendto</code>,因为这里调用的是一个<code>快捷方式</code>,它有一个特点,可以指定自己的<code>start in</code>参数,下图是<code>sendto</code>中<code>reviewed</code>快捷方式的属性:

记一次当前工作目录问题的排查经历

这里的<code>start in</code>属性指定的就是调用命令时的<code>当前工作目录</code>,它的值是<code>m:\admin\tools\review</code>,因此一个相对路径扩展之后变成:

这个路径当然是错误的,这就可以解释为何<code>sendto</code>的方式会出错了。

前面介绍过打开版本树通常有两种方式,除了刚刚讨论的命令行,还可以从<code>clearcase资源管理器</code>中打开,这种情况不存在我们讨论的问题,因为点击打开版本树选项之后,资源管理器直接将完整的路径传给了<code>clearvtree</code>进程,因此不管它的当前工作目录位于何处,都可以正确的处理。这也可以解释文章最开始提出的问题:为什么这个问题有时出现,有时不出现?

对于问题3,没有找到相关的资料,推测可能是由于刷新之后,<code>clearvtree</code>进程内部将路径扩展,这是程序内部实现的问题,在这里不作讨论。

找到问题之后,应该如何解决呢?想到两种解决思路:

把<code>当前工作目录</code>设置成与<code>clearvtree</code>一样

在<code>reviewed_by_me.bat</code>脚本中将相对路径扩展成绝对路径

对于方案二,打算扫描每个view,然后去匹配路径,但这样速度一定很慢,而且还无法保证正确性,放弃。

那么只有采用方案一,现在的问题变成,如何获得<code>clearvtree</code>的当前路径?在<code>reviewed</code>调用脚本的时候,只能从<code>clearvtree</code>那里获得文件的相对路径,存储在<code>%1</code>中。工作目录从哪里获取到呢?

在前面讨论中,一个进程调用子进程时,默认情况下子进程的<code>当前工作目录</code>会与父进程一样,什么是默认情况?其实就是子进程没有明确指定自己<code>当前工作目录</code>的情况,而这里的<code>reviewed</code>快捷方式是设置了<code>start in</code>,这样就意味着如果将它清空,脚本便会从<code>clearvtree</code>中获得正确的<code>当前工作目录</code>,根据这个分析开始验证:

清空<code>reviewed</code>快捷方式的<code>start in</code>

在<code>reviewed_by_me.bat</code>脚本中输出<code>当前工作目录</code>: <code>for /f %%i in ('cd') do echo %%i</code>

本来期盼着得到:<code>q:\</code>,结果却出乎意料,输出的是:

这根本不是一个目录,不知为何<code>clearvtree</code>将脚本的工作路径设置成了这样一个奇怪的”目录”。但是不管怎么样,起码我们可以得到一个大概的路径,知道它在<code>q:\</code>盘下,现在的相对路径是:

可以从前面的<code>当前工作目录</code>中得到盘符,然后去猜测相对路径,或者拿相对路径与<code>当前工作路径</code>去匹配,计算得到一个正确的路径……

这种方法可以得到正确的结果,但是有些复杂,最终没有采用,因为发现了一种更为简单的<code>workaround</code>。

上面的奇怪”目录”是怎么来的,注意看该节点完整的路径:

对比可以发现,其实是由于操作系统不知道<code>clearcase</code>采用的文件命名方式:

操作系统将上面<code>clearcase</code>内部路径当成了一个普通的文件路径,<code>版本号7</code>被当成了文件名,而从右边开始的第一个<code>版本号分隔符\</code>被当成了<code>路径分隔符</code>,操作系统去除”文件名”,所以才出现了这种奇怪的”目录”。

为了印证这一点,我又对另外一个节点创建<code>reviewed</code>属性:

这是在<code>main</code>上的一个<code>test</code>分支,再次选择<code>sendto</code>下的<code>reviewed</code>快捷方式,得到下面的<code>当前工作目录</code>:

看到这里想必大家都明白我要做什么了,没错,这个奇怪的”目录”其实就差一个<code>版本号</code>就可以构成一个完整的节点路径,而<code>clearvtree</code>传过来的<code>%1</code>参数就包含了这个<code>版本号</code>,截取之后拼在上面的目录后就可以得到完整的路径,这里是最终的<code>reviewed_by_me.bat</code>代码:

(2014.06.06更新)

因为有时并不是只对已经archive的节点进行review和unittest,而是会直接操作于checkout文件。那么此时便没有了上面的<code>版本号</code>一说,直接使用上面的代码会出现问题。还是用上面的例子来说明:

此时,假设test.cpp是checkout的文件,我们想对其增加review和unittest属性。这里得到的当前工作目录是:

路径完全正确,没有了前面<code>@@main</code>引起的干扰。所以文件的完整路径就可以这样得到:

但是如果直接用前一节的<code>reviewed_by_me.bat</code>脚本,却会得到这样的结果:

这里少了后缀名,为什么呢?问题出在代码中的<code>set version_num=%%~ni</code>,因为<code>%~ni</code>只表示文件名,因此只有<code>test</code>出现。如果想要取得完整的路径,那么只用再加上后缀名即可,即将<code>set version_num=%%~ni</code>改成<code>set version_num=%%~nxi</code>,仅仅增加一个<code>x</code>。因为这种情况下后缀名为空,因此也不会影响前面说的archive文件。

所以,最终版本的<code>reviewed_by_me.bat</code>代码变为:

(2014.08.22更新)

因为有时会不使用<code>sendto</code>快捷方式来打标签,而是直接调用脚本,此时,当前工作目录可能会出错,为了避免这种情况,脚本又做了以下更新:

测试亦作了相应的更新。

最后对下面几种方式启动的版本树进行了测试,所有的情况都成功的运行:

至此,该问题成功解决。

这是一次平常众多工作中遇到的一个细小的问题,通过一点点的排除,顺藤摸瓜,最终找到了问题的根源。这篇文章非常详尽(啰嗦似乎更合适)的通过自问自答的方式,记录了整个分析过程。对我来说,重要的不是解决了这个问题,而是训练自己遇到事情不去忍受,想办法解决的意识。

记得刚刚参加工作时,由于要分析程序崩溃的原因,用一个命令每次一行的分析backtrace文件,通过内存地址去得到堆栈的确切位置。每次都需要拷贝地址,修改命令参数,执行,通常要执行五六次以上才能够找到有用的信息。但当时好像每个人都认同这种方式,不就是简单的几步操作么,没觉得有多麻烦啊,我也一样。后来一个哥们写了一个简单的shell脚本,输入为backtrace文件,一次把所有的内存地址转换成文件的确切位置,看到这个脚本时立刻被震撼到了,不是这个脚本有多复杂,而是除了他,没有任何人想到这点,所有人都在忍受,甚至连忍受都感受不到,麻木的接受一切。

其实每一份忍受都源自于对现状的不满足,各行各业存在的目的不就是为了解决人们各种各样的不满足么,一个公司能否持续发展不也是看它能否不断发现并满足人们的不满足么?换种说法,这种不满足也就是需求。面对需求我们应该做些什么?发现并抓住它,有可能就成为机遇;忽略它,可能就变成了抱怨。人的追求是无止尽的,现状永远都满足不了人,所以我一直认为,只要存在不满足的地方,就一定存在着机遇,问题在于我们怎样面对它们。我们领导经常说这样一句话:别抱怨,提建议;别建议,解决它。我很喜欢这句话,送给大家,与君共勉,做一个有心人。

(全文完)

继续阅读