Quantcast
Channel: Darkthread
Viewing all 428 articles
Browse latest View live

Windows驗證歷程觀察與Kerberos/NTLM判別

$
0
0

昨天談到IIS整合式Windows驗證會優先嘗試Kerberos,不行再改用NTLM,那麼如何得知現在用的驗證方式是哪一種?

瀏覽器的F12開發工具雖然有HTTP往來記錄,但不會顯示驗證過程,因此,Fiddler才是最佳觀察工具。

為了捕捉標本,我特地用Hyper-V架了AD,還學會用「setspn -a  HTTP/機器名稱 AppPool執行帳號」註冊SPN的技巧,費了好大力氣,終於做出Kerberos驗證。以下介紹如何用Fiddler觀察Windows驗證過程:

首先測試Kerberos,URL要用機器名稱,讀取一個GIF作為簡單測試。當一切符合要求(SPN、網域內、機器名稱),瀏覽器就會使用Kerberos驗證。

由Fiddler可觀察到前文提過的「兩次401+一次200」歷程。第一次Server先回傳HTTP 401,而Response Header提示IIS支援Negotiate及NTLM兩種認證方式,此時瀏覽器會跳出帳號密碼對話框等待使用者輸入帳號密碼:

使用輸入帳號密碼後,瀏覽器發出第二個Request,包含Authorization Header,IIS再次回應HTTP 401,Response包含WWW-Authenticate Header。

切到Fiddler獨有的Auth頁籤,可以看到詳細資訊,Request Header裡包含的就是前文提過的Kerberos Ticket,而Response Header回應則是Kerberos Reply:

後續發出的Request內含Kerberos Ticket,順利取回GIF圖檔:

 

接著,我們把URL的機器名稱換成IP,刻意違背Kerberos要求,預期將採用NTLM。第一個HTTP 401 Response Header一樣提示IIS支援Negotiate及NTLM兩種Provider:

輸入帳號密碼後瀏覽器發出第二個Request,Header包含Authentication資訊,401 Response則有WWW-Authenticate Header:

切到Auth頁籤可看出NTLM與Kerberos的明顯差異:Kerberos在第二次Request時送出是Kerberos Ticket,意味著Client端已向DC完成身分認證,直接向Server出示身分證明;而採用NTLM驗證時,第二個401 Response回覆的是NTLM Challenge,代表此時才開始用使用者的帳號密碼對Challenge內容進行演算,隨後得到200的Request才會送出針對Challenge的Response結果。

第三個Request已正確回傳HTTP 200取得圖檔,而Request Auth頁籤裡有玄機,其中橘底文字的完整內容為:Target Information block provided for use in calculation of the NTLMv2 response. 在此可完整見證NTLM Challenge/Response的運作。

如果沒有Fiddler可用,看不到HTTP 401歷程,要怎麼區別瀏覽器是走NTLM還是Kerborse。有一密技-當你在Request Authorization Header看到「TlR」字首,就代表目前使用的是NTLM(如圖所示):
註:此一技巧來自MSDN Blog,但該文提到Kerberos標頭固定為YII與我的觀察不同,NTLM為TlR倒是所語不假。

學會這招,用Chrome F12開發者工具也能看出NTLM囉~

NTLM/Kerberos觀察技能點數+1,收工。

參考資料:


【茶包射手日記】Windows睡眠、關機後風扇續轉

$
0
0

記錄這兩天遇到的鬼問題。

【聲明】因處理過程龐瑣且某些操作屬不可逆,無法反覆驗證追出真兇,本文僅整理處理經驗供參。

前陣子將家裡的PC升級成SSD,重灌Windows 10後,SATA硬碟傳輸速度有點怪(這是另一則奇妙故事,一言難盡,有機會再寫,此處略過細節避免失焦),主機板Asus P8H67-M EVO已有5年歷史(這篇文章還有它的照片哩),BIOS五年沒更新過,值得試試。

Asus網站驅動程式分門別類做得挺好,雖是五年前的舊產品,很快就在下載網頁找到BIOS更新,最新版本是2013/5/3,也有三年之久,心想新版總比舊版強,更新肯定有益無害,動手吧!將檔案放在USB行動碟,進BIOS開機選單就可選取指定韌體更新,挺方便。更新過程需重新開機兩次(後來發現,2012/4/27那次改版本有提「*從之前舊版本升級到此版時需要刷新兩次Bios,完成後需要CLRTC。」),更新完成原本的設定會跑掉,需進入選單重設Intel VT、AHCI模式、開機硬碟順序等。

進入Windows 10,發現硬碟速度未解,就跑Intel驅動程式更新公用程式下載更新晶片組驅動程式,問題還是沒解決,還冒出USB失效的問題。(此時我已累積兩項變更:BIOS更新、晶片組驅動更新)

測試到心灰意冷,準備找時間再戰,按下睡眠鈕,發現一件超可怕的事情!電腦進入睡眠狀態後,螢幕關閉、主機電源燈閃爍(為該主機板進入睡眠狀態的標準行為),但機殼風扇跟電源供應器(PSU)風扇依舊轉個不停,跟開機中沒兩樣,而且用鍵盤、滑鼠,甚至電源鈕都無法喚醒,只能長按電源鈕或拔電。更恐怖的是,就連關機也一樣,螢幕關閉後,電源燈長亮,機殼風扇跟PSU風扇繼續轉呀轉,按鈕按鍵均無反應,只能關電重開。更!怎麼會搞成這樣,心中滿是狂奔的羚羊~

病急亂投醫,我試了以下方法:

  • 改換稍舊一點的BIOS,試過2012/11/28、2012/06/07、2012/04/27版本,無效!而且換回較舊版本有個問題,開機時會出現「CPU Over Tempature Error」(但當時CPU溫度才55度),只能按F1再進入BIOS操作畫面無法繼續開機,必須取消「出錯時等待F1鍵」選項才能正常開機。換了其他版本BIOS,Windows 10睡眠、關機後不斷電的問題仍在。
  • 想還原回出廠年份的舊版BIOS,很抱歉,系統提示該版本BIOS過舊無法更新。(猜想2012/4/27那次改版做了不可逆動作,才需要重開機兩次,所以我回不去了…)
  • 爬文找解,換成舊版Intel晶片組程式(9.2.0.1025),意外解決USB驅動失效問題,但關機不斷電問題仍在。
  • 爬文找解,有人說跟Intel Management Engine Interface有關,停用該裝置亦沒改善。
  • 回到上次Windows更新的還原點(這裡要推一下Windows 7起新增的還原點功能,很讚,救了我好幾次),無效。

最後,想到先前BIOS下載說明提到需要CLRTC我沒有做(原以為更新後設定跑光,BIOS背後已自己清掉CMOS),乖乖打開機殼跳Jumper,重開機後沒直接測試,還試了爬文找到的一招,取消「啟用快速啟動」功能(順便也啟用了「休眠」)。由於我還原到未裝Windows更新的還原點,還跑了Windows自動更新重開機。想想,該做的都做了,再試一次,膽顫心驚按下睡眠鈕,風扇聲嘎然而止,哈里路亞~拎杯把它修好了。

不過,再次重新勾選「啟用快速啟動」、取消「休眠」,關機睡眠功能還是正常,故無從驗證是清CMOS的效用、BIOS更新後重跑Windows Update,還是停用過快速啟動的功勞。射茶包過程只要一次混雜兩種以上變因,就會讓挖掘真相變得困難,這是不變的鐵律。但如果重來一次,我會一次只做一個動作,耐心記錄對照找出真相嗎?不會!被電腦不能關機搞得我一肚子火,只想快點恢復正常,就像病危之際哪來的心情研究對照不同藥劑的藥效?推測可能有效的通通打下去,快點把人救起來比較重要吧?

茶包射手只會穩健騎馬或步行,跟在一群羚羊後面狂奔的,一定不是~

Windows驗證時成群出現的HTTP 401

$
0
0

前面我們介紹了Kerberos/NTLM驗證,也實地觀察過HTTP 401、401、200的歷程。登入網站只輸入一次密碼,想當然爾同一個身分驗證可用來存取後續的css、js、jpg、gif… 若是每次讀檔都401、401、200重新走一次,未免太沒效率。

隨便開個Windows驗證的網頁,Fiddler側錄結果卻又讓人迷惑。如下圖範例,網頁default.aspx引用一堆js、css,就觸發了成串HTTP 401,這回是401後接200,兩個Request/Response往返就完成。但也有例外,像是第5個/AfaClient/css/jquery.tab.css就沒出現401,一次200搞定,這到底是怎麼一回事?

用個實驗來說明:

