2011年初開始做一個項目,開始體驗使用微軟網站發布工具來發布網站。在服務器端安裝發布服務后,可以在Visual Studio界面中右鍵點擊Web項目,再點發布,第一次填好發布設置,以后就可以實現一鍵發布,雖然還有不少高級功能沒有用到,不過已經方便得不敢相信了。敏捷開發的一個要素不就是每日構建嗎,開發過程中,每天下班前Check In代碼(Visual Studio裝了 Anksvn插件 ),再發布到服務器上,連一分鐘都不用。
具體步驟這里不介紹了,大家有興趣可以看下 Scott Guhire的博客 。順便說一下,那個WebPlatform Installer要比我當時逐個網上搜索下載方便多了,卻要你先安裝.Net 2.0,明顯無理要求嘛,我只裝了.Net 4.0。只要把安裝包文件提取出來,再改下其config文件讓其兼容4.0就可以了。
按計劃過年前,要發布Beta版本,幾名領導會來觀看演示。可就在演示前,出現了麻煩,站點怎么也部署不上去了。出現下面的錯誤:
折騰了一個多小時,終于想到之前發布都是成功的,可能因為在上線前一天,改了很多東西。于是我在給我幫忙的實習生電腦上試了下,上面的代碼還是舊的,結果她那邊可以發布成功。我拿到舊代碼,在本機同樣成功。其實本來直接將站點手工復制到服務器上也沒什么大不了,但我這個人比較愛鉆牛角尖,既然排除了的發布工具或服務器端突然秀逗的原因,那就只能是代碼的原因。于是采用折半排除大法,排除一部分文件在項目外,再嘗試發布。直到最后,才找到罪魁禍首-正是web.config文件。有點出乎意料,原以為這個文件不用編譯,直接復制就可以。
原因也找到了,是前一天將web.config的<appsettings>加了許多項,很多項中含有轉義字符如“><&”之類,刪掉這些項就可以了,然后把web.config手工拷到服務器網站根目錄下。
演示進行得比較順利,領導們接著又提出了幾項新功能。過年回來后,盡管忙得不可開交,而我還一直糾結著這個令人摸不著頭腦的錯誤。
隨著站點部署上線,開發告一段落,我終于騰出手來,想把這個問題搞個水落石出。
首先要找到Microsoft.Web.Publishing.Tasks這個程序集,和一般.Net Framework程序集的不同,它是在C:\Program Files\MSBuild\Microsoft\VisualStudio\v10.0\Web目錄下。根據錯誤信息,用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);? //錯誤拋出源 logger.LogError(uri.LocalPath, exception.LineNumber, exception.LinePosition, exception.Message, new object [0]); flag = false ; }
一目了然,這個方法處理異常的代碼出現了的邏輯問題。項目已經上線,我目的已不是讓遠程發布順利完成,而是找到真正的錯誤原因。XmlException是從哪里拋出的呢?對于我這種只用IDE調試的菜鳥來說,有點麻煩。
馬上想到的,是用一個程序加載此程序集,通過拋出異常的InnerException屬性的堆棧,應該能找到異常源。問題是怎么啟動這個程序集,我鄉下人,怕黑不喜歡研究命令行。這里我找到一個不錯的借口,即使找到異常源,也沒法去調試(這不是.Net Framework一部分,沒有源代碼下載的)。
要想調試代碼,還得求助于Reflector。不過這個程序集估計有幾萬行代碼,不能指望代碼導出來,想把它改得編譯通過有點懸。所以爭取只導出最少的,與錯誤相關的代碼。既要定位到異常源,還要獲悉源方法的上下文變量。束手無策的看了兩天源代碼,終于決定從IL入手。在園子里,看過多位朋友寫過如何改造VSPaste插件,既然同是.Net程序集,我應該也可以在ParameterizeTransformXml.Execute方法中塞一點東西,曝光其一些運行時的真相。
臨淵羨魚,不如退而結網。耐下性子,學了幾天IL語法基礎,練了幾個示例,然后準備開刀了。由于reflector導出的IL也有問題,所以手術刀還是用ildasm工具,直接轉儲就可以了。會生成三個文件,擴展名為res和resources的兩個不用管,只打開擴展名il的那個文件,有4兆多,所以還是用一個給力點的文本編輯器吧,我用的是NotePad++。
找到Execute方法,我選了幾個可能的關鍵點,分別插入了一段IL代碼,來將下一個函數調用的參數值保存到日志文件中。代碼可以先用C#寫好,在Reflector中查看編譯的程序集,把IL復制過去,再根據上下文修改下如何獲取要保存的參數即可。比如這行代碼:xmlTarget = OpenSourceFile( this .Source, this .sourceIsFile),對應的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)??
//
將棧頂的兩個字符串合并成一個(原來棧有三個變量,現為兩個)
??????? call void [mscorlib]System.IO.File::AppendAllText(string, string)
//記錄日志,現在
棧被清空
??????? 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命令進行編譯,又修正一些錯誤,基本都復制多或少一塊造成的。編譯成功后,將原位置的Microsoft.Web.Publishing.Tasks.dll文件備份后替換掉,在Visual Studio中發布,卻又報錯了,說“簽名不匹配”,無法加載dll。
趕緊又一頓搜索,將程序集IL中.hash語句刪除,再編譯,替換,重啟VS,發布,果然成功了!還是顯示原來的錯誤,不過剛才嵌入的IL代碼,如同打入敵人堡壘內部的同志,通過log文件中,成功地送出了致命的情報。
運氣非常好,因為日志文件中只多了兩行,說明還就是OpenSourceFile方法出錯了。Source屬性正是網站項目web.config文件的絕對路徑,sourceIsFile值為True。在Reflector中進入OpenSourceFile方法,更簡單,只有聊聊幾行:
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方法,目標精確已夠,可以收網抓捕嘍。接著可以建一個測試工程,就把這個類,以及與其相關的類代碼,從Reflector中拷貝出來。看上去代碼量也不少,不過有些比如說是用來寫Xml,可以直接去掉。編譯通過后,F5調試運行。終于,剝開重重迷霧后,這次看到異常的廬山真面目:XmlAttributePreservationDict類ReadPreservationInfo(string elementStartTag)方法拋出的XmlException-“ 有未閉合的字符串。 第 3 行,位置 47 。”
異常源找到了,接著要找原因。由于在調試狀態,直接可以看到方法參數傳進的值出了問題:雖然還不明白這個方法的目的,但elementStartTag不應該一個被截斷的Xml節點字符串,而這個節點,正是那天導致發布失敗的修改中加入的<appsettings>下的一個<add>節點。
仔細一看web.config文件中那個出錯的節點,頓時讓我氣得不打一處來。原來對節點中屬性中的Html標簽中的一個尖括號,沒有作轉義處理。如今實習生實在是太靠不住了,差點被害死,我還特別強調過要小心轉義符號啊。
當然,微軟的代碼肯定也有毛病,雖然轉義特殊字符是標準做法,但尖括號是居于屬性引號中,沒理由不能正確解析。實際上,XmlTransformableDocument類的基類XmlDocument(大家都很熟悉了吧),就不存在這種問題。
ReadPreservationInfo不是最終元兇,真正的元兇是調用它的幕后黑手,我揪它出來,給大家示眾:
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 ; } }
這段代碼不長,從個人角度說,我傾向于用一個全局的而不是局部的StringBuilder變量。導致本文問題出現在while語句的判斷條件上,0x3e正是右尖括號的ASC碼,以尖括號的出現作為節點結束標志,顯然沒有做到完全的嚴謹。不知道是微軟的開發人員偷工減料,還是對XML的理解有點小偏差。其實,個人覺得發布網站有必重寫這么多東西嗎?
其實,在.Net Framework中對XML操作的核心類-XmlReader,幾乎是滴水不漏。看上去命名空間一個是Microsoft,一個是System;我看到了一個是程序員,一個是大師,你感覺到了嗎?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

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