Quantcast
Channel: 黑暗執行緒
Viewing all 2311 articles
Browse latest View live

讓Windows輾轉難眠的臉書

$
0
0

Windows,你為什麼不睡覺?一文中,我學到播放影片軟體會佔用音效裝置,導致Windows閒置自動睡眠設定失效,而powercfg /requests指令則可用來快速查詢資源是否處於使用中。此後,每次讓電腦睡眠前,我會特別關掉播放中的影片,避免半夜Windows自動更新或其他排程甦醒後不再自動睡眠,平白空轉一整天。

前幾天接連兩次,電腦半夜醒來後一直醒著,用powercfg /requests一查,果然音效狀置在使用中,問題是我只有瀏覽器跟Visual Studio沒關,瀏覽器沒上YouTube看影片,那會是誰卡著音效裝置不放?

經過一番調查,居然只因Chrome開著臉書!我錄了一段操作重現問題:

如以上影片所示,當開啟黑暗執行緒臉書專頁,使用powercfg指令檢查還正常;一旦開啟Microsoft專頁,powercfg立即顯示USB Audio Device在使用中。可以預期,當此現象發生,Windows就不會啟動閒置後自動睡眠,凶手現形。

結論是:當瀏覽器開啟特定臉書網頁會佔用Audio Device。經過簡單對照,我推測關鍵在臉書的影片自動播放,特别挑Microsoft專頁示範,就因為它的前幾則貼文剛好有內嵌影片。

知道這點,下回叫Windows上床前,就知道該把瀏覽器的臉書網頁都關一關,省得電腦半夜爬起來偷看影片不肯回去睡。


【茶包射手日記】解決舊作業系統檔案無法刪除問題

$
0
0

問題情境如下:整理原屬舊作業系統碟的硬碟,在刪除檔案時常遇到系統檔被設定只有TrustedInstaller等系統帳號才能刪除的狀況,此時需修改NTFS進階安全設定克服。

如下圖所示,檔案被設成TrustedInstaller才能完全控制權限,連Administrators都無法修改權限。唯一解法是先將檔案擁有者設成自己再加入修改及刪除權限。

手工調整成千上萬個檔案讓人發狂,上網爬文找到批次指令

警告:本案例討論對象為舊作業系統檔案,請勿用於更動現行作業系統檔案權限,以免造成系統損壞。

先用takeown工具取得檔案擁有權:

takeown /A /R /F "Windows"

再用icacls工具將Windows下所有子目錄改成Jeffrey可以完全存取:

icacls "Windows" /grant:r jeffrey:F /t

搞定,收工!

Visual Studio編譯小技巧:工具程式一檔搞定

$
0
0

我經常寫小工具程式,不用安裝程式,單一EXE檔隨Copy隨用是最理想的部署設計。不過,程式稍稍複雜就難免依功能屬性拆分多個專案,有時需用到跨專案的共享程式庫,至於引用Json.NET、Dapper、NLog等必備套件的情況更是普遍。例如以下專案,Tool為Console Application(EXE)專案,引用Model類別程式庫專案,還參考了Json.NET:

編譯後會產生三組組件檔:Tool.exe、Model.dll與Newtonsoft.Json.dll,得一起部署到客戶端才能正確執行。

要實現單一EXE檔搞定,.NET有個好用工具-ILMerge,可將多個DLL、EXE檔合併成單一檔案(ILMerge使用方式可參考保哥的介紹文),原本想花點時間研究怎麼安排AfterPost事件執行ILMerge,驚喜地發現已有好心人包成MSBuild的Task!

在NuGet使用msbuild.ilmerge查詢,可以找到MSBuild.ILMerge.Task,二話不說,安裝到專案。

安裝後專案會多出ILMerge.props、ILMergeOrder.txt,但大部分情況下不需修改,直接編譯就好。

重新編譯可發現Model.dll及Newtonsoft.Json.dll不見了,只剩一個變胖的Tool.exe,使用時只需Copy這個檔案就行了。

用JustDecompile解析,可以看到Tool.exe裡藏了Model跟Newtonsoft.Json組件裡的所有型別。