<html>
<head>
<title>HTTP 401 Test</title>
</head>
<body>
<button>Test</button>
<scriptsrc="http://code.jquery.com/jquery-2.2.3.min.js">
</script>
<script>
    $("button").click(function() {
        $.get("/magicwand.gif?t=" + Math.random());
    });
</script>
</body>
</html>

test1.html是個簡單網頁,按鈕後用jQuery.get()取回magicwand.gif圖檔(URL加上亂數避免快取)。test1.html、magicwand.gif二者都需Windows驗證。以下是載入網頁後按鈕三次的Fiddler側錄結果:

除了/Test1.html載入時歷經401、401、200,之後讀取magicwand.gif都是一次200到位不囉嗦,關鍵在這裡:

Test1.html回傳Response Header中有個Persistent-Auth: true,宣告這條連線可以沿用身分認證,而Request Header約定連線採Keep-Alive模式,讀完資料後保留TCP連線供後續使用(節省重建連線成本,提高效率)。於是再送出的GET magicwand.gif就不需要驗證,快速通關。如下圖所示,Request沒有包含Authorization Header,就能直接得到HTTP 200。

由以上可知,連線再驗證後就不需要401、200的過程,可以直接200取回內容。

再看另一個實驗:

<html>
<head>
<title>HTTP 401 Test</title>
</head>
<body>
<button>Test</button>
<scriptsrc="http://code.jquery.com/jquery-2.2.3.min.js">
</script>
<script>
    $("button").click(function() {
for (var i = 0; i < 32; i++) {
            $.get("/magicwand.gif?n=" + i + 
"&t=" + Math.random());
        }
    });
</script>
</body>
</html>

這回我們狠一點,一口氣同步送出32個$.get(),結果就不太一樣了:

初期出現一大堆401,但後期就清一色都是200。仔細數數,扣除Test2.html的兩個401,還出現11次401。為什麼?記得剛才有提到「Response出現Persistent-Auth: true 代表這條Keep-Alive連線可沿用身分認證」,而當我們同時發出多個HTTP請求時,瀏覽器不會呆呆的只用一條連線抓完這個再抓下一個,而會建立多條連線同時下載。這個觀念之前在IE MaxConnectionsPerServer參數效果實測一文曾研究過,當時測試的對象是IE8,預設最大同時連線數是6條,我沒找到記載 IE11@Windows 10最大同時連線數預設值的文件,但由觀測結果來看是12條。回頭數數第一張圖中的401數目,扣除default.apsx有兩個401,總數也是12個。所以,每次新建連線後要走一次401、200過程,之後靠Persistent-Auth沿用驗證,就不要需要401。

另外補充,IIS是否允許Perisistent-Auth是可以設定的,詳情可參考MSDN文件,而NTLM與Kerberos的設定是分開的,NTLM預設就有,Kerberos需要額外開啟

結論:使用Windows驗證時,Perisitent-Auth功能允許一條連線只需驗證一次,後續不必每次先401再200,以提升效能。當瀏覽器同時發出多個HTTP Request時,背後會新建多條HTTP連線,每條新建連線必須先走一次401再200的驗證步驟,後續則可免除401的過程。IIS預設NTLM已開啟Persistent-Auth,Kerberos則需額外設定。

【茶包射手日記】NuGet Package Manager升級3.4.2.830後無法登入私服

$
0
0

同事報案,使用Visual Studio 2015 NuGet連私服時一直彈出帳號登入對話框無法連上(公司的NuGet私服設成Windows驗證),另一位同事與我卻無此問題。比對後發現大家NuGet Package Manager版本不同,出問題的同事是3.4.2.830,我是3.3.0.167,另一位沒問題的同事則是3.4.1。

大膽假設:我們的NuGet Server是多年前架設的舊版,與新版NuGet Package Manager不相容。

在NuGet Package Manager for Visual Studio 2015下載頁面,看到網友對NuGet Package Manager新版的惡評如潮,罵聲不絕,評價只剩1.6顆星,前幾則就有網友提到連線私服(Private Package Source)出現問題,心中浮現不祥預感。

但,不親身試試又如何找出真相,發揮神農氏精神,一咬牙升上3.4.2.830,哈!果然升級3.4.2.830後就無法登入私服,一直跳出帳號登入畫面。心想也可能是NuGet Server太舊,利用這個機會升級Server也好。翻出滿佈灰塵的Source Code,發現原本版本是1.7,現已出到2.1,便花了點功夫將NuGet Server升上2.10.1。

很不幸,NuGet Server升級無法解決新版NuGet Package Manager無法登入私服的問題,證實此仍3.4.2的Bug!但我的NuGet Package Manager卻已壯烈犠牲,啊啊啊啊~(神農氏倒臥在地,口吐白沫抽搐不已,手裡握著一株姑婆芋…)

還好在 https://dist.nuget.org/index.html找到NuGet Package Manager的歷代版本,移除3.4.2版再重裝3.4.1,一切恢復原狀,至於遇到問題的同事,也建議他移除重裝舊版,結案。

TypeScript的this偵錯陷阱

$
0
0

接獲同事報案追查TypeScript問題,二人一起陷入迷霧近20分鐘才恍然大悟…

有段TypeScript程式自訂類別,在類別方法用this.PropName="..."修改自身屬性值(註:類似需求我習慣用self大法,寫成self.PropName="…"),偵錯時用瀏覽器F12開發者工具下指令檢查,卻發現this.PropName沒有被正確設定,我建議在程式碼加入console.log(this.PropName)交叉檢查,跑出更詭異的狀況,如下圖:

程式碼中的console.log(this.Name)得到"Jeffrey",在F12 Console下指令查this.Name卻得到undefined,WTF?

百思不得其解,許久之後才猛然想起-TypeScript的this語法糖!遇到類別方法內出現this,會偷偷將this換成_this,幫開發者節省另外宣告self、that形成Closure的麻煩。

留意上圖顯示的程式碼檔案是lab.ts而非lab.js,這是瀏覽器的德政,透過lab.js.map讓開發人員能用TypeScript原始碼偵錯,比使用編譯過的JavaScript更接近原本的演算法及程式邏輯。

開啟lab.js後真相大白。TypeScript Test()方法裡用的this,實際上已被TypeScript換成_this,永遠指向類別的Instance;而在setTimeout觸發函式的當下,從F12 Console下指令所存取的this則是DOM window,而非Player Instance,要改用_this.Name才對,TypeScript裡的this.Name有魔法加持,參考它拿來F12 Console測試,馬車變南瓜。之前幾乎都用self處理,一時不察便中招。

佛心的TypeScript為this加上魔法,貼心的瀏覽器提供TypeScript偵錯,粗心的開發者卻因此鬼打牆…

看完以上說明,一下this是Player,一下this是window,被搞得好亂,對吧?這也是我不愛TypeScript this魔法,寧可傻傻自己宣告self的理由。頭腦清楚時,還能記得this其實是_this,等那天射茶包射到天昏地暗或疲勞駕駛,難保不會再次跌坑。

評估之後,還是自己宣告self比較好。

【延伸閱讀】

【茶包射手日記】JS Bin的無窮迴圈保護機制

$
0
0

同事報案,用JS Bin跑迴圈計算從1加到n測試效能,發現 for 迴圈次數增加到100萬後加總結果不對,每次執行會得到小於正確值(499999500000)的隨機數字;但若不用for改用lodash _.times(),跑再多次結果也是正確的。

為了調查,先將程式碼簡化到可重現問題的最精簡內容:

var count = 1000000;
var sum = 0;
for (var i=0;i<count;i++)
      sum += i;
    console.log("Inline:"+sum);

發現一個現象,如果用<script>將程式內嵌進HTML,執行結果正確;要移到JavaScript區塊才會出錯,如下例:

Inline測試結果為499999500000,而JavaScript測試結果為每次不同的亂數,然後我還注意到下方有段警告:

Exiting ptoential infinite loop. To disable protection: add "// noprotect" to your code.

JS Bin說它「已跳出潛在無窮迴圈」!

研判這是JS Bin防止程式碼陷入無窮迴圈的機制,當迴圈連續執行超過一定次數就強行中止(聽起來頗神奇,不知是怎麼做到的),但後面的程式碼會繼續執行。中斷時機不一可以解釋為什麼每次跑出的數字不同,而_.times()的演算法不符合潛在無窮迴圈的標準,故未受影響。

要避免保護機制影響執行結果,在程式碼開頭加入// noprotect關掉保護,搞定收工!

從Visual Studio發布NuGet Package的好幫手-NuGet Packager

$
0
0

