2011年初開始做一個(gè)項(xiàng)目,開始體驗(yàn)使用微軟網(wǎng)站發(fā)布工具來(lái)發(fā)布網(wǎng)站。在服務(wù)器端安裝發(fā)布服務(wù)后,可以在Visual Studio界面中右鍵點(diǎn)擊Web項(xiàng)目,再點(diǎn)發(fā)布,第一次填好發(fā)布設(shè)置,以后就可以實(shí)現(xiàn)一鍵發(fā)布,雖然還有不少高級(jí)功能沒有用到,不過已經(jīng)方便得不敢相信了。敏捷開發(fā)的一個(gè)要素不就是每日構(gòu)建嗎,開發(fā)過程中,每天下班前Check In代碼(Visual Studio裝了 Anksvn插件 ),再發(fā)布到服務(wù)器上,連一分鐘都不用。
具體步驟這里不介紹了,大家有興趣可以看下 Scott Guhire的博客 。順便說(shuō)一下,那個(gè)WebPlatform Installer要比我當(dāng)時(shí)逐個(gè)網(wǎng)上搜索下載方便多了,卻要你先安裝.Net 2.0,明顯無(wú)理要求嘛,我只裝了.Net 4.0。只要把安裝包文件提取出來(lái),再改下其config文件讓其兼容4.0就可以了。
按計(jì)劃過年前,要發(fā)布Beta版本,幾名領(lǐng)導(dǎo)會(huì)來(lái)觀看演示。可就在演示前,出現(xiàn)了麻煩,站點(diǎn)怎么也部署不上去了。出現(xiàn)下面的錯(cuò)誤:
折騰了一個(gè)多小時(shí),終于想到之前發(fā)布都是成功的,可能因?yàn)樵谏暇€前一天,改了很多東西。于是我在給我?guī)兔Φ膶?shí)習(xí)生電腦上試了下,上面的代碼還是舊的,結(jié)果她那邊可以發(fā)布成功。我拿到舊代碼,在本機(jī)同樣成功。其實(shí)本來(lái)直接將站點(diǎn)手工復(fù)制到服務(wù)器上也沒什么大不了,但我這個(gè)人比較愛鉆牛角尖,既然排除了的發(fā)布工具或服務(wù)器端突然秀逗的原因,那就只能是代碼的原因。于是采用折半排除大法,排除一部分文件在項(xiàng)目外,再嘗試發(fā)布。直到最后,才找到罪魁禍?zhǔn)祝莣eb.config文件。有點(diǎn)出乎意料,原以為這個(gè)文件不用編譯,直接復(fù)制就可以。
原因也找到了,是前一天將web.config的<appsettings>加了許多項(xiàng),很多項(xiàng)中含有轉(zhuǎn)義字符如“><&”之類,刪掉這些項(xiàng)就可以了,然后把web.config手工拷到服務(wù)器網(wǎng)站根目錄下。
演示進(jìn)行得比較順利,領(lǐng)導(dǎo)們接著又提出了幾項(xiàng)新功能。過年回來(lái)后,盡管忙得不可開交,而我還一直糾結(jié)著這個(gè)令人摸不著頭腦的錯(cuò)誤。
隨著站點(diǎn)部署上線,開發(fā)告一段落,我終于騰出手來(lái),想把這個(gè)問題搞個(gè)水落石出。
首先要找到Microsoft.Web.Publishing.Tasks這個(gè)程序集,和一般.Net Framework程序集的不同,它是在C:\Program Files\MSBuild\Microsoft\VisualStudio\v10.0\Web目錄下。根據(jù)錯(cuò)誤信息,用Reflector翻出了ParameterizeTransformXml.Execute方法。
bool flag = true ; IXmlTransformationLogger logger = new TaskTransformationLogger( base .Log, this .StackTrace); XmlTransformation transformation = null ; XmlTransformableDocument xmlTarget = null ; try { logger.StartSection(SR.GetString( "BUILDTASK_TransformXml_TransformationStart" , new object [] { this .Source }), new object [0]); xmlTarget = OpenSourceFile( this .Source, this .sourceIsFile); logger.LogMessage(SR.GetString( "BUILDTASK_TransformXml_TransformationApply" , new object [] { this .Transform }), new object [0]); transformation = OpenTransformFile( this .Transform, this .transformIsFile, logger); this .storageDictionary.TokenFormat = this .TokenFormat; this .storageDictionary.UseXpathToFormParameter = this .UseXpathToFormParameter; transformation.AddTransformationService( this .storageDictionary.GetType(), this .storageDictionary); flag = transformation.Apply(xmlTarget); if (flag) { logger.LogMessage(SR.GetString( "BUILDTASK_TransformXml_TransformOutput" , new object [] { this .Destination }), new object [0]); this .resultXml = SaveTransformedFile(xmlTarget, this .Destination, this .destinationIsFile); } } catch ( XmlException exception) { Uri uri = new Uri (exception.SourceUri);? //錯(cuò)誤拋出源 logger.LogError(uri.LocalPath, exception.LineNumber, exception.LinePosition, exception.Message, new object [0]); flag = false ; }
一目了然,這個(gè)方法處理異常的代碼出現(xiàn)了的邏輯問題。項(xiàng)目已經(jīng)上線,我目的已不是讓遠(yuǎn)程發(fā)布順利完成,而是找到真正的錯(cuò)誤原因。XmlException是從哪里拋出的呢?對(duì)于我這種只用IDE調(diào)試的菜鳥來(lái)說(shuō),有點(diǎn)麻煩。
馬上想到的,是用一個(gè)程序加載此程序集,通過拋出異常的InnerException屬性的堆棧,應(yīng)該能找到異常源。問題是怎么啟動(dòng)這個(gè)程序集,我鄉(xiāng)下人,怕黑不喜歡研究命令行。這里我找到一個(gè)不錯(cuò)的借口,即使找到異常源,也沒法去調(diào)試(這不是.Net Framework一部分,沒有源代碼下載的)。
要想調(diào)試代碼,還得求助于Reflector。不過這個(gè)程序集估計(jì)有幾萬(wàn)行代碼,不能指望代碼導(dǎo)出來(lái),想把它改得編譯通過有點(diǎn)懸。所以爭(zhēng)取只導(dǎo)出最少的,與錯(cuò)誤相關(guān)的代碼。既要定位到異常源,還要獲悉源方法的上下文變量。束手無(wú)策的看了兩天源代碼,終于決定從IL入手。在園子里,看過多位朋友寫過如何改造VSPaste插件,既然同是.Net程序集,我應(yīng)該也可以在ParameterizeTransformXml.Execute方法中塞一點(diǎn)東西,曝光其一些運(yùn)行時(shí)的真相。
臨淵羨魚,不如退而結(jié)網(wǎng)。耐下性子,學(xué)了幾天IL語(yǔ)法基礎(chǔ),練了幾個(gè)示例,然后準(zhǔn)備開刀了。由于reflector導(dǎo)出的IL也有問題,所以手術(shù)刀還是用ildasm工具,直接轉(zhuǎn)儲(chǔ)就可以了。會(huì)生成三個(gè)文件,擴(kuò)展名為res和resources的兩個(gè)不用管,只打開擴(kuò)展名il的那個(gè)文件,有4兆多,所以還是用一個(gè)給力點(diǎn)的文本編輯器吧,我用的是NotePad++。
找到Execute方法,我選了幾個(gè)可能的關(guān)鍵點(diǎn),分別插入了一段IL代碼,來(lái)將下一個(gè)函數(shù)調(diào)用的參數(shù)值保存到日志文件中。代碼可以先用C#寫好,在Reflector中查看編譯的程序集,把IL復(fù)制過去,再根據(jù)上下文修改下如何獲取要保存的參數(shù)即可。比如這行代碼:xmlTarget = OpenSourceFile( this .Source, this .sourceIsFile),對(duì)應(yīng)的IL是:
??????? IL_0042:? ldarg.0
??????? IL_0043:? call?????? instance string Microsoft.Web.Publishing.Tasks.ParameterizeTransformXml::get_Source()
??????? IL_0048:? ldarg.0
??????? IL_0049:? ldfld????? bool Microsoft.Web.Publishing.Tasks.ParameterizeTransformXml::sourceIsFile
??????? IL_004e:? call?????? class Microsoft.Web.Publishing.Tasks.XmlTransformableDocument Microsoft.Web.Publishing.Tasks.ParameterizeTransformXml::OpenSourceFile(string,bool)
在其前面插入:
??????? ldstr "D:\\pub.log"????
//
將字符串加載到棧上
??????? ldarg.0???????????????????
//
加載自己(this)的引用到棧上
?????????????????
?? call?????? instance string Microsoft.Web.Publishing.Tasks.ParameterizeTransformXml::get_Source()
//讀取屬性到棧上
??????? ldstr "[Source]\r\n"
??????? call string [mscorlib]System.String::Concat(string, string)??
//
將棧頂?shù)膬蓚€(gè)字符串合并成一個(gè)(原來(lái)?xiàng)S腥齻€(gè)變量,現(xiàn)為兩個(gè))
??????? call void [mscorlib]System.IO.File::AppendAllText(string, string)
//記錄日志,現(xiàn)在
棧被清空
??????? ldstr "D:\\pub.log"
??????? ldarg.0
??????? ldfld????? bool Microsoft.Web.Publishing.Tasks.ParameterizeTransformXml::sourceIsFile
//讀取字段
??????? box bool
?
//裝箱
??????? ldstr "[sourceIsFile]\r\n"
??????? call string [mscorlib]System.String::Concat(object, object)
??????? call void [mscorlib]System.IO.File::AppendAllText(string, string)
小心冀冀花了半天,修改完保存,用ilasm命令進(jìn)行編譯,又修正一些錯(cuò)誤,基本都復(fù)制多或少一塊造成的。編譯成功后,將原位置的Microsoft.Web.Publishing.Tasks.dll文件備份后替換掉,在Visual Studio中發(fā)布,卻又報(bào)錯(cuò)了,說(shuō)“簽名不匹配”,無(wú)法加載dll。
趕緊又一頓搜索,將程序集IL中.hash語(yǔ)句刪除,再編譯,替換,重啟VS,發(fā)布,果然成功了!還是顯示原來(lái)的錯(cuò)誤,不過剛才嵌入的IL代碼,如同打入敵人堡壘內(nèi)部的同志,通過log文件中,成功地送出了致命的情報(bào)。
運(yùn)氣非常好,因?yàn)槿罩疚募兄欢嗔藘尚校f(shuō)明還就是OpenSourceFile方法出錯(cuò)了。Source屬性正是網(wǎng)站項(xiàng)目web.config文件的絕對(duì)路徑,sourceIsFile值為True。在Reflector中進(jìn)入OpenSourceFile方法,更簡(jiǎn)單,只有聊聊幾行:
private static XmlTransformableDocument OpenSourceFile( string sourceFile, bool isSourceFile) { XmlTransformableDocument document2; try { XmlTransformableDocument document = new XmlTransformableDocument { PreserveWhitespace = true }; if (isSourceFile) { document.Load(sourceFile); } else { document.LoadXml(sourceFile); } document2 = document; } catch ( XmlException exception) { throw exception; } catch ( Exception exception2) { throw new Exception (SR.GetString( "BUILDTASK_TransformXml_SourceLoadFailed" , new object [] { exception2.Message }), exception2); } return document2; }
追蹤到了XmlTransformableDocument的Load方法,目標(biāo)精確已夠,可以收網(wǎng)抓捕嘍。接著可以建一個(gè)測(cè)試工程,就把這個(gè)類,以及與其相關(guān)的類代碼,從Reflector中拷貝出來(lái)。看上去代碼量也不少,不過有些比如說(shuō)是用來(lái)寫Xml,可以直接去掉。編譯通過后,F(xiàn)5調(diào)試運(yùn)行。終于,剝開重重迷霧后,這次看到異常的廬山真面目:XmlAttributePreservationDict類ReadPreservationInfo(string elementStartTag)方法拋出的XmlException-“ 有未閉合的字符串。 第 3 行,位置 47 。”
異常源找到了,接著要找原因。由于在調(diào)試狀態(tài),直接可以看到方法參數(shù)傳進(jìn)的值出了問題:雖然還不明白這個(gè)方法的目的,但elementStartTag不應(yīng)該一個(gè)被截?cái)嗟腦ml節(jié)點(diǎn)字符串,而這個(gè)節(jié)點(diǎn),正是那天導(dǎo)致發(fā)布失敗的修改中加入的<appsettings>下的一個(gè)<add>節(jié)點(diǎn)。
仔細(xì)一看web.config文件中那個(gè)出錯(cuò)的節(jié)點(diǎn),頓時(shí)讓我氣得不打一處來(lái)。原來(lái)對(duì)節(jié)點(diǎn)中屬性中的Html標(biāo)簽中的一個(gè)尖括號(hào),沒有作轉(zhuǎn)義處理。如今實(shí)習(xí)生實(shí)在是太靠不住了,差點(diǎn)被害死,我還特別強(qiáng)調(diào)過要小心轉(zhuǎn)義符號(hào)啊。
當(dāng)然,微軟的代碼肯定也有毛病,雖然轉(zhuǎn)義特殊字符是標(biāo)準(zhǔn)做法,但尖括號(hào)是居于屬性引號(hào)中,沒理由不能正確解析。實(shí)際上,XmlTransformableDocument類的基類XmlDocument(大家都很熟悉了吧),就不存在這種問題。
ReadPreservationInfo不是最終元兇,真正的元兇是調(diào)用它的幕后黑手,我揪它出來(lái),給大家示眾:
internal class XmlAttributePreservationProvider { ... .... public XmlAttributePreservationDict GetDictAtPosition( int lineNumber, int linePosition) { if ( this .reader.ReadToPosition(lineNumber, linePosition)) { int num; StringBuilder builder = new StringBuilder (); do { num = this .reader.Read(); builder.Append(( char )num); } while ((num > 0) && ((( ushort )num) != 0x3e)); if (num > 0) { XmlAttributePreservationDict dict = new XmlAttributePreservationDict (); dict.ReadPreservationInfo(builder.ToString()); return dict; } } return null ; } }
這段代碼不長(zhǎng),從個(gè)人角度說(shuō),我傾向于用一個(gè)全局的而不是局部的StringBuilder變量。導(dǎo)致本文問題出現(xiàn)在while語(yǔ)句的判斷條件上,0x3e正是右尖括號(hào)的ASC碼,以尖括號(hào)的出現(xiàn)作為節(jié)點(diǎn)結(jié)束標(biāo)志,顯然沒有做到完全的嚴(yán)謹(jǐn)。不知道是微軟的開發(fā)人員偷工減料,還是對(duì)XML的理解有點(diǎn)小偏差。其實(shí),個(gè)人覺得發(fā)布網(wǎng)站有必重寫這么多東西嗎?
其實(shí),在.Net Framework中對(duì)XML操作的核心類-XmlReader,幾乎是滴水不漏。看上去命名空間一個(gè)是Microsoft,一個(gè)是System;我看到了一個(gè)是程序員,一個(gè)是大師,你感覺到了嗎?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