用這種做法即可輕鬆一檔搞定小工具程式的部署,非常方便。

補充:MSBuild.ILMerge.Task預設會將參照到的DLL都包進EXE檔,如果想略過特定DLL,可將DLL的Copy Local屬性設為False即可排除。

2016鳳梨馬(八卦山台地馬拉松)

$
0
0

清明時節,返鄉掃墓兼跑馬的時刻又到了,連續第三年的鳳梨馬。

今年安排較充實的行程,前一天去了台灣地理中心碑、埔里酒廠跟紙教堂。

行前功課沒做足,對於哪個才是「台灣地理中心碑」有點迷惘,先在山下看到一個感覺不夠威,加上Fenix 3高度計顯示的海拔與資料上的555公尺差約100公尺,爬階攻上山頂找到另一座呈現魔法陣概念的石柱群,海拔吻合顯然才是真正的地理中心。

      
      

回頭在入口處看到介紹牌,原來地理中心碑的確指的是山腳刻有山清水秀,白色弧牆拱抱立有長桿的褐色石碑,為日據時間所立。光復後經重新測量發現中心碑位置有誤,真正的地理幾何中心在虎頭山頂。簡單地說,地理中心碑不在地理中心,真的地理中心沒有碑,報告完畢。而這個故事也告訴我們「RTFM」的重要性。

埔里酒廠比想像大,展售中心有各式各樣的酒類衍生商品,從冰棒、巧克力、蜜餞到生技化妝品都有,傳說中買不到的花雕雞泡麵不意外地高掛缺貨牌。

     

展售中心樓上有酒廠沿革及製酒介紹,意外學到二鍋頭的由來:指傳統製酒蒸餾換上第二鍋冷凝水時得到的成品。下圖是女兒紅展示區,兩個站在酒罈後的金髮西服模特兒像路人亂入,噗!

數大便是美的酒罈牆,拍起照來很有fu~

傍晚去了紙教堂,天色漸暗原以為看不到什麼好風景。

沒想到紙教堂愈夜愈美麗,點燈後更美! 

第二天照例一早到南投家樂福等接駁車,發現連我只有五位跑友要搭車,還發生人車各在兩頭枯等的小插曲,在下一站又接了兩名跑友,七人獨享一台大巴士前往會場。

鳳嗚國中校舍改建中,司令台跟教室大樓不見了。會場安排與動線依舊維持一貫的專業水準,在我心中是名列前茅的優質賽事。

大會開放加購的紀念品,鳳梨娃娃。

連下了一週的雨,週六才開始放晴,氣溫降到11度左右,老天爺賜了一個乾冷好天氣,那就認真一點好了。:P

賽前注意到會場有三位黑人選手,猜想就是傳說中的肯亞軍團,長期駐台四處參賽討生活,這也是台灣馬拉松賽事密集產生的新行業,據說年薪可破百萬,但這行飯不是人人吃得起就是了。三人小組兩位跑全馬一位跑半馬,里程已過大半後方仍有台灣選手緊咬的態勢,估計油門只踩八分,畢竟在他們的規劃裡這是日常上班不比參加歌唱選秀,使出渾身解數搞到氣力放盡明天還要不要上班?XD

    
    

139縣道的好風景依舊,可惜小葉欖仁新葉未萌,不然就更美了~

好久不見滾鐵圈的大哥,速度驚人,拍照時我大概已落後2K以上!倒是折返後在對向遇到馬拉松狗黃檸檬,落後我約1K,心中燃起小宇宙,難得有機會,這回我不要再跑輸狗了。

    

鳳梨馬的交管很到位,每個路口都有交警、志工管制車輛,經過時心中對受影響的車輛偷偷說聲抱歉。

最近氣候反常,去年開得無法無天的九重葛今年盛況不再。

遇到在路旁寫生的畫家,裝扮很有藝術家的fu。

台地看出去的好風景,可惜空氣品質不佳,展望不好。

鳯梨服裝展示,今年流行的主軸偏向多樣化,我們可以看到拉丁美洲風大圓帽,以及中世紀歐洲風格鵝黃網紗,以及展現熱帶風情的葉片素材包法… (純唬爛,勿砲)

   