最近在寫共用元件,打算放在公司的NuGet私服供同事下載安裝,換版時還可自動更新,大大降低管理成本。講到製作NuGet Packet,NuGet Package Explorer雖然方便,但畢竟是GUI工具,我希望修改元件並測試OK後,直接在Visual Studio專案按個鍵就自動上傳到NuGet伺服器。經過評估,找到一個好用套件-NuGet Packager

我習慣修改元件後先手動丟上測試環境,測試一陣子沒問題再發布到NuGet伺服器,不要每次建置就發布,因此不適合將發布程序做成Build Task或寫成建置事件。NuGet Packager的做法是提供NuGet Package專屬專案,專案依據Package規格做好content、lib、tools、src目錄, 當需要額外加入Script、工具、內容檔案就放入對應的資料夾,檔案還能納入版控;實務上Package打包檔案的來源也可能來自其他專案項目或編譯結果,此時可在Package.nuspec使用相對路徑,編譯時直接由來源取得最新結果,不需要手動複製及同步,十分方便。(詳見隨後範例)

由於是獨立專案,我們可自由決定發布時機,而NuGet Packager採取的規則是:使用Debug模式編譯只產生.nupkg檔案,在Release模式則會編譯並上傳NuGet伺服器。

以下用簡單範例介紹NuGet Packager的使用方式。

首先,在Extensions and Updates搜尋"nuget packager"並安裝:

安裝之後,Visual Studio會多出一種專案型別-NuGet Packager:

在解決方案中新增一個NuGet Packager專案,專案的目錄結構如下:

content、lib、src、tools都是NuGet用來擺放不同用途檔案的資料夾,而tools裡也先預備好init.ps1、install.ps1、uninstall.ps1等安裝及解除安裝腳本範本。(如果不知道檔案該怎麼擺,可先用NuGet Package Explorer操作再觀察檔案結構)

下一步是編輯Package.nuspec:

<?xmlversion="1.0"?>
<package>
<metadata>
<id>MagicApiClient</id>
<version>1.0.0</version>
<title>MagicApiClient</title>
<authors>Jeffrey</authors>
<owners></owners>
<description>某個魔法API的客戶端元件</description>
<releaseNotes>
</releaseNotes>
<summary>簡化魔法API呼叫邏輯的神祕元件</summary>
<language>en-US</language>
<projectUrl>http://blog.darkthread.net</projectUrl>
<iconUrl>httq://some-server//Icons/magic.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<licenseUrl>http://opensource.org/licenses/Apache-2.0</licenseUrl>
<copyright>Copyright  2016</copyright>
<dependencies>
</dependencies>
<references></references>
<tags></tags>
</metadata>
<files>
<filesrc="lib\"target="lib"/>
<filesrc="..\MagiApiClient\bin\Debug\MagicApiClient.*"target="lib"></file>
<filesrc="tools\"target="tools"/>
<filesrc="content\"target="content"/>
</files>
</package>

需要修改的欄位包含id、version、title、description、summary、projectUrl、iconUrl,另外範本預設會將放在lib、tools、content下的所有檔案打包。前面提到.nuspec可以設成直接抓取來自其他專案的內容,在以上範例我加了一個<file src="..\MagiApiClient\bin\Debug\MagicApiClient.*" target="lib"></file>,將MagicApiClient專案的bin\Debug\MagicApiClient.dll、MagicApiClient.pdb、MagicApiClient.xml打包裝入lib,如此其他專案引用時會有Intellisense提示函式、參數說明,出錯時還會顯示程式碼位置。

改完Package.nuspec,用Debug模式編譯,相關檔案就會打包成MagicApiClient.1.0.0.nupkg放在NuGet.Packager專案根目錄,已可手動上傳至NuGet Server,手動上傳方式可以參考之前的介紹

手動上傳有點遜,當然要做到點幾下滑鼠就自動上傳才酷,有幾個步驟:

  1. 編輯NuGet.Packager專案的nuget.config檔案,將私服加入packageSources
  2. 如果你的NuGet私服有設API Key(建議要設,以免阿貓阿狗亂傳蓋檔或被惡意人士下毒),要先設定上傳時使用的API Key。有兩種做法:
    第一種是使用nuget.exe,透過nuget.exe setApiKey my-api-key -Source httq://intranet-server/NuGetServer/nuget 指定特定伺服器所用的API Key。設定後,NuGet會將API Key加密儲存在目前帳號的%appdata%\NuGet\NgGet.config,之後由該帳號上傳至該NuGet Server就會自動引用。
    另一種做法是將API Key寫入NuGet.Packager專案的nuget.config,由於NuGet採取加密後儲存,要先用nuget setApiKey設定,再從%appdata%\NuGet\NuGet.config抄apiKeys設定。不過,NuGet config檔加密時用的是使用者專屬加密金鑰,故無法在多開發者間共享API Key設定。

修改後的nuget.config如下例:

<?xmlversion="1.0"encoding="utf-8"?>
<configuration>
<apikeys>
<addkey="httq://intranet-server/NuGetServer/nuget"value="AQAAANC…34uA=="/>
</apikeys>
<packageSources>
<addkey="Private NuGet source"value="httq://intranet-server/NuGetServer/nuget"/>
</packageSources>
</configuration>

接者改用Relase模式編譯專案,MagicApiClient.1.0.0.nupkg就會自動上傳到NuGet伺服器囉~ 整個NuGet Package打包上傳的程序是不是變得簡單多了呢?NuGet Packager萬歲!

註:若NuGet Package已存在於NuGet Server,上傳的版號必須比現存版號高,否則會發生Package已存在無法覆寫的錯誤。

安裝NuGet Package時在web.config加入設定

$
0
0

第一次嘗試需要在web.config設定appSettings的共用元件,因此打包NuGet Package時要多加入修改web.config的安裝腳本,其中有些小眉角,我摸索了一陣子才搞定,以下是心得分享。

我要做的事是在appSettings裡新増一筆<add key="afa:WebApiUrl" value="Web API測試台網址" />,在NuGet Package的做法是在content目錄加入web.config.install.xdt及web.config.uninstall.xdt,NuGet便會在安裝及解除安裝時依據這兩個檔案的指示修改web.config。XDT(Microsoft Xml Document Transformation)是微軟發明的XML文件轉換規則,ASP.NET專案透過web.debug.config、web.release.config為偵錯及正式發布產生不同web.config,也是運用相同原理。參考

XDT的基本語法不難,本質上就是個XML,額外加註xdt:Transform="Insert"/xdt:Transform="Replace"之類的指令新増、覆寫或移除XML元素。NuGet官網有如何用XDT修改config的簡單介紹,不過我遇上一個小難題…

在web.debug.config應用情境中,web.config的內容也由我們掌握,是對已知的XML結構進行操作。而NuGet Package被安裝到各式專案時,當時的web.config長什麼樣子是未知的,故轉換邏輯必須很有彈性。例如:若web.config己經有<appSettings>,直接在其中新増一筆<add>即可;若還沒有<appSettings>,就要連<appSettings>一起新増,再插人<add>。

實測時,我就遇到web.config缺少<appSettings>導致安裝失敗的狀況。爬文找到一個xdt:Transform="InsertIfMissing",貌似能見機行事,發現缺少才新増,幾經嘗試不成,才發現很多人反應該篇文章示範的做法有問題,在stackoverlow上找到網友分享的一記妙招:先在XML文件開頭新増一個<appSettings>,將<add>加入文件中「最後一個<appSettings>」,有兩種情況:

  1. web.config原本就有<appSettings>,XML中有兩個<appSettings>,<add>被加在原本的<appSettings>,第一個<appSettings>是空的
  2. web.config原本沒有<appSettings>,XML中有一個<appSettings>其中包含剛才加入的<add>

最後,刪除沒有元素的<appSettings>,將第一種狀況產生的重複<appSettings>清除,大功告成,非常聰明。

我試出來的web.config.install.xdt如下,這才成功加入appSetting設定。

<?xmlversion="1.0"encoding="utf-8" ?>
<configurationxmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettingsxdt:Transform="InsertBefore(/configuration/*[1])"/>
<appSettingsxdt:Locator="XPath(/configuration/appSettings[last()])">
<addkey="afa:WebApiUrl"value="測試台Web API URL"xdt:Transform="Insert"/>
</appSettings>
<appSettingsxdt:Transform="RemoveAll"xdt:Locator="Condition(count(*)=0)"/>
</configuration>

解除安裝的web.config.uninstall.xdt相對單純

<?xmlversion="1.0"encoding="utf-8" ?>
<configurationxmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<addkey="afa:WebApiUrl"xdt:Transform="Remove"xdt:Locator="Condition(count(*)=0)"/>
</appSettings>
</configuration>

補記一個小眉角:反覆測試時,記得每次測試的NuGet Package版號要不同,不然Visual Studio用Cache住的舊版,害你做白工。(我是歷經「怎麼改都不成功,怒拔xdt錯誤居然相同」才頓悟 orz)


2016石碇馬

$
0
0

比起前兩年(20142015)搭配六月豔陽35度高溫燒烤,今年石碇馬辦在四月下旬,氣溫低了快十度,總爬升1600公尺又算得了什麼,二話不說又報了!

今年氣侯異常,近四月底依然涼爽,氣象預報氣溫為18-26度,不過30%下雨的機率有點討厭,早晨出門還在下雨,但仍稱得上是不錯的跑馬天氣。

大會會場在華梵大學,6:06起跑,比預計晚了幾分鐘。與賽人數不多(事後看完賽證明的統計數字,全馬跑者不及900人),是我偏愛的小而美賽事。

起跑點幾乎在賽道最高點,跑沒多久還能看到遠方山頭的偽雲海,美~

最開始一公里多的歡樂下坡結束,開始爬坡了。大家識趣地一起切成步兵模式,誰也不為難誰,明智選擇!呵。


茶園風景也是石碇馬特色之一。

去年令我印象深刻部署警力盤查抓逃犯的小廟旁超陡坡,今年風景大不同,民房跟小廟不知何故拆了。(下圖附上去年照片供對照)

 

廟內龍柱、石雕堆置在旁邊,不確定會遷走還是原地重建?為一探究竟,看來明年得再報一次了。XD

今年Cosplay的跑友不多,我只注意到發福的火影 XD

賽道有一段要繞兩圈,其中包含令人聞風喪膽的「螞蟻路」(小粗坑產業道路),超~級~陡~。雖然是下坡,但斜度近-30%,四月跑有個小缺點,地面長了青苔較為濕滑,只能小心翼翼踩著小碎步前進,浪費掉大好下坡加速的機會。照片中的前輩示範一招-倒著走下坡,著地時不會腳趾抵鞋尖,比向前走舒服,但一直轉頭看路脖子很酸,下回應該帶後照鏡來的。XD

小插曲:螞蟻路下來沒多久,有兩輛大會機車經過,聽到騎車裁判在討論沒看到第二名,經驗豐富如我,馬上判斷出這是第一名的前導車。沒看到跑者,這回被我推坑的忠孝哥,在我慫恿之下假掰地跟在前導車後方跑了一小段,享受跑者一生追尋的尊榮,並拍照留念。(腳沒鉤起來有失專業跑者風範是美中不足,期待下回改進 XD)

沒多久,第一名追上前導車,竟是名棕髮外國人,他還親切地回頭跟忠孝哥說了「加油」。 事後忠孝哥一直津津樂道「自己被第一名追上的故事」(根本是被人超前一整圈輾過吧?),然後打聽到第一名是位英國人,在這種路線跑出3:46的成績,我的天吶~

比賽途中遇上兩波不小雨勢,無可避免地鞋子濕透,後半馬轉為擔心鞋濕腳腫起水泡跟跑下坡抵腳趾頭會黑指甲,所幸幾年操練下來,雙腳已成老油條等級,安然過關。

 

倒是下雨天讓不少蚯蚓離家逃難,看到幾隻接近手指粗細的霸王級蚯蚓,我還把一條在路中央遊蕩的移到路邊,省得被人踩扁。(怕嚇到人就只放小圖,有求知精神的讀者請自行點開)

石碇馬的補給一向很優,但本次有新品-現煮餃子,品嚐後我當場宣佈一則重大消息:在我心中稱霸多年的「田中馬維力炸醬麵」自此讓出寶座,「石碇馬水餃」成為馬拉松補給界的王者。天雨濕冷之際,徒手抓起有點燙手的韭菜水餃沾辣椒醬塞進嘴裡,鹹甜辛辣溫度全都恰到好處,身心靈同時獲得滿足,或許也靠陡坡爬升1000公尺這道神奇佐料加持,無論如何,它設下了難以超越的障礙。不信?若非親身體驗過,我也不會相信,歡迎大家自己來試試。之後又吃了好吃的煮泡麵、現煎葱油餅… 開心~

狹窄山路沒法開回收車接人,途中看到有趣的殘念區立牌,是要落馬的人到後面先坐小板凳等機車接駁嗎?呵

每年經過都要拍照的塑模工廠,看到它表示終點近了。

有外國朋友幫忙掛獎牌,猜想是華梵大學的外藉同學。

最後5K催了點油門,6:29:35完賽,勉強保住630,成績排在全部選手的60%左右,差強人意。更衣帳有大水桶跟蓮蓬頭,水壓挺強,就順勢沖個冷水澡,氣溫低水冰了點,洗完精神都來了。

奇妙的完賽伴手禮組合:咖啡、魔術頭巾、紀念電子錶、一條根藥膏、濕紙巾、口罩、礦泉水~ :P

 

今年的獎牌跟紀念衫一樣走可愛風,第35馬入手。

【茶包射手日記】勿用UrlEncodeUnicode/escape

$
0
0

寫WebClient.DownloadString()時用了"some.aspx?t=" + HttpUtility.UrlEncodeUnicode("中文")寫法組網址及Query String參,遇到一些問題,學到一些知識,筆記之。

先來個範例好說明。為便於測試,我寫了一個超簡單的ChkQueryString.aspx傳回Request.Url.Query檢查URL查詢參數:

<%@ Page Language="C#"%>
<% Response.Write("QueryString=" + Request.Url.Query); %>

分別嘗試三種不同組裝中文查詢參數URL的做法,HttpUtility.UrlEncodeUnicode()、HttpUtility.UrlEncode()以及直接寫中文不編碼:

        static void Test2()
        {
            WebClient wc = new WebClient();
            var testUrl = "httq://localhost:30055/ChkQueryString.aspx";
            Debug.WriteLine(wc.DownloadString(
                testUrl + "?t=" + HttpUtility.UrlEncodeUnicode("測試")));
            Debug.WriteLine(wc.DownloadString(
                testUrl + "?t=" + HttpUtility.UrlEncode("測試")));
            Debug.WriteLine(wc.DownloadString(
                testUrl + "?t=測試"));
 
        }

執行結果如下:

UrlEncode()與直接寫中文的結果相同,都是轉為"測試"二字的UTF-8編碼,共六個Byte的ASCII碼(%xx),但UrlEncode()的16進位數字為小寫,寫中文交給DownloadString()轉碼的十六進位數字則為大寫。UrlEncodeUnicode()轉碼結果為%unnnn格式,但經過DownloadString()時又被轉了一次變成%25uxxxx(「%」被換成%25),多重轉碼造成參數在Server端無法正確還原。

為什麼DownloadString()可以正確處理UrlEncode()甚至原始的中文參數,處理%unnnn格式時確會出錯呢?

經過一番調查,得到一個結論:

UrlEncodeUnicode()包含Unicode字樣,貌似更先進,卻是過時的產物!勿用!

MSDN文件上,UrlEncodeUnicode()被標示為「注意:此 API 現在已經過時。/ Note: This API is now obsolete.」,理由是UrlEncodeUnicode()將多國語言文字轉成的%unnnn格式,對應的是JavaScript escape()函式的轉碼規則,而escape()從ECMAScript V3起就因無法完善支援多國字元被宣告成過時勿用[參考],應改用encodeURI()或encodeURIComponent()。現有多程式庫、API為保持向前相容或許還能解析,但不保證會繼續支援下去。例如:WebUtility.UrlDeocde()就拿掉了%unnnn解析邏輯:[參考]

// *** Source: alm/tfs_core/Framework/Common/UriUtility/HttpUtility.cs
// This specific code was copied from above ASP.NET codebase.
// Changes done - Removed the logic to handle %Uxxxx as it is not standards compliant.
privatestaticstring UrlDecodeInternal(stringvalue, Encoding encoding)

追進HttpUtility原始碼,DownloadString()內部使用Uri物件解析URL字串,處理時也不將"%unnnn"視為單一字元,導致%被轉碼為%25,由以下測試可驗證:


【結論】

  • 在JavaScript端,QueryString參數編碼請用encodeURIComponent(),勿再使用escape()。
  • 在C#端,請用HttpUtility.UrlEncode()取代HttpUtility.UrlEncodeUnicode()。

【茶包射手日記】Oracle Client版本與分散式交易

$
0
0

接獲報案,同事欲將測試網站移至新主機,遇到Oracle無法進行分散式交易的情況,得到以下錯誤訊息:

    Oracle.DataAccess.Client.OracleException
    Unable to enlist in a distributed transaction /無法列於分散式交易中