鳳梨馬的特色:鳳梨跟跑者相映成趣~(還有喝不完的土鳳梨汁)

「吃很好」也是鳳梨馬的傳統,香蕉、檸檬、小蕃茄、巧克力、小饅頭、鹹蛋、火腿、麵條、貢丸湯、炸(假)干貝、薯條、蠻牛… 多到記不住,把地瓜球攤子推來現炸這招很殺 XD 而可以喝到飽的土鳳梨汁是我的最愛。

拜氣溫低所賜,前30K跑了3h20m,成績尚可,偷偷訂下SUB 5目標(還有,不要被黃檸檬刷卡),近10點太陽開始發威,掛上墨鏡戴起MP3聽快節奏舞曲,開啟小宇宙燃燒模式… (其實只是少走一點,多跑一些,均速還是很難看)

34K經過終點拱門,過家門而不入體驗+1,最後8K倒數。順道一提,賽道上每公里的里程標示跟我的GPS錶測得距離誤差均小於200公尺,非常精準。

除了鳳梨田,還有壯觀的茶園。

天空之橋到了,倒數6K。

最後卯足全勁,保4成功,4:56:20完賽,考量賽道有超過500公尺爬升以及老爺車年年增長的車齡,成績不算差,多虧天氣幫忙。終點有Show Girl為選手掛獎牌,賽前沒宣佈害我一點心理準備都沒有,不過這種驚喜多多益善。XD

一條龍服務很讚,瞬間領完成績單、伴手禮、海鮮鹹粥(現場搭辦桌棚子現煮,好吃!),領物後還有更衣室可以沖澡更衣,很貼心。

離開前在教室走廊巧遇黃檸檬,正吸引三個小女生圍著牠(狗帥真好),這才想起:對耶,我終於跑嬴黃檸檬了,YA!

跟狗主黃大哥小聊幾句,大哥感慨八歲的黃檸檬今年體力明顯衰退。屈指一算,黃檸檬差不多是男甲組,我居然跟六十歲的阿公級跑者比成績,好丟臉。被小女生摸頭時,黃檸檬幾度低頭閤眼顯露疲態,再無當年40K就遇到牠逆跑排乳酸的煥發,心中浮起歲月不饒人(狗)的感慨。無論如何,恭喜雷夢80馬完賽,並祝早破百馬。

回程順道去看了八卦山大佛,這才知道跑了三回的139縣道,一路跑下去就能跑到大佛。童年來過,如今再見,第一個感覺是訝異與記憶中的高大相差甚多,仔細想想原因有二,一是是兒時身高感受的空間感不同,二是隨時代演進高樓林立,對於「高」的定義也變了。

小攤在賣鵝蛋跟鴕鳥蛋,很新奇。我們買了鵝蛋嚐鮮,蛋殼差不多有信用卡的厚度,蛋白相當彈牙,很新鮮的口感。

大佛前有個環狀步道,彰化市區一覽無遺。

發現微笑單車已經部署到彰化了,不知有沒有人挑戰從台北騎下來。

補上完賽獎牌照片。

    

很巧合地,賽前疑似被小木頭傳染感冒,有輕微症狀,跑完不意外地變嚴重,咳了一週才好,跟去年情節幾乎完全相同,為「跑全馬影響扺抗力人體實驗」再添一例 orz。

改良式GetCachableData可快取查詢函式

$
0
0