該網站尚有其他SQL分散式交易正常,單獨連線Oracle不參與分散式交易也正常,主機Oracle Client版本為ODAC121024(12.1.0.2),並確認已安裝Oracle Services for Microsoft Transaction Server。

先寫一小段程式進行對照,建立TransactionScope包入SQL及Oracle查詢,通過測試:

using (TransactionScope tx = new TransactionScope()) 
{
using (var cnSql = new SqlConnection("data source=SqlSvAr;user id=someone;password=****")) 
    {
        cnSql.Open();
        var cmd = cnSql.CreateCommand();
        cmd.CommandText = "select getdate()";
        var dr = cmd.ExecuteReader();
        dr.Read();
        Console.WriteLine(dr[0]);
    }
using (var cnOra = new OracleConnection("data source=OraSvrA;user id=someone;password=****"))
    {
        cnOra.Open();
        var cmd = cnOra.CreateCommand();
        cmd.CommandText = "select sysdate from dual";
        var dr = cmd.ExecuteReader();
        dr.Read();
        Console.WriteLine(dr[0]);
    }
    tx.Complete();
}

由此可確認該主機可支援Oralce分散式交易。下一步請同事修改測試範例,逐一換成問題程式所用的連線或物件,觀察到一個現象:將測試程式所連線的Oracle Server由OraSvrA換成OraSvrB,就會重現無法參與分散式分易錯誤。

比對後發現兩台Oracle Server版本不同,OraSvrA為11.2.0.2.0,有問題的OraSvrB為10.2.0.4.0,推測可能是新版Oracle Client 12.1.0.2搭配舊版Oracle Server之相容問題,依此方向爬文,找到一篇Oracle論壇討論提到極度類似狀況,依網友測試結果,問題出現在Oracle 12新版Client連線10.2.0.3及10.2.0.4等舊版Oracle Server,升級到10.2.0.5可解決:

…My team has been able to reproduce this problem with DB 10.2.0.3, but not with DB 10.2.0.5. If it's possible, I would recommend using 10.2.0.5 if you need an immediate solution. …

… the same issue to me in Oracle 10. @10.2.4. …

至此得到結論,OraSvrB為10.2.0.4,符合論壇所說的出錯情境。

不想糾結於舊版要不要升級,決定花時間將OraSvrB上的資料庫搬至OraSvrA,問題消失,收工!

克服入口網站內嵌其他網站之跨網站存取限制

$
0
0

文章標題有點饒舌難懂,直接說我需求就清楚了。我想在員工入口網站(例如:portal.utopia.com)加入人事、行政、會計、電子表單等現成網站功能,這些應用程式各有自己的網站(例如:webap.utopia.com),最簡單的整合方法是在入口網站放個Iframe將其他網站的網頁內嵌進來,兩分鐘搞定,用膝蓋就能完成。

BUT,人生最機X的就是這個BUT!

PM/老闆/使用者一定不會這麼簡單放過你,既然網頁已經整在一起,那麼切換樣式跟入口網站融為一體,審完表單入口網站的待審數字要減一,非常合情合理,應該難不倒你吧?不!瀏覽器跳出來說:「Over my dead body!」

母網頁跟Iframe網頁要溝通基本上不是難事,可用靠JavaScript操作另一方的DOM搞定,但若是兩個網頁分屬不同站台,問題就沒這麼單純。舉個實例,入口網站網頁httq://portal.utopia.com/SOP/container.aspx長這樣:

<%@ Page Language="C#" %>
 
<!DOCTYPEhtml>
 
<html>
<head>
<title></title>
<style>
        iframe { width: 320px; height: 240px; margin: 12px; }
</style>
</head>
<body>
<h5></h5>
<div>
<button>Get value from IFrame</button>
</div>
<iframeid="frmEmbedded"src="http://webap.utopia.com/SOP/Frame.aspx"></iframe>
<scriptsrc="https://code.jquery.com/jquery-2.2.3.min.js"></script>
<script>
        $("h5").text(location.href);
        $("button").click(function () {
            alert($("#frmEmbedded").contents().find("#txtValue").val());
        });
</script>
</body>
</html>

被嵌入的應用程式網頁httq://webap.utopia.com/SOP/embedded.aspx長這樣:

<%@ Page Language="C#" %>
<!DOCTYPEhtml>
<html>
<head>
<title>Frame</title>
<style>
        body { 
            background-color: #ddd;
        }
</style>
</head>
<body>
<h5></h5>
<inputid="txtValue"value="32767"/>
<scriptsrc="https://code.jquery.com/jquery-2.2.3.min.js"></script>
<script>
        $("h5").text(location.href);
</script>
</body>
</html>

我們希望按下Container.aspx <button>時可以讀取Embedded.aspx的<input id="txtValue">並顯示其值,當Container.aspx與Embedded.aspx分屬不同主機(portal.utopia.com與webap.utopia.com),由Container.aspx存取Embedded.aspx的行為將被瀏覽器禁止:

出現以下錯誤:

Uncaught SecurityError: Failed to read the 'contentDocument' property from 'HTMLIFrameElement': Blocked a frame with origin "httq://portal.utopia.com" from accessing a frame with origin "httq://webap.utopia.com". Protocols, domains, and ports must match.

這個限制來自瀏覽器的同源政策(Single Origin Policy),是瀏覽器防止惡意程式作怪的基本安全防線,前端攻城獅必須摸清它的特性。關於SOP的細詳解說,我推薦阮一峰先生寫的文章:浏览器同源政策及其规避方法,是我目前看到最淺顯完整的中文文件。

我的案例發生在公司內部,符合後端域名相同條件,幸運地可以靠Container.aspx/Embedded.aspx同時加上document.domain="utopia.com"克服。

如此,藉由加註document.domain,入口網站與應用程式網站的網頁在彼此存取對方的DOM時,瀏覽器視為個網站的兩個網站應用程式,就不會受到同源政策的限制囉~

【補充心得】

  1. document.domain="…"指定的內容必須與目前網址中的網域名稱相符。如果你在httq://portal.utopia.com的網頁中指定document.domain="darkthread.net",會出錯:Uncaught DOMException: Failed to set the 'domain' property on 'Document': 'darkthread.net' is not a suffix of 'utopia.com'.
  2. 必須雙方配合才能成功,除了入口網站要加,也要協調被嵌入網頁的開發單位配合在網頁上加入document.domain設定。
  3. 關於document.domain的網名值,建議不要寫死成字串,讓.NET或JavaScript自動由目前的網址抓取是上策。
    C#可以使用以下語法:
        string.Join(".", Request.Url.Host.Split('.').Skip(1).ToArray())
    JavaScript則複雜一點:
        /http(s)*:\/\/(.+?)\//i.exec(location.href)[2].split('.').slice(1).join(".")
  4. 要用這招,網址只能用網域名稱,不能用IP,故在公司進行內部測試時需配套措施:向DNS註冊測試主機,並限定使用者一律用網域名稱URL進行測試。

【茶包射手日記】瀏覽器播影片有聲無影處理經驗一則

$
0
0

家裡的電腦出現奇怪狀況,發現Chrome看臉書影片時聲音、進度條正常,但畫面全黑,重新開機亦無起色。

為了對照起見,做了以下測試:

  • Chrome播放YouTube正常
  • IE播放Facebook影片跟YouTube畫面全黑
  • Edge播放Facebook影片跟YouTube也畫面全黑

依老江湖多年經驗,這種狀況常與硬體加速功能有關,先關閉Chrome硬體加速再試試:

關閉硬體加速功能後,Chrome播放臉書影片功能恢復正常,確診為硬體加速問題。

再依據老江湖的經驗,遇到影片播放硬體加速問題,一定要先拆坐墊,不是啦,是先更新顯卡驅動程式。

檢查目前的顯卡驅動版本是2015/11/4出的,按「更新驅動程式」檢查得知已是最新版本。AMD網站有個驅動程式自動偵測工具也顯示驅動程式已是最新版本,難道老江湖也黔驢技窮了嗎?當然不,換個版本照樣能試手氣,在AMD網站找到2015/7/29釋出的15.7.1版,安裝完還沒重新開機,Chrome就已能正常播放Facebook影片,又試了IE、Edge,播放功能完全恢復。

補上更新後可正常運作的驅動程式版號供參:

閒聊-你敢不敢幫請假的同事編譯程式上線?

$
0
0

前幾天,參與的專案遇到緊急狀況,剛改版的系統有一段邏輯因正式台資料與預期不同而出錯,需要緊急換版,負責的同事因故無法即刻救援,改派我代打上陣。有一段時間沒參與,我對最新開發進度有點脫節,本次代打任務形同開發團隊的一次臨時抽考。

在我的開發機器開啟Visual Studio,先從TFS版控抓回最新的程式碼版本(Get Latest Version),檢視問題程式的修改歷史(View History),使用版本比對(Compare)功能找出本次修改位置,與PM確定規格後修正程式重新簽入(Check In),再利用先前設好的建置定義(Build Definition)由TFS Build Service抓取修改後程式碼編譯並部署到驗收測試環境,確認問題修復後由OP將程式上到正式台,危機解除,通過隨堂測驗!(下圖為TFS常用版控功能的示意)

任務完成後內心升起小小成就感:寫了十幾年專案,一路學習更完善的原始碼管理(雖然沒有很積極,而且多是同事主推,我拿香跟著拜 :P),終於有這麼一天-當事人不在,專案其他成員都也敢放膽修改程式上版,不擔心版本錯亂!

想到一項有趣但中肯的原始碼版本管控水準衡量指標:「你敢不敢幫請假的同事編譯專案上線?」

依我的觀點,要實現的關鍵有二:程式碼永遠只從版控取最新版本、編譯上版作業一律由自動程序執行。

前者是目標,後者為手段。每次上線建置一律由版控系統抓最新版,可避免上線程式混入某人硬碟才有的特有版本。不能精準掌握線上系統的原始碼版本,意味著不保證下次還能編譯出一模一樣的程式。「上次明明修過的Bug,換版後又冒出來」、「Bug修好了,上個月加的新功能卻消失了」… 一直是開發團隊的惡夢。

解決這個問題要從「禁止使用來路不明程式碼編譯上線」做起,任何時候,要上線的程式只能來自版控系統最新版本,只要不是從版控抓的就叫「來路不明」,防堵「要靠某人電腦D糟才能編譯正確版本」的危機。

即便所有程式碼都進版控,只要編譯過程允許人為介入,還是可能產生疏漏,因此要做到「編譯上版作業一律由自動程序負責」。人工操作常隱含陷阱,像是編譯前忘記抓最新版、編譯出錯時為求省事靠手動排除。前者會造成版本錯亂,後者讓其他人無法重新編譯出相同結果,都很惱人。要徹底解決這個問題,「人類閃開,讓機器來」-使用自動化編譯程序,程序一律由版控取回最新版編譯後再部署到測試或正式主機,不需要任何人為介入。若遇上編譯失敗或結果不正確,只能靠修改版控裡的程式碼克服。如此,才能確保任何人啟動自動編譯部署的結果都相同。

整理一些執行要點:

  1. 版控!版控!版控!
    不管是你是用VSS、TFS或是Git/Github,都好,確認所有重要專案都有進版控,少了這一步,其餘免談。版控系統確保所有人的開發成果都被彙整妥善保管,必要時可以追溯修改軌跡,甚至能改變所有的錯,讓我從頭改過…
    另外,一旦開始依賴版控,別忘了為版控系統規劃備份機制,省得天有不測風雲,所有努力化為青煙。
  2. 不要Check In任何還不打算上線的程式碼
    程式碼一旦Check In,就會被編譯進要上線的版本,但實務上不可能等到程式全部改好測完才Check In,開發過程三不五時要Check In才好保留軌跡。針對較複雜的修改或新功能開發,可在版控切出Branch並行開發,完成後再Merge回線上版本,在Branch裡即可自由Check In/Check Out,不必擔心影響正式系統。
    還有一種做法是一開始就Branch出開發版跟上線版,開發人員永遠Check In進開發版,要上線時再逐一列舉項目Merge回上線版。這種做法更嚴謹,隔離性更佳,但每次上線要詳列更動項目,手續較為繁瑣。
  3. 貫徹「編譯上線工作一律由機器執行」
    前面提到編譯上線不交給人工處理,才能貫徹「永遠使用版控的最新版,誰來執行都得到相同結果」,也不會出現「忘記Get Latest Version」這類失誤(某中年程序員慚愧低頭 orz)。要執行自動編譯上線程序,我們過去用過CruiseControl.NET,現在則逐步改用TFS Build Service。開Visual Studio人人都會編譯,改成自動程序時要改用MSBuild,雖然不難,但仍需要花點時間學習。

當然,在軟體工程領域,這一步之後還有更美妙的境界-整合自動測試,每次編譯後先跑測試,驗證功能無誤後再上線。到此境界,不要說替同事編譯上線,就連保全大哥、茶水間阿姨、林志玲、陳妍希… 人人可以幫系統上線不擔心出亂子,很美吧!而我們離那塊流著奶與蜜之地,仍有西天取經般的距離。

有很長一段時間,每次編譯上線都提心吊膽版本錯亂,到今天進步到「膽大包天敢代替同事上程式」,能達此境界老骨頭內心已十分有感,也祝福大家早日走到這一步。

Dapper出現sql_variant is incompatible with ntext

$
0
0

有個古老資料庫,裡面還有NTEXT型別欄位(SQL 2005加入NVARCHAR(MAX)後,應該沒人想用TEXT/NTEXT了),用Dapper執行一段SQL更新NTEXT欄位,發生古怪錯誤。

指令如下:

cn.Execute("UPDATE SomeTable SET NTextField = @data WHERE Id = 1", new { data = "…" });

錯誤訊息為:sql_variant is incompatible with ntext