多年前發展過一種可快取查詢:呼叫GetCachableData函式時傳入Cache Key、查詢或產生資料Callback函式、Cache保留期限(或指定閒置未用多久自動清除)三個參數,GetCachableData會依「若Cache有資料就直接沿用;若Cache無資料則當場產生並存入Cache」原則聰明處理,從此不需操心何時該查資料何時用Cache,應用起來挺方便的。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace GetCachable
{
publicstaticclass CacheManager
    {
/// <summary>
/// 取得可以被Cache的資料(注意:非Thread-Safe)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">Cache保存號碼牌</param>
/// <param name="callback">傳回查詢資料的函數</param>
/// <param name="cacheMins"></param>
/// <param name="forceRefresh">是否清除Cache,重新查詢</param>
/// <returns></returns>
publicstatic T GetCachableData<T>(string key, Func<T> callback, 
int cacheMins, bool forceRefresh = false) where T : class
        {
            ObjectCache cache = MemoryCache.Default;
string cacheKey = key;
 
            T res = cache[cacheKey] as T;
//是否清除Cache,強制重查
if (res != null&& forceRefresh)
            {
                cache.Remove(cacheKey);
                res = null;
            }
if (res == null)
            {
                res = callback();
                cache.Add(cacheKey, res, 
new CacheItemPolicy() {
                        SlidingExpiration = new TimeSpan(0, cacheMins, 0)
                    });
            }
return res;
        }
 
 
/// <summary>
/// 取得可以被Cache的資料(注意:非Thread-Safe)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">Cache保存號碼牌</param>
/// <param name="callback">傳回查詢資料的函數</param>
/// <param name="absExpire">有效期限</param>
/// <param name="forceRefresh">是否清除Cache,重新查詢</param>
/// <returns></returns>
publicstatic T GetCachableData<T>(string key, Func<T> callback, 
            DateTimeOffset absExpire, bool forceRefresh = false) where T : class
        {
            ObjectCache cache = MemoryCache.Default;
string cacheKey = key;
//取得每個Key專屬的鎖定對象
            T res = cache[cacheKey] as T;
//是否清除Cache,強制重查
if (res != null&& forceRefresh)
            {
                cache.Remove(cacheKey);
                res = null;
            }
if (res == null)
            {
                res = callback();
                cache.Add(cacheKey, res, new CacheItemPolicy()
                {
                    AbsoluteExpiration = absExpire
                });
            }
return res;
        }
    }
}

不過原本的設計有個小問題,例如:有個網站透過GetCachableData由資料庫讀取五千筆員工資料並Cache住一小時,以便後續能快速地用員編查姓名。想像一個場景,尖峰時刻Cache逾時被清除(或是網站因故重啟),線上一百名使用者同時瀏覽某一網頁使用到員工姓名查詢,於是GetCachableData同時被100條Thread呼叫,MemoryCache本身為Thread-Safe多執行緒讀寫不致出錯,但Cache不存在觸發100個資料庫查詢,對形成一波完美的DDoS攻擊!接著資料庫忙碌、網頁卡住、使用者無助、老闆暴怒、開發者想哭…

以下範例可展示此問題,同時開啟三條Thread呼叫GetCachableData,則Callback動作也會同時三份(Callback執行時會印出Thread n Start/Stop Job訊息以利觀察)。這三次查詢動作只有一次是必要的,其餘兩次將取得相同結果覆寫同一Cache,平白消耗資源,在極端案例中甚至可能讓系統崩潰。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace GetCachable
{
class Program
    {
staticvoid Main(string[] args)
        {
            var tasks = new List<Task>();
for (var i = 0; i < 3; i++)
            {
                tasks.Add(Task.Factory.StartNew(() =>
                {
                    var data = CacheManager.GetCachableData<string>("KEY",
                        () =>
                        {
                            Console.WriteLine("Thread {0} Start Job",
                                Thread.CurrentThread.ManagedThreadId);
                            Thread.Sleep(3000);
                            Console.WriteLine("Thread {0} Stop Job",
                                Thread.CurrentThread.ManagedThreadId);
return"OK";
                        }, 10);
                    Console.WriteLine("Data:" + data);
                }));
            }
            tasks.ForEach(t => t.Wait());
 
            Console.WriteLine("Done");
            Console.ReadLine();
        }
    }
}

執行結果如下,可觀察到三條Thread同時執行Callback:

Thread 13 Start Job
Thread 11 Start Job
Thread 12 Start Job
Thread 13 Stop Job
Data:OK
Thread 11 Stop Job
Thread 12 Stop Job
Data:OK
Data:OK
Done

要改良此一缺點,可在多執行緒查詢時加入Lock機制,相同Key值的查詢單一時間只允許一組Callback執行,執行完成後其餘等待的Thread可直接取用Cache結果,省下無效益的Callback動作。程式範例如下,依Key值建立Object作為鎖定對象,即能實現一Key值不會有兩份以上Callback同時執行:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Text;
using System.Threading.Tasks;
 
namespace GetCachable
{
publicstaticclass BetterCacheManager
    {
//加入Lock機制限定同一Key同一時間只有一個Callback執行
conststring AsyncLockPrefix = "$$CacheAsyncLock#";
/// <summary>
/// 取得每個Key專屬的鎖定對象
/// </summary>
/// <param name="key">Cache保存號碼牌</param>
/// <returns></returns>
staticobject GetAsyncLock(string key)
        {
            ObjectCache cache = MemoryCache.Default;
//取得每個Key專屬的鎖定對象(object)
string asyncLockKey = AsyncLockPrefix + key;
lock (cache)
            {
if (cache[asyncLockKey] == null) cache.Add(asyncLockKey,
newobject(),
new CacheItemPolicy() {
                        SlidingExpiration = new TimeSpan(0, 10, 0)
                    });
            }
return cache[asyncLockKey];
        }
 
/// <summary>
/// 取得可以被Cache的資料(注意:非Thread-Safe)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">Cache保存號碼牌</param>
/// <param name="callback">傳回查詢資料的函數</param>
/// <param name="cacheMins"></param>
/// <param name="forceRefresh">是否清除Cache,重新查詢</param>
/// <returns></returns>
publicstatic T GetCachableData<T>(string key, Func<T> callback, 
int cacheMins, bool forceRefresh = false) where T : class
        {
            ObjectCache cache = MemoryCache.Default;
string cacheKey = key;
 
//取得每個Key專屬的鎖定對象
lock (GetAsyncLock(key))
            {
                T res = cache[cacheKey] as T;
//是否清除Cache,強制重查
if (res != null&& forceRefresh)
                {
                    cache.Remove(cacheKey);
                    res = null;
                }
if (res == null)
                {
                    res = callback();
                    cache.Add(cacheKey, res, 
new CacheItemPolicy() {
                            SlidingExpiration = new TimeSpan(0, cacheMins, 0)
                        });
                }
return res;
            }
        }
 
 
/// <summary>
/// 取得可以被Cache的資料(注意:非Thread-Safe)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">Cache保存號碼牌</param>
/// <param name="callback">傳回查詢資料的函數</param>
/// <param name="absExpire">有效期限</param>
/// <param name="forceRefresh">是否清除Cache,重新查詢</param>
/// <returns></returns>
publicstatic T GetCachableData<T>(string key, Func<T> callback, 
            DateTimeOffset absExpire, bool forceRefresh = false) where T : class
        {
            ObjectCache cache = MemoryCache.Default;
string cacheKey = key;
//取得每個Key專屬的鎖定對象
lock (GetAsyncLock(key))
            {
                T res = cache[cacheKey] as T;
//是否清除Cache,強制重查
if (res != null&& forceRefresh)
                {
                    cache.Remove(cacheKey);
                    res = null;
                }
if (res == null)
                {
                    res = callback();
                    cache.Add(cacheKey, res, new CacheItemPolicy()
                    {
                        AbsoluteExpiration = absExpire
                    });
                }
return res;
            }
        }
    }
}

改用BetterCacheManager後,同時三條Thread呼叫GetCachableData()只會觸發一次Callback,可減少高承載系統產生重複查詢的壓力:

Thread 9 Start Job
Thread 9 Stop Job
Data:OK
Data:OK
Data:OK
Done

以上私房做法,提供大家參考。

關於IIS整合式Windows驗證的冷知識

$
0
0

在企業內部寫ASP.NET的人都用過整合式Windows驗證吧?來個小測驗:

  1. 沒加入網域的PC可以用AD帳號登入隸屬該網域的IIS嗎?
  2. 在測試環境建立一測試網域,與公司正式AD網域間無信任關係。正式網域PC是否可以用測試網域帳號登入測試網域的IIS?
  3. 承上,正式網域PC需將DNS指向測試網域DC才能用測試網域帳號登入嗎?

依據實際使用經驗,我知道的答案是:Yes、Yes、No,但不知所以然。最近因工作需要做了粗淺研究,整理筆記備忘兼分享。(如有謬誤,有請各方高人指正)