System.Data.SqlClient.SqlException (0x80131904): Operand type clash: sql_variant is incompatible with ntext
   於 System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   於 System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   於 System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   於 System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   於 System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   於 System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
   於 System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)
   於 System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)
   於 System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   於 Dapper.SqlMapper.ExecuteCommand(IDbConnection cnn, CommandDefinition& command, Action`2 paramReader) 於 D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs: 行 2802
   於 Dapper.SqlMapper.ExecuteImpl(IDbConnection cnn, CommandDefinition& command) 於 D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs: 行 1060
   於 Dapper.SqlMapper.Execute(IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Nullable`1 commandTimeout, Nullable`1 commandType) 於 D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs: 行 995
   於 Managers.ApiService.UpdateBlah(Guid id, String userId, String reason) 於 X:\TFS\Boo\MAIN\src\WebApi\ApiService.cs: 行 847

第一次看到sql_variant型別,依文件所說,sql_variant常用於資料行、參數、變數及使用者定義函數的傳回值中,不能用來儲存varchar(max)、nvarchar(max)、text、ntext、xml… 等,最大長度為8016 Bytes。

推測Dapper在底層用sql_variant來存放@data參數字串,在指定給NTextField欄位時產生錯誤。爬文找不到有人反應類似問題,猜想原因是會Dapper的新世代不會摸到NTEXT,而存取NTEXT欄位的程式還停在ASP、VB6、ADO.NET世界打滾,像我這樣用3D印表機印尪仔標,算是少數中的少數。 XD

福至心靈,既屬Dapper內部行為,這様算Bug嗎?如果是Bug,後來有修正嗎?用NuGet Manager查Dapper版本還停在1.26.0,而最新版已到1.42.0。

處理軟體茶包的SOP,先更新到最新版再說。更新至1.42.0,不藥而瘉,結案~


小密技-在IIS主機現場撰寫測試ASPX偵錯

$
0
0

ASP.NET Web Application Project(WAP)與 Web Site Project(WSP)之間有一段有趣的消長演進:ASP.NET 1.0/1.1時代的ASP.NET網站要先編譯成DLL才能執行,稱之為Web Application Project;ASP.NET 2.0起推出Web Site Project架構,採用Code-Beside,不需事先編譯,Blah.aspx與Blah.aspx.cs一起放上IIS網站就能運行。雖然開發者還是可以選擇用WAP寫網站,但WSP改完存檔就能立刻看結果顯然比較迷人,於是WSP成為較多人的選擇。而到了ASP.NET MVC時代,Controller架構依賴事先編譯,加上WAP同時支援WebForm、MVC、WebAPI,於是WAP再次取代WSP回歸主流。延伸閱讀:Web Site Project vs Web Application ProjectWeb Site Project為何沒落?

近幾年來,新專案都已改用WAP,WebForm、MVC進可攻退可守,網頁執行前都需要編譯,WSP那個「寫兩行程式存檔就能看測試結果」的時代似乎已一去不返… 其實沒有!ASPX即時編譯的功能一直都在,也是本篇要分享的IIS網站除錯小密技。

實務上,不少場合我們還是非常需要ASPX的「隨寫隨測」功能,像是射IIS茶包,開Notepad寫幾行測完,絕對完勝開Visual Studio改程式編成DLL再丟到IIS,快了豈止十倍。舉幾個我自己遇過的例子:

  • 懷疑正式台因appSetting設定值不對出錯,想檢查設定值是否為預期內容
  • 想做個TransactionScope包DbConnetion測試確認分散式交易環境正常
  • 在正式台想知道實際使用的ODP.NET元件版本、DLL路徑
  • 列舉檢查正式主機是否欠缺必要的DbProvider

以上狀況,都要靠寫幾行程式丟上主機執行獲得解答。在測試開發環境還簡單,用Visual Studio寫幾行程式部署到IIS執行便知分曉。若問題發生在正式環境,部署過程會複雜一些,例如:若上版流程已進化到只能透過自動編譯部署,為了查問題測個小東西要簽入版控重上程式,等測完再還原未免荒謬。而從本機另編一顆DLL送上正式機,測完再換回來也沒簡單到哪裡去。

面對類似情境,我會在IIS Web的目錄新増一個test.aspx,用Notepad記事本輸入以下內容:(以檢測DbProvider為例)

<%@ Page Language="C#" %>
<ol>
<%
  System.Data.DataTable dt =
  System.Data.Common.DbProviderFactories.GetFactoryClasses();
for (int i = 0; i < dt.Rows.Count; i++)
    Response.Write("<li>" + dt.Rows[i][2] + "</li>");
%>
</ol>

薑!薑!薑!薑~

是不是重現了WSP時代那種「隨寫隨測」的敏捷性?而且測完刪除test.aspx就恢復原樣(提醒:記得要將測試程式刪除,以免被誤用產生風險),省去置換DLL還要還原的麻煩,又可以現改現看,輕巧方便許多。

最後再補充兩則小技巧,應該很多人像我一樣,用Notepad少了Intellisense就不會寫程式,所以我會在本機開Visual Studio把程式寫好,再複製貼到遠端桌面環境。另外,在Code-Behind中我們常用using省去打一堆Namespace,在Inline ASPX裡可用<%@ Import Namespace="…" %>宣告產生相同效果,例如:

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.Common" %>
<ol>
<%
  DataTable dt = DbProviderFactories.GetFactoryClasses();
for (int i = 0; i < dt.Rows.Count; i++)
    Response.Write("<li>" + dt.Rows[i][2] + "</li>");
%>
</ol>

雖然沒什麼學問,卻是十分簡單好用的小技巧,下回再遇到正式環境IIS茶包,大家該知道如何在ASP.NET網站現場撰寫測試程式偵錯了吧?

NLog問題偵錯技巧

$
0
0

NLog是我們開發團隊的奧林匹克指定Log元件,但經驗裡遇過不少次沒有寫Log檔的狀況,而NLog為了避免寫Log過程出錯導致主程序中斷,預設不會拋出錯誤訊息,這讓NLog茶包特別難找。過去較常見問題是對Log資料夾缺少寫入權限(尤其是IIS 7.5+會用IIS APPPOOL\XXXX虛擬帳號,需要額外開權限),補設權限後就OK,對NLog問題如何除錯也未多深究。不料前幾天踢到鐵板,足足卡了一小時找出原因(代表以前是假會,只要不是權限問題就卡關,嗚~),不過因此學會NLog異常排除技巧是意外收獲。

NLog預設會忽略所有發生的錯誤(包含NLog.config壞掉都假裝沒事)以免干擾主程式執行,當需要偵錯時,NLog.config有個開關:<nlog throwExceptions="true" />,開啟後便知NLog為什麼沒成功寫入Log。

來個測試,故意移除Log資料夾寫入權限,並加上throwExceptions="true",錯誤訊息與爆炸程式行數一目瞭然:

除了設定NLog在出錯時直接拋出錯誤,還有另一種有效蒐集錯誤訊息的做法:<nlog internalLogLevel="Debug" internalLogFile="c:\temp\nlog-internal.log">,透過internalLogLevel及internalLogFile開啟NLog的內部Log,從中也可找出NLog無法寫入Log的原因(如下圖)。與throwExceptions="true"有以下不同:

  1. internalLogFile錯誤訊息的StackTracce資訊從NLog元件內部開始(在下例為NLog.Tragets.Target.Write),追不到logger.Debug()等寫入Log的程式來源。
  2. 若internalLogFile路徑遇上無權寫入,一樣暴斃驗無傷。
  3. internalLogFile不只可以記錯誤訊息,當internalLogLevel設成Debug、Trace等模式,還能看到更多NLog運作資訊。

同場加映一則小技巧-如何快速找出Target f的檔案路徑?結合先前介紹過的取得NLog檔案路徑以及在IIS主機現場撰寫測試ASPX偵錯,以下程式顯示LoggerName為"AAA"時的Log路徑,並試寫一個Debug Log,第一行NLog.LogManager.ThrowExceptions = true 可取代 <nlog throwExceptions="true">設定,在NLog出錯時顯示錯誤訊息。透過此一測試可以確認Log檔案位置以及寫入權限。

<%@ Page Language="C#" %>
<%
NLog.LogManager.ThrowExceptions = true; 
Response.Write( 
(NLog.LogManager.Configuration.FindTargetByName("f") as NLog.Targets.FileTarget).FileName 
.Render(new NLog.LogEventInfo() { TimeStamp = DateTime.Now, LoggerName = "AAA" }) 
); 
NLog.LogManager.GetLogger("AAA").Debug("TEST"); 
NLog.LogManager.Flush(); 
%>

還有一點,NLog.config在修改後,需要重啟Web Application才會生效,有個<nlog autoReload="true">可在NLog.config修改時自動載入,可在不中斷ASP.NET或Windows程序調整NLog設定。

【結論】

  1. NLog預設會壓抑所有NLog錯誤以免中斷主要程式邏輯,設定<nlog throwExceptions="true">可顯示NLog錯誤。
    提醒:問題排除後建議改回false,或用在測試程式中以NLog.LogManager.ThrowExceptions=true替代。
  2. 遇到NLog問題時,可寫一個迷你NLogTester.aspx檢查路徑及寫入權限。(範例請見文章)
  3. <nlog autoReload="true">可設定NLog.config修改後自動更新設定,省去重啟程序的困擾。

【延伸閱讀】

  1. Configuration file · NLog-NLog Wiki · GitHub
  2. Logging Troubleshooting · NLog-NLog Wiki · GitHub

【後記】

我這次遇到的狀況是程式換版,將已平測一段時間的新版資料夾取名取代舊版(包含NLog.config),後來想到要沿用原本NLog.config指定的Log路徑,再把舊版的NLog.config覆寫回去,從此再也沒看到有Log寫入。該Log路徑在換版前原本就運作良好,不該是權限問題,讓過去只會處理NLog DLL版本衝突跟Log目錄權限茶包的我黔驢技窮,瞎試半天無解,爬文學會開啟throwExceptions才水落石出:

Exception Details: System.IO.FileNotFoundException: Could not load file or assembly 'NLog.Targets.Syslog' or one of its dependencies. The system cannot find the file specified.

舊版的NLog.config設定了Syslog Target,如下例:

<?xmlversion="1.0"encoding="utf-8" ?>
<nlogxmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<extensions>
<addassembly="NLog.Targets.Syslog"/>
</extensions>
<targets>
<targetxsi:type="File"name="f"fileName="D:/Log/${logger}/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}"encoding="utf-8"/>
<targetname="syslog"type="Syslog"syslogserver="syslog-server-ip"
port="514"facility="Local7"
sender="BLAH.UAT"layout="${longdate} ${uppercase:${level}} ${message}"/>
</targets>
<rules>
<loggername="*"minlevel="Trace"writeTo="f,syslog"/>
</rules>
</nlog>

extensions引用了NLog.Targets.Syslog,舊版bin下有NLog.Targets.Syslog.dll,忘了複製到新版bin目錄導致錯誤,補上後問題排除。

首遇TFS自動合併出錯案例

$
0
0

換用TFS版控時我們開始採用「多重簽出」原則,大幅改善VSS時代「專案一被人簽出其他人就動不了」的困擾。但隨之而來的副作用是:多人同時修改,若簽入時別人已先簽入更新的版本,就需要執行程式碼合併。

在我們的經驗裡,TFS有個神奇又方便的「自動合併」功能,只要程式修改幅度不大,沒有改到同一段程式,TFS幾乎都能正確自動合併,不需人為介入,少數難以判別的情況才會跳出提示要求人工處理。

但時間久了,我不免懷疑,程式碼合併的情境百百種,肉眼合併都難保沒有疏漏,演算法要怎麼寫才能不出錯?但這一兩年下來,記憶中都還真沒出過亂子,TFS自動合併演算法直逼AlphaGo呀,令人讚嘆不已… 直到我膝蓋中了一箭!

前幾天在合併分支時出現一起TypeScript屬性重複案例,追查發現是自動合併造成的。有一段CodeGen產生的TypeScript Model定義,因故調換屬性順序,自動合併時調換位置的屬性出現兩次釀禍。

用實例說明比較好理解,注意下例TypeScript型別的SomePrz及SomeQty屬性,原本排在ColB之後,ColC之前:

export class Blah extends ViewModelBase {
/** 欄位A */
    ColA: KnockoutObservable<any> = ko.observable();
/** 欄位B */
    ColB: KnockoutObservable<any> = ko.observable();
//...省略...
/** 價格 */
    SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
    SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位C */
    ColC: KnockoutObservable<any> = ko.observable();
/** 欄位D */
    ColD: KnockoutObservable<any> = ko.observable();
/** 欄位E */
    ColE: KnockoutObservable<any> = ko.observable();
/** 欄位F */
    ColF: KnockoutObservable<any> = ko.observable();
/** 欄位G */
    ColG: KnockoutObservable<any> = ko.observable();
/** 欄位H */
    ColH: KnockoutObservable<any> = ko.observable();
//...省略...
}

Branch版本裡類別增加了幾個額外屬性,並調動了SomePrz及SomeQty順序,由ColC前方移至ColE後方:

export class Blah extends ViewModelBase {
/** 欄位A */
    ColA: KnockoutObservable<any> = ko.observable();
/** 欄位B */
    ColB: KnockoutObservable<any> = ko.observable();
//...省略...
/** 欄位C */
    ColC: KnockoutObservable<any> = ko.observable();
/** 欄位D */
    ColD: KnockoutObservable<any> = ko.observable();
/** 欄位E */
    ColE: KnockoutObservable<any> = ko.observable();
/** 價格 */
    SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
    SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位F */
    ColF: KnockoutObservable<any> = ko.observable();
/** 欄位G */
    ColG: KnockoutObservable<any> = ko.observable();
/** 欄位H */
    ColH: KnockoutObservable<any> = ko.observable();
//...省略...
}

Merge回主線時,由TFS自動合併解決衝突,變成以下的樣子,SomePrz/SomeQty在ColC欄位前方及ColE後方各出現一次。

export class Blah extends ViewModelBase {
/** 欄位A */
    ColA: KnockoutObservable<any> = ko.observable();
/** 欄位B */
    ColB: KnockoutObservable<any> = ko.observable();
//...省略...
/** 價格 */
    SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
    SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位C */
    ColC: KnockoutObservable<any> = ko.observable();
/** 欄位D */
    ColD: KnockoutObservable<any> = ko.observable();
/** 欄位E */
    ColE: KnockoutObservable<any> = ko.observable();
/** 價格 */
    SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
    SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位F */
    ColF: KnockoutObservable<any> = ko.observable();
/** 欄位G */
    ColG: KnockoutObservable<any> = ko.observable();
/** 欄位H */
    ColH: KnockoutObservable<any> = ko.observable();
//...省略...
}

所幸,TypeScript為強型別,不允許屬性重複宣告,我們很快查出錯誤,合併Branch時暫時停用自動合併功能即可避開問題。

出問題的TypeScript檔案不小,有近2500行,而重複屬性的位置在1668行左右。事後我試著模擬屬性順序調動的情境,卻怎麼都無法重現自動合併錯誤,猜想需要夠大的檔案或某些特殊條件下才會導致自動合併誤判。

爬文沒有找到太多TFS自動合併錯誤的個案,有一個接近的案例是自動合併.csproj發生項目重複,原因也跟項目順序重排有關,證明調換順序是差異比對演算法的大魔王無誤!

這是我第一次遇到TFS自動合併失誤,說不上震憾,充其量只是證實「不存在完美的差異比對演算法」的假設,不致影響我對它的信心。就當成一記失投,它依然是我心目中的賽揚獎投手~ 經此經驗,未來使用自動合併時會提高警覺,預做它可能出錯的心理準備。

當然,如果你寧可辛苦一點也不想承擔任何出錯風險,可考慮將它關閉(如下圖),大家請自行拿捏。

【茶包射手日記】MSBuild.ILMerge.Task發生型別重複錯誤

$
0
0

讀者Peter回饋一起MSBuild.ILMerge.Task合併錯誤案例:專案引用Manatee.Trello.WebApi套件,其依賴Microsoft.AspNet.WebApi.Client.5.2.3(System.Net.Http.Formatting.dll)及Microsoft.AspNet.WebApi.Core.5.2.3(System.Web.Http.dll),合併時出現錯誤:ILMerge.Merge: ERROR!!: Duplicate type 'System.Net.Http.HttpRequestMessageExtensions' found in assembly 'System.Web.Http'. Do you want to use the /alllowDup option?

一般來說,Namespace加上型別名稱應該是唯一的,撞名實屬罕見,引發我的好奇想一探究竟。試開一個新專案引用Manatee.Trello.WebApi與MSBuild.ILMerge.Task,合併組件時也拋出相同錯誤訊息。能重現錯誤一切好辦,著手展開調查。

手動以ILMerge.exe合併組件也出現相同錯誤,依訊息補上/allowDup參數便可避免錯誤。

依此研判,問題出在要合併的組件內含完全相同的類別名稱!使用Telerik JustDecompile分析,果真在System.Net.Http.Formatting.dll與System.Web.Http.dll發現兩個同名型別-HttpRequestMessageExtensions與MediaTypeFormatterExtensions:

 

原來這兩個型別是宣告擴充方法(Extension Method)的靜態型別,應用時完全不會用到型別名稱,故在不同DLL重複出現也無妨;一旦具有同名型別的組件要合併,名稱重複問題才會浮上檯面。

由此可知,執行ILMerge.exe時加上allowDup參數即可解決問題,但一直沒找到文件示範如何為MSBuild.ILMerge.Task加上allowDup參數。歷經一番研究,我發現合併作業的核心邏輯寫在MSBuild.ILMerge.Task.dll,反組譯發現其中有實作AllowDuplicateType參數,但似乎只能透過targets設定檔指定。

在packages\MSBuild.ILMerge.Task.1.0.2\build\MSBuild.ILMerge.Task.targets找到<MSBuild.ILMerge.Task>,加上AllowDuplicateType = "*"(或是明確列出已知的重複名稱型別,如AllowDuplicateType = "HttpRequestMessageExtensions,MediaTypeFormatterExtensions"),等同為ILMerge.exe加上/allowDup參數。(註:此種參數修改做法影響範圍將涵蓋整個解決方案)

修改後重新編譯,組件成功合併,問題排除,收工~

【茶包射手日記】無法使用別名登入本機IIS

$
0
0

前陣子研究出克服入口網站內嵌其他網站跨網站存取限制的方法,實際會用於整合兩台以上網站,但在開發測試期間也要搞兩台機器太麻煩,於是我用了點技巧,在windows/system32/drivers/etc/hosts加入額外設定:

127.0.0.1    portal.dev.net
127.0.0.1    webap.dev.net
127.0.0.1    mybox

如此開發機的IIS就可一人分飾多角,用 httq://portal.dev.net/ 及 httq://webap.dev.net/ 都能連上,還會被瀏覽器視為不同網域機器,模擬正式環境的情境。經初步測試成功就以為天衣無縫,沒想到今天踢到鐵板。

初期開啟匿名存取,測試一切順利,到第二階段改用Windows驗證,卻發現使用localhost、127.0.0.1、機器名稱登入正常,一旦改用 portal.dev.net 或 webap.dev.net,甚至 mybox,網頁會不斷彈出帳號密碼視窗,帳號密碼正確也無法登入。進行對照實驗,若改從另一台機器測試(在hosts加入同樣項目指向我的LAN IP)可正常登入,歸納出「使用localhost、127.0.0.1、機器名稱或FQDN以外的URL登入本機IIS,將無法通過Windows驗證」的結論。

頓時心頭涼了半截,難道為此真得搞另一台機器進行測試?

爬文找到線索:MS KB有篇討論用FQDN或自訂Web名稱無法登入IIS的文章提及類似場景。文中提到Windows 2003 SP1起啟用一種名為Loopback Check的安全機制,藉以防止本機惡意程式模擬外來連線進行攻擊。KB提到兩種解決方法:修改Registry列舉會使用的別名或停用LoopBackCheck。前者採白名單開放較嚴謹,後者一次搞定較乾脆,衡量停用檢查增加風險有限,決定使用後者:

加入Registry後IISREST,終於能用portal.dev.net登入本機,再次驚險過關。

Viewing all 428 articles
Browse latest View live