IIS實現Windows驗證的方式有兩種,NTLM與Kerberos。NTLM如其名,是NT時代就有的Challenge/Response驗證方式(不透過網路傳輸密碼本身);Windows 2000起改用Kerberos作為預設認證協定,主要有以下優點:

  • 驗證速度快
    使用NTLM時,若資源伺服器本身不是DC(Domain Controller,網域控制器),則轉向DC求證身分真偽(稱之Pass-Through Authentication);而資源伺服器取得Kerberros Tickets(或稱Authenticator)時就已取得足以驗證客戶端的資訊,較有效率。
  • 交互驗證
    Kerberos驗證時,不只客戶端向伺服器證明自己身分,伺服器也可要求伺服器驗明正身,社絕NTLM可能被冒牌伺服器騙取身分的風險。
  • Kerberos為開放標準
    由於Kerberos為開放標準,故有機會整合Windows與其他作業系統達成單一登入(Single Sign On)。
  • 支援驗證委派
    資源伺服器取得代表使用者身分的Kerberos Ticket後,可直接代表該使用者存取其他資源伺服器。例如:使用者登入Web後,Web以該使用者身分登入SQL。 
  • 支援Smart Card登入
    可整合Smart Card做到雙因子登入,登入時需擁有實體卡片並知道密碼,降低身分盜用風險。

當我們設定IIS使用Windows驗證時,預設的提供者為Negotiate,包含Kerberos及NTLM兩種驗證方式,而其選用規則為「與瀏覽器協商,先嘗試使用Kerberos,若條件不符則改用NTLM」。

IIS採用NTLM或Kerberos則有以下區別:

NTLM

  • 客戶端不管有沒有加入網域都適用
  • 可使用網域帳號或本機帳號
  • 使用網域帳號時,只有IIS主機需要連線DC
    註:忙碌的NTLM Server (IIS, Exchange, TMG/ISA)可能產生大量NTLM請求,對DC形成負擔
  • 客戶端只需連上IIS,不用連到DC
  • 可以穿透支援HTTP Keep-Alives的Proxy
  • 認證過程需多次往返(HTTP 401 –> HTTP 401 –> HTTP 200)
  • 不支援Double-Hop Authenticataion(即驗證委派,將使用者認證用於另一台電腦執行的服務)
  • 支援Windows 2000以下的舊版系統(註:此點已可忽略)
  • 易受LM Auth Level不一致影響(lmcompatibilitylevel不吻合)
  • 為Negotiate提供者的備援方案,若 Kerberos不成功就轉用NTLM

Kerberos

  • 只適用已加入網域的客戶端
  • 客戶端必須自己連上AD DC(TCP/UDP 88 Port)
  • 不易穿透Proxy
  • 憑證效期長(10小時),DC負荷較輕
  • 認證過程為HTTP 401 –> 傳送Token(6-16KB) –> HTTP 200
  • 認證身分可以轉用。例如:使用者登入IIS後,Web使用同一AD帳號登入SQL
  • Negotiate提供者的優先選項
  • 必須搞定SPN註冊(手續挺複雜)
  • 非常容易失敗,如果你…
    • 在URL使用IP而不是機器名稱
    • 沒註冊SPN
    • 重複註冊SPN
    • SPN註冊錯帳號 (KRB_ERR_AP_MODIFIED)
    • 客戶端無法連線DNS/DC
    • Proxy/Local Intranet Zone沒設好

理解以上知識,一開始的三個問題就有了答案,而我才這發現平日使用的Windows驗證,幾乎都是走NTLM,理由包含:使用測試AD帳號、使用IP URL、未特別為網站註冊SPN、啟用Web Farm或負載平衡設備無法用機器名稱… 換言之,Kerberos雖然更安全,但成立條件嚴苛,稍有不慎就會掉回NTLM。

活到老學到老,今天又上了一課。

參考資料:

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(".")
        location.hostname.split('.').slice(1).join(".") (感謝Ammon大開示,location.hostname可直接取主機名稱)
  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,雖然不難,但仍需要花點時間學習。

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

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

Viewing all 2311 articles
Browse latest View live