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

ActionFilter Attribute 共用特性與狀態保存

$
0
0

同事報案,某個 Web API 會不定期出錯。進一步調查是近期啟動的一個新排程同步發出多個 API 呼叫,當 Web API 同時被多方呼叫,Web API 加掛用來寫 Log 的 ActionFilter Attribute 偶爾會發生 Dictionary.Add 重複加入相同鍵值的錯誤。因 Dictionary 被設成 ActionFilter Attribute Instance 的私有欄位,依我原先的理解,ActionFilter Attribute 每次呼叫時都應建立新 Instance,不致因共用打架,但觀察結果顯然與假設不符。進一步檢查 Log 軌跡,確實找到兩次 Request 共用 ActionFilterAttribute 覆寫變數內容的證據!由此,為什麼只在 Instance 執行一次 Dictionary.Add 卻發生鍵值重複便有了合理解釋。

爬文才發現,自 ASP.NET MVC3 起,Action Filter 更積極藉由 Cache 機制重複使用,不再每次 Request 重新建立:參考

In previous versions of ASP.NET MVC, action filters are create per request except in a few cases. This behavior was never a guaranteed behavior but merely an implementation detail and the contract for filters was to consider them stateless. In ASP.NET MVC 3, filters are cached more aggressively. Therefore, any custom action filters which improperly store instance state might be broken.

用以下實例重現問題。用 ActionFilterAttribute 做一個超簡單的執行耗時顯示,在 OnActionExecuting() 以物件欄位記錄 p 參數及開始時間、OnResultExecuted() 計算並顯示耗時及 p 參數:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MVCLab.Models
{
publicclass ExecTimeAttribute : ActionFilterAttribute
    {
string id;
        DateTime start;
publicoverridevoid OnActionExecuting(ActionExecutingContext filterContext)
        {
            id = filterContext.HttpContext.Request["p"] ?? "NULL";
            start = DateTime.Now;
        }
publicoverridevoid OnResultExecuted(ResultExecutedContext filterContext)
        {
            var dura = DateTime.Now - start;
            filterContext.HttpContext.Response.Write($"<span>{id}: {dura.Milliseconds}ms</span>");
        }
    }
}

在 HomeController.cs Index Action 加上 [ExecTime] Attribute:

using MVCLab.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
 
namespace MVCLab.Controllers
{
publicclass HomeController : Controller
    {
        [ExecTime]
public ActionResult Index()
        {
            var rnd = new Random();
            Thread.Sleep(rnd.Next(500));
return View();
        }
    }
}

Index.cshtml 長這様:

@{ Layout = null; }
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Index</title>
</head>
<body>
<div>測試(@Request["p"])</div>
</body>
</html>

寫個 Test1.html 做測試:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
<style>
        iframe { width: 150px; height: 60px; margin: 3px; float: left; }
</style>
</head>
<body>
<inputid="txtUrl"value="/Home/Index?p=001"/>
<buttononclick="test()">Test</button>
<br/>
<iframeid="frmTest"src="about:blank"></iframe>
<script>
function test() {
            document.getElementById("frmTest").src = document.getElementById("txtUrl").value;
        }
</script>
</body>
</html>

測試結果符合預期:

問題發生在多個 Request 並行時。用三個 IFrame 一次發出 3 個 Request,參數分別傳入 001、002、003:

<!DOCTYPEhtml>
<html>
<head>
<title></title>
<metacharset="utf-8"/>
<style>
        iframe { width: 150px; height: 60px; margin: 3px; float: left; }
</style>
</head>
<body>
<iframesrc="/Home/Index?p=001"></iframe>
<iframesrc="/Home/Index?p=002"></iframe>
<iframesrc="/Home/Index?p=003"></iframe>
</body>
</html>

然後它就壞掉了!下方顯示的 p 參數全部被覆寫成 003…

由此可知,使用 Attribute 物件屬性或欄位保存狀態可能因物件重複使用導致資料覆寫,故較好的做法是將狀態改存入專屬每個 Request 的 HttpContext,TempData 是不錯的選擇。修改後程式如下:

using System;
using System.Web.Mvc;
 
namespace MVCLab.Models
{
publicstaticclass MyContextExt
    {
publicstatic T GetVar<T>(this ResultExecutedContext ctx, string varName)
        {
return (T)ctx.HttpContext.Items[varName];
        }
publicstaticvoid StoreVar<T>(this ActionExecutingContext ctx, string varName, T data)
        {
            ctx.HttpContext.Items[varName] = data;
        }
    }
 
publicclass ExecTimeAttribute : ActionFilterAttribute
    {
//REF: http://stackoverflow.com/a/8937793/4335757
publicoverridevoid OnActionExecuting(ActionExecutingContext filterContext)
        {
            filterContext.StoreVar<string>("id", filterContext.HttpContext.Request["p"] ?? "NULL");
            filterContext.StoreVar<DateTime>("start", DateTime.Now);
        }
publicoverridevoid OnResultExecuted(ResultExecutedContext filterContext)
        {
            var dura = DateTime.Now - filterContext.GetVar<DateTime>("start");
            var id = filterContext.GetVar<string>("id");
            filterContext.HttpContext.Response.Write($"<span>{id}: {dura.Milliseconds}ms</span>");
        }
    }
}

改用 TempData 保存狀態後,程式運作正常,問題排除。


好問題:GUID 真的不會重複嗎?

$
0
0

前幾天,「系統產生的 GUID 是否可能發生重複?」在辦公室引起熱議。我主張:GUID 透過網卡 MAC 地址、產生時間以及一些機制(防止同時間點產生兩筆或時鐘往回調)確保世上任何電腦都不會產生相同 GUID,只要所有電腦的 MAC 地址沒有亂來,理論上不可能發生重複。這說法挺有說服力,解除了大家心中的疑慮。

BUT,禁不住好奇爬了文,這才發現「我錯了!」

倒不是不該信任 GUID 永不重複,而是我們現在使用的 GUID 早已不是依據 MAC 及時間產生,而是靠隨機亂數產生。

GUID(Globally Unique Identifier)是微軟依據 UUID(Universally Unique Identifier)規範實作的唯一識別碼。至於 UUID 的演算法主要有四種:(Version 2 極罕用,直接忽略)

Version 1

使用 Timestamp 與網卡 MAC 網址確保唯一性,這就是我原本認知的做法。Version 1 將結構分成:

  1. 60 位元的時間戳記(Timestamp)
  2. 4 位元的演算法識別碼,固定為 0001(Algorithm 1),只要各演算法保證自己不產生重複值,不同演算法間也不會重複
  3. 14 位元的緊急識別位元(Emergency Uniquifier Bits)
    由於 UUID 依賴時間產生唯一性,當同一時間要產生多筆 UUID 時或時鐘被調回過去,藉著會遞增的 Uniquifier 確保時間相同 UUID 也不致重複
  4. 2 位元保留值,固定為 01
  5. 48 位元為產生該 UUID 電腦的識別碼,通常使用網卡的 MAC 地址;若沒有網卡,則第一個位元設為 1其餘 47 位元為亂數。(網卡 MAC 的第一位元永遠為 0,故不會跟無網卡的亂數識別重複)

60 + 4 + 14 + 2 + 48 = 128 位元,完整 UUID 是唯一的,但只擷取其中部分就不保證。

Version 3 & Version 5

依據 Namespace 及 Name 字串雜湊碼(Vesrion 3 用 MD5、Version 5 用 SHA1)產生,固定輸入將產生一致的 UUID。

Version 4

由 4 位元版號及 2位元 UUID Variant 加上 122 位元亂數產生,主要靠 Pseudorandom Number Generator(PRNG,仿隨機數字產生器)產生具有唯一性的亂數(但不保證不可預測性)。

Version 1 就是我小時候學過 GUID 保證不重複的原理依據。不過,因為有洩露 MAC 地址的安全疑慮(當年這個漏洞還被用來追查梅杜莎病毒來源),微軟從 Windows 2000 起便改用 Version 4 ,以隨機亂數產生 GUID:參考

With Windows 2000, Microsoft switched to version 4 GUIDs, since embedding the MAC address was viewed as a security risk.

不信?你可以產生一堆 NewGuid() 試試,看看第三節數字的第一位是否永遠為 4?這個 4 即代表它是 Version 4 的 UUID!

 

【結論】

好,所以現在我們知道了!GUID 已不是靠 MAC 地址及時間確保唯一性,而是靠隨機亂數產生,靠著 122 位元數夠多讓重複機率趨於0。那那那,GUID 有沒有可能重複?當然有可能,但機率有多低呢?依據維基百科,預估的機率是 2.71 * 10^18 分之一,若每秒產生 10 億個 UUID 連續 85 年,將有 50% 的機率至少發生一次重複。如果你很想目睹 GUID 強碰,必須產生 103 百萬兆個 UUID,才會有 10 億分之一的重複機率

這下麻煩了,只是機率極低而非全無可能,那我們可不可以說「GUID 永遠唯一」?

我的看法是「因人而異」!

如果你常擔心走在路上會被殞石爆頭,再不然就是明天有外星艦隊攻打地球,常煩惱要是回家路上撿到彩券中樂透,該不該馬上辭掉工作四海雲遊?那麼你應該不介意再多操煩一件芝麻小事:「靠北!要是 GUID 重複系統崩潰該怎麼辦?」。

不然的話,我建議大家學我一樣,別想太多,大聲說「別擔心,GUID 永遠不重複!」就好。

要是真遇上了,我誠心道歉並恭賀閣下獲得老天爺對人品至高無上的肯定。

【參考資料】

WinDBG 應用實例:找出 ASP.NET CPU 100% 原因

$
0
0

故事是這様的,我們有一批網站搬到新主機出現詭異現象:每隔一段時間某些 IIS AppPool Process 會吃滿 25% CPU 使用量,在 4 核機器這象徵有一條 Thread 陷入無窮迴圈吃光一個 CPU Core 的時間。有時也會出現多個 AppPool 同時發難,每個 Process 吃 25%,把整體 CPU 使用率逼上 50%、75%,甚至 100%。出問題時,該 AppPool 網站仍能使用,但無法透過 IIS 管理回收 AppPool,只能用 TaskManager 砍掉 Process。砍掉 Process 後,系統會乖一陣子,但幾個小時或隔天又復發。

原本正常的程式移到新環境不穩定讓人心慌,很想知道吃光 CPU 的 AppPool 究竟在忙什麼?到底是哪支網頁是老鼠屎?我唯一想到的可用武器是強大但陌生的 WinDbg (Windows Debugger),爬文與一番摸索後居然真讓我成功抓出問題所在,興奮到想轉圈灑花。:P

以下就分享本次使用 WinDbg 找出 CPU 100% 程式來源的經驗:

  1. 使用 TaskManager 產生 Memory Dump 檔
    找出吃光 CPU 的 w3wp.exe Process,叫出右鍵選單執行「Create dump file」 產生記憶體傾印檔。

    有件事要注意,要先釐清 AppPool 是 32 位元還是 64 位元,若為 32 位元需改用 32 位元版 TaskManager,否則 Dump 檔解析出來的資訊會類似 wow64cpu!TurboDispatchJumpAddressEnd+0x598,看不到真實執行位置。32 位元版 TaskManager 位於 C:\Windows\SysWOW64\taskmgr.exe,執行後 Process 名稱應為「Task Manager (32bit)」。

    若 CPU 是不定期飆高一陣子後恢復,SysInternals 有個超好用工具 procdump,遇到 32 位元 Process 時會自動產生 32 位元 Dump 檔,還可以指定「當 CPU 高於 x% 並持續 y 秒時自動產生 Dump 檔」,可多加利用。
  2. 啟用 WinDbg 載入 Dump 檔案
    Windows Debugger 有 32/64 位元之分,請視要分析對象的位元版本選擇對應版本的 WinDbg。
    啟動 WinDbg 後,使用 File / Open Crash Dump 開啟剛才取得的 w3wp.DMP。

    註:Windows Debugger 下載安裝資訊請自行參考 MSDN,此處不多贅述。
  3. WinDbg 的指令既多且雜,有興趣的朋友請參考文件,此處記錄並說明我動用的指令:
    .sympath srv*X:\Symbol*https://msdl.microsoft.com/download/symbols
    分析過程需要 Symbol 檔,指定 WinDbg 自動由微軟網站下載,並 Cache 在 X:\Symbol 目錄避免重複下載
    !sym noisy
    指定顯示完整 Symbol 下載資訊
    .cordll -ve -u  -l
    自動載入 CLR 偵錯相關模組(分析來自其他台機器 Dump 檔時,這招好用) 
  4. 找出佔用 CPU 最多的 Thread,並將偵錯對象切換成該 Thread
    !runaway
    找出每個 Thread 消耗的 CPU 時間,在本案例中,Thread 27 耗用超過一個小時,無疑是吃光 CPU 的兇手
    ~27s
    將偵錯對象切換成 Thread 27

  5. 使用 !clrstack 列出 Thread 27 的 Callstack

兇手現形!MasterPage 中某個共用元件使用 ODP.NET 讀取使用者基本資料,程式卡在 Oracle.DataAccess.Client.OpsSql.ExecuteReader(),有可能等待 Oracle 資料庫回應時陷入無窮迴圈並耗盡 CPU。

陸續分析了幾起案例的 Dump 檔,都能找到一條 Thread 明顯耗用大量 CPU,而 Callstack 問題都指向 Oracle.DataAccess.Client.OpsSql.ExecuteReader(),鎖定問題後推測是網路設備干擾 Oracle 連線的問題(先前看過的症狀都是收到明確錯誤,像這樣 Hang 住的狀況是第一次遇到),避開後問題排除。

就這樣,靠著 WinDbg 漂亮偵破一個看似毫無頭緒的疑案。

呼口號時間:

WinDbg 好威呀!

ASP.NET CPU 飆高問題之傻瓜分析工具-DebugDiag Tools

$
0
0

昨天使用 WinDbg 追查 ASP.NET CPU 100% 原因的文章得到不少朋友的回饋,其中 Robert Hu 留言提到一個更方便的 Dump 擷取與問題分析工具,試用之下果然犀利,在此補上介紹。

Debug Diagnostic Tool (DebugDiag) 是微軟針對程式當掉(Crash)、當住(Hang),以及記憶體洩漏(Memory Leak)等問題設計的快速偵察工具,目前最新版為 Debug Diagnostic Tool v2 Update 2,共有三項兵器:

  1. DebugDiag 2 Collection

    自動化蒐集 Dump 檔的工具,可針對不同情境指定抓取 Dump 時機,例如:程式拋出非預期的例外、效能計數器達到門檻、HTTP 回應時間過慢、監測記憶體用量改變… 等,功能強大。


    DebugDiag Collection 最強悍之處在於設定規則守株待兔,當程序 CPU 使用量或其他效能指標超過門檻時匯出 Dump 檔(注意:匯出 Dump 期間程序會暫停,將影響線上使用者),對付「ASP.NET 不定期 CPU 飆高」之類的問題格外有用!
    下圖範例即為主機 CPU 使用百分比超過 30% 連續五秒時產生一到多個 AppPool Process 的 Dump 檔,有助於抓出耗用 CPU 的網站程式。
  2. DebugDiag2 RuleBuilder

    若 DebugDiag 2 Collection 預設抓取規則不夠用,Rule Builder 支援以視覺化流程圖方式自訂規則。
  3. DebugDiag 2 Analysis

    自動 Dump 分析工具,針對當機、當住及記憶體洩漏等情境預先寫好分析邏輯,輸入一個或多個 Dump 檔後自動產生分析報表。上回說到的 ASP.NET CPU 100% 問題,使用 PerfAnalysis 可以瞬間找到答案。

操作方法很簡單,如上圖所示,按下 Start Analysis,選好 Dump 檔,DebugDiag Analysis 就開始分析,稍待約一分鐘(運算時間長短視 Dump 大小、數量、分析項目而定)熱騰騰的分析報表網頁就出爐了。在報表尋找 Analysis Details /PerfAnalysis / Performance Summary / Statistical Rollups / Top 1 Operations By Duration,相當於用 WinDbg 尋找耗用 CPU 最久的 Thread 的分析資料,而共用元件呼叫 ODP.NET OracleCommand.ExecuteReader 的 Callstack 資料就在眼前。

使用 DebugDiag Analysis,只需幾個 Click 就能得到與 WinDbg 一連串指令操作相當的結果。若 WinDbg 是傳統單眼,DebugDiag Analysis 就像傻瓜相機,管它什麼快門調多快光圈開多大,按下快門就有好照片,還附贈美肌效果,神奇無比。

至於這兩種工具的選擇,若目的在檢視問題 Thread 當下的 Callstack,要「快」請用 DebugDiag Analysis,要「帥」就用 WinDbg 吧!哈。

Oracle 追討 Java 授權費議題之研究心得

$
0
0

前陣子有 Oracle 對企業追討 Java 授權費的新聞搞得人心惶惶:

被很多人問到「為了跑 Java 程式裝 JRE/JDK 也會被收錢嗎?」「裝什麼版本才會被收錢?」…

身為 Java 麻瓜,我知道才有鬼,新聞寫得不是很清楚,自己也好奇有無方法排除侵權疑慮,故門外漢爬文整理心得如下。(聲明:對此領域全然陌生,純為爬文心得拋磚引玉,如有錯誤歡迎補充指正)

  1. Java 產品很多,Oracle 官方網頁列舉的 Java 產品系列: 參考來源:PTT
    *Java SE (最通用的一般平台)
    *Java EE (企業級,例如:Serverlet)
    *Java ME (行動平台,已漸漸勢微)
    *Java SE Support (顧問服務)
    *Java SE Advanced & Suite (SE的擴增功能、更強大的SDK… 等)
    *Java Embedded、Java DB、Web Tier、Java Card、Java TV
    Java EE/ME 需購買授權眾所周知,最有爭議的是 Java SE,也就是一般 PC 跑 Java 程式要去 Oracle 網站下載的版本。
  2. Java SE(JRE、JDK)是免費的。BUT!在一些情境下需購買授權: 參考
    *用於「智慧系統中的專門嵌入式電腦」(specialized embedded computers used in intelligent systems)
    *透過工具來大規模部署開發完成的應用,例如:Windows Installer Enterprise JRE Installer工具」,會超出「一般運算用途」(general purpose computing)
    *使用 Advanced Desktop、Advanced、Suite 等進階商業付費功能
    前兩者情境很好識別,但第三種情境就有點模糊空間,甚至像是陷阱。

  3. 進階商業付費功能變成陷阱的原因在於 Oracle 沒有為 Advanced Desktop、Advanced、Suite 拆出獨立安裝程式,全部包在 Java SE 標準安裝裡,就算你不想用,一旦下載安裝,電腦就具備使用 Advanced Desktop、Advanced、Suite 的能力,啟動 JVM 時加上 –XX:+UnlockCommercialFeature 參數即可開啟。
    實際觀察,Java SE Adavnced 跟 Suite 的下載連結就是指到 Java SE 標準版的下載連結,應可證明 Oracle 並未區隔出不同的安裝程式。

    另外,我也找到一篇國外討論也提到 Oracle 沒有針對免費及付費產品提供不同安裝程式的問題:
    "Java SE is free for what Oracle defines as “general purpose computing”...But it is customers in these general-purpose settings getting hit by LMS. The reason is there’s no way to separate the paid Java SE sub products from the free Java SE umbrella at download as Oracle doesn’t offer separate installation software"
    這讓我想起餐廳「還沒點菜桌上就擺了幾盤小菜,等著你一挾就得付錢」的小伎倆,合法但存在爭議。
    在沒有選擇的情況下安裝了付費功能,變成使用者得證明自己沒用不需付費,稱之為陷阱也不為過。

  4. 一般使用者自然不會知道 –XX:+UnlockCommercialFeature 這種鬼參數,只知道點兩下程式就會動了。若使用者下載了第三方提供的 Java 程式,開發者在啟動程式批次指令裡下了 –XX:+UnlockCommercialFeature 參數… 其實我搞不懂,這樣子是開發者要付費還是使用者要付費?
    付費功能是使用者安裝的沒錯,但使用者可主張自己從沒打算使用(其實是被硬塞的),封印被第三方程式解除,此時該算誰的責任是個好問題。
    這有點像在手機裝了某個 App 在亂發簡訊,帳單得自己繳沒話說,但手機裝 App 前會有明顯的要求簡訊發送的授權確認,Java SE 進階功能在啟用時少了明確親民的提示,就易惹來爭議。

新聞裡有提到:

不少企業已經請授權專家和 Java 專家來檢視自家的 Java 版權。專家建議,當使用者下載 Java 就會得到所有的元件,因此必須要確認你只安裝那些需要且符合授權的元件,並刪除不需要的元件。

但依據上述研究心得,沒授權但不想用元件會被包在 Java SE 一起被裝起來,要怎麼刪除是個謎。而 Oracle 為什麼把事情搞得這麼複雜?把 Java SE 安裝程式分成免費版跟商業版,很難嗎?

最後,若想徹底遠離扯不清的 Java SE Advanced Desktop、Advanced、Suite 授權爭議,有幾個辦法:

  • 如果完全用不到 Java 程式,可以移除 Java SE 製造不在場證明
  • 改用 OpenJDK吧!開源萬歲~

2017 渣打馬拉松與 SUB 4 之夢

$
0
0

跑馬當踏青,荒唐一整年,總得有場玩真的,渣打馬就是你了!

去年渣打馬遇到連貓空都下霰的霸王級寒流,今年雖是暖冬,渣打馬照例再跟入冬最冷寒流強碰,原本預測將到 10 度以下,最後是 11 度的乾冷天氣。老天爺送上大禮,加上近來無傷無痛,這様還破不了 PB(個人最佳成續)豈不人神共憤,天地不容?

賽前我還很假掰認真地參考 RunningQuotient分析報表調整跑量,設法拉高狀況指數(狀況=體能-疲勞),破 PB 勢在必得。

我的 PB 是 2015 年國道馬創下的 4 小時 15 分,之後十來場都在吃喝玩樂,視成績如浮雲。近半年體重維持不錯加上核心肌群練得勤,去年入冬天氣轉涼後相同配速的心率明顯下降。晨跑 5K 心率 M 區間(HRR 80-88%) 可以跑出 Pace 5:10-5:30,破 PB 情勢大好。只是,之前兩次妄想扣關 SUB4 的經驗告訴我,能維持目標配速穩穩跑完 30 K 不等於有本事撐到完賽,35K 後兵敗如山倒的滋味難以忘懷。即便近況不錯,感覺 SUB4 仍是遙不可及的目標。

清晨四點半出門,拜 SuperDry 極度乾燥之賜,體感溫度不如預期冷,是好兆頭。會場離家算近,不用半小時就抵達總統府,算是天時地利,感覺破 PB 的希望又更濃了。

這張照片說明我熱愛小而美賽事的理由:

起跑前來一張,拼 PB 專心跑都來不及,就乖乖把手機收起來不拍照了,不留給自己任何「我是邊跑邊拍所以才…」的爛藉口。

在意成績,刻意排在前面一點想多少節省超車時間,另一方面前段跑友平均實力略強,寄望靠「同儕壓力」逼出潛能。這場比賽有好多 Garmin 全馬 PB 班同學穿著醒目的紫色漸層背心,個個實力堅強(即使女跑友亦然),是很棒的假想敵。

上屆四小時內上五次廁所搞到信心崩盤自暴自棄,參考這陣子天冷練跑的補水經驗,決定每個水站只喝一杯,避免喝多頻尿悲劇重演。這次全程只上了一次廁所,算是策略執行成功。至於配速,跟教練團(咦?)討論後,決定回歸 Follow Your Heart 的 HRDR(Heart Rate Driven Run),心率維持在平日最習慣的 M 區間不爆衝,預期配速應可自然落在平日晨跑慣用的 5:20。

1-10K: 05'57"/05'21"/04'58"/05'11"/05'13"/05'23"/05'11"/05'50"/05'11"/05'16"
起跑後人群洶湧,但仍能拉到 6 分速,排在前段多少有幫助,第二公里後就能依自己預期的速度前進,前 10K 花了 53分跑完。

11-20K:05'13"/05'16"/05'10"/05'24"/05'08"/05'19"/05'04"/05'42"/05'11"/05'16"
11K 起狀況維持得不錯,配速都在 5'15" 左右,只有一次停水站吃了幾塊餅乾多花了 30 秒,20K 只花 1 小時 46 分。

21-30K:05'18"/05'24"/05'06"/05'19"/05'24"/05'24"/05'29"/05'28"/05'35"/5'31"
前 21K 跑了 1:52:02,破了半馬 PB,心情大好,但身體很誠實地說「我快沒力了…」,速度開始下滑至 5'30",前 30K 2 小時 40 分完成,繼續刷新記錄。

31-40K:05'18"/05'39"/05'41"/05'48"/05'42"/05'43"/05'43"/06'19"/05'35"/05'55"
最恐怖的 30K 大魔王來了!果不其然,速度再下滑到 5'40", 36K 起大腿開始硬化,感覺已站上抽筋邊緣,將速度放慢緩和一陣子抽筋感才消失,但已無力加速。但看看時間擦了擦眼睛,有點不敢相信,這場真的有機會 SUB 4 了,只求最後不要抽筋失速,

41-42K:06'02"/05'46"/05'41"
使出吃奶力氣,咬牙拖著僵硬的兩腿跑完最後兩公里… 天啊,第一次跑完全馬計時器還在 3 字頭,SUB 4!我辦到了!

沒有想像中的熱淚盈眶,心情甚至不如前兩次扣關失敗激動,就這樣平靜地跨過自己跑馬生涯的新里程碑。

當年沒破四就偷買先用的 Fenix 3,今天起總算可以戴得名正言順了,哈!

晶片成績 3:55:05,還意外突破傳說中的「陳冠希障礙」!(不過,接下來鐵腿兩天 Orz 證明拎杯真的盡全力了)


照片來源:http://sports.qq.com/a/20151216/012453.htm

垂垂老矣的馬拉松五年級生,SUB 4 完成!

公告:Facebook 粉絲專頁破萬抽獎活動

$
0
0

剛圓了 SUB4 之夢,部落格也悄悄跨過另一座里程碑-黑暗執行緒臉書專頁按讚數破萬了!(轉圈灑花)
PS:咦?好像有人衝去退讚了… 哼!不管!拎杯不在乎天長地久,只在乎曾經擁有。

為了紀念這個小小成就,還記得以前辦過的五百萬人次紀念抽嗎?是的,黑暗執行緒萬讚紀念抽來了!

先介紹本次抽獎獎品-薑!薑!薑!薑~獨一無二但毫無價值的「黑話(黑大說過的話)紀念書籤」,全球限量推出三枚,由黑大親手設計與製作:

(謝謝兩位臨演,可以下去領 500了)

書籤為廉價飛機木板高質感原木素材經 3D 雷射蝕刻加工而成,書籤收錄的金句字字珠璣,在您查不到Bug重開沒效找不出問題,人生失意徬徨迷惘手足無措之際,為您點亮一盞明燈… 以上都唬爛的,其實就只是有趣的紀念品而已。第一次玩雷射雕刻,這三枚書籤搞了我好幾天,燒了一堆瑕疵品,工藝水平不怎樣,但保證誠心打造。

我辦的抽獎當然要用我心中最公平公正公開的 Open Source 電腦抽獎法-程式演算法與亂數種子取碼規則預先公開,且需支援反覆驗證。懶得重想,直接借用上次五百萬紀念抽的程式,只微調最後一段改成抽出三位得獎者。Online Demo

using System;
using System.Collections.Generic;
using System.Linq;
 
publicclass Test
{
publicstaticvoid Main()
    {
string raw = @"1.Jeffrey
2.Darkthread
3.球證
4.旁證
5.主辦
6.協辦
7.全都是我的人";
            List<string> candidates = new List<string>(raw.Split('\n'));
            Random rnd = new Random(975047);
            Console.Write(
string.Join("\n", 
                candidates.OrderBy(o => rnd.Next()).Take(3).ToArray()));
    }
}

至於亂數種子就取 D 日的台股收盤指數,以 2/24 為例,台股加權指數為 9,750.47,亂數種子就取 975047:

要如何參加抽獎呢?請大家在臉書抽獎貼文留言「抽」或「+1」就可以囉~(以臉書留言為準,部落格文章留言恕不計入)

報名截止時間(D日T時)使用以下 JavaScript 取出名單,另排序以「最新動態」為準:

 

var dupCheck={}, list=[], idx=0;
document.querySelectorAll(".UFIList .UFICommentActorName").forEach(function(span) { 
var id=span.getAttribute("data-hovercard").match(/id=(\d+)/)[1];
var name=span.innerText; 
if (id in dupCheck || name == "黑暗執行緒") return;
  dupCheck[id]=name;
  list.push(idx++ + "." + name + "(" + id + ")");
});
console.log(list.join("\n"));

將名單送入程式,輸入 D 日台股指數當亂數種子,三位得獎就出爐囉~

好了,祝大家幸運中獎!

PS1:獎品很鳥,就不請律師公證了,大會保留任意修改及解釋抽獎規則的權利(靠,是有沒有這麼蠻橫?),如果擔心規則不公權益受損,請勿參加以免受氣。

PS2:中獎者請透過粉專訊息跟我連絡,由於獎品將採郵寄方式,依慣例若中獎者住在海外、外星球或其他銀河系,恕只能代寄到指定的台灣住址。

使用 Process Explorer 查看 .NET Callstack

$
0
0

WinDbg 追查 CPU 飆高問題一文發表後,在 FB 收到網友 Webber Han 回饋(在此感謝),提到射茶包利器 Process Explorer也能像 WinDbg 一樣檢視 Callstack 中的 .NET 組件、函式資訊,查了一下,這是 2012 年 15.2 版就加入的功能,Lag 大了。

關鍵在於「Configure Symbols」有無設定妥當,Process Explorer 的 .NET Callstack 解析也是借助 WinDbg 完成,故機器要先裝妥 WinDbg,開啟 Process Explorer 選單 Options / Configure Symbols… 將 Dbghelper.dll 改指向 WinDbg 的 dbghelper.dll,並比照 WinDbg 設定 Symbols path:

我寫了一支執行數十萬次 JsonConvert.SerializeObject 的 ASP.NET 程式當成測試對象。開啟 Process Explorer 找耗用大量 CPU 的 w3wp.exe,點兩下開啟 Properties,切換到 Threads 頁籤,找出佔用 CPU 最多的 Thread,點選 Stack:

此時 Stack 資訊可看出 ASP.hang_asp.Page_Load() 呼叫 HangTest.dll TestHelper.RunTest() ,RunTest() 呼叫 RunJsonTest(),其中再呼叫 JsonConvert.SerializeObject() 的呼叫關聯。

如此便能即時檢視執行中 .NET 程序的 Callstack,快速鎖定問題來源,在調查程式當住(Hang)問題時又多了一項選擇!


TIPS-以不同使用者身分執行程式

$
0
0

在一些情境下,我們需要切換成其他使用者身分執行程式,例如:以 UserA 登入 Windows,因特殊需求改用 UserB 帳號啟動特定程式。一個經典範例是 SSMS,如下圖所示,當選擇「Windows Authentication」認證方式,Username 欄位固定為當下登入帳號,無從改變。

要改變 SSMS 中的 Windows Authentication Username,就必須改用其他使用者身分執行 SSMS。Windows Vista 起有個內建命令列工具 Runas能指定執行身分,但找出程式路徑還有敲指令,有點費事:

runas /netonly /user:domain\user "C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\ManagementStudio\Ssms.exe"

最近我才發現,Windows 7 起可直接透過右鍵選單「以不同的使用者身分執行」(Run as different user),方法是按著 Shift 再按滑鼠右鍵,「以系統管理者身分執行」下方就會多出「以不同的使用者身分執行」。

跟連線網路磁碟機一樣,Windows 會彈出登入對話框,輸入帳號密碼完成驗證就能以其他使用者身分執行程式囉~

在 TFS 2012 Build Service 編譯 VS2015 專案

$
0
0

工作環境用的是 TFS 2012 Build Service,最近要編譯 VS2015 專案,程式用到 C# 6.0 超好用的字串插值寫法當場被打臉,得到 Unexpected character '$' 錯誤。原因很明顯,VS2015 改用 Roslym 編譯器,TFS 2012 Build Service 上沒裝是要編個屁?

經過一番摸索(還學到 TFS 的編譯範本原理)終於搞定,細節整理如下。

首先要下載 MS Build Tools 2015安裝到 Build Service 主機。(安裝完整 VS2015 應該也成,但有殺雞用牛刀之嫌)

安裝完畢 Program Files (x86) 目錄會多出 14.0 資料夾,這個路徑稍後會用到。

建置定義編輯畫面的 Process 區有個 Build process template 項目,預設是收合的,打開可以目前使用 Default Template (DefaultTempalte.11.1.xaml),而它位於 TFS 版控該專案的 BuildProcessTemplates 資料夾(每次建立 TFS 專案都會看到系統自動產生 BuildProcessTemplates 目錄,至今才明白它的用意)。這個 XAML 檔是一套 Windows Workflow 流程,最棒的是可直接用 Visual Studio 視覺化編輯。由於 DefaultTemplate.11.1.xaml 適用 VS2012/VS2013,需要修改才能用於 VS2015 專案。修改此範本使用其支援 VS2015 是種解法,而更好的做法是另外新增一個 VS2015 專用的建置範本,二者並存,未來再視 VS2013 或 VS2015 專案選擇適用範本。

其實 DefaultTemplate.11.1.xaml 只要小改路徑設定就可以編譯 VS2015 專案,我複製一份並改名為 DefaultTemplate.VS2015.xaml 放在 BuildProcessTemplates 目錄下,點擊兩下即可使用 Visual Studio 修改流程。在流程圖中找到「對專案執行MSBuild」:

開啟其屬性設定找到 ToolPath 項目填入我們安裝 MS Build Tools 2015 的路徑:"C:\Program Files (x86)\MSBuild\14.0\Bin":

此處有個眉角-XAML 裡還有另一個「對專案執行MSBuild」,也要修改:

將 XAML 存檔並簽入 TFS,接著將建置定義的建置範本換成 DefaultTemplate.VS2015.xaml(若它不在下拉選單中,請使用 New 選取加入)重新執行建置,就可以在 TFS 2012 Build Service 成功編譯 VS2015 專案囉~

查詢 SQL Server 詳細版本資訊

$
0
0

資安更新作業需確認 SQL Server 詳細版本資訊,需細到 Service Pack 及累積安全更新,爬文查到超好用的 SQL Server 版本資訊偵測腳本。這組自動偵測腳本由微軟OneScript Team提供,可顯示版號、產品名稱、產品層級(Service Pack 版本)、版本別(Express、Enterprise、Developer…)、32 / 64 位元、Service Pack 版本、累積更新(Cumulative Update)等資訊。

更厲害的來了,偵測結果還包含後續若要更新,最新版 GDR(General Distribution Release)及 QFE(Quick Fix Eningeering)的下載網址。

【小辭典】

SQL Server 更新的種類超多,順手整理如下: 參考

  • QFE,Quick Fix Engineering,也常被稱為 Hotfix
    較未經過大量測試的修正,經索取才提供(留下 Email 才寄送下載連結,也方便事後發現問題通知),通常用於較緊急狀況。
  • COD,Critical On-Demand
    針對影響嚴重、範圍層面廣或安全相關問題所出的緊急更新,通常會收納到下次 CU/SP,COD 有時會包含多個 QFE。
  • CU,Cumulative Update
    在 SP 與 SP 之間的更新,通常以 60 天為單位匯集發行,測試完整度不如 SP,安裝到正式環境前應先測試,但微軟鼓勵積極更新,可省去追查一些已被修復的問題。延伸閱讀(傳統上 CU 測試與驗證完整度不如 SP,但近年來 CU 的可靠性已接近 SP
  • GDR,General Distribution Release
    一些重大或安全相關更新的累積更新,若沒有額外資訊測試 CU,GDR 是風險較低的選擇。
  • SP,Service Pack
    可視為特殊標示的 CU,包含安全更新、Bug 修復,有時還會包含新功能。

以下為版本偵測結果範例:

---------------------------------------------------------------------------------------------------------
--//Your current Microsoft SQL Server information:
---------------------------------------------------------------------------------------------------------
Product Version:          11.0.5343.0
Product Name:             SQL Server 2012
Product Level:            SP2 + Security update(GDR)
Product Edition:          Developer Edition (64-bit)
---------------------------------------------------------------------------------------------------------
Note, if you want to know information about CU, please read this KB below.
KB321185, 
---------------------------------------------------------------------------------------------------------
Support Lifecycle stage: Mainstream Support Phase. For additional information refer to 
https://support.microsoft.com/en-us/lifecycle/search?sort=PN&alpha=SQL%20Server&Filter=FilterNO, and Q6, Q18
in the FAQ section of Support Lifecycle page at: https://support.microsoft.com/en-us/lifecycle#gp/lifePolicy
---------------------------------------------------------------------------------------------------------
Full information:
Microsoft SQL Server 2012 - 11.0.5343.0 (X64) 
	May  4 2015 19:11:32 
	Copyright (c) Microsoft Corporation
	Developer Edition (64-bit) on Windows NT 6.3  (Build 9600: ) (Hypervisor)

---------------------------------------------------------------------------------------------------------
--//Recommended updates: 
--### RTM -> QFE or GDR
--### SP  -> QFE or GDR
--### QFE -> QFE
--### GDR -> GDR or QFE
---------------------------------------------------------------------------------------------------------
Install the latest service pack:              SP3, 
Install the latest Cumulative Update (CU) of SP3:  CU7, 
---------------------------------------------------------------------------------------------------------
###### QFE branch updates
---------------------------------------------------------------------------------------------------------
11.0.2376 (SQL Server 2012 RTM QFE) http://support.microsoft.com/en-us/kb/2716441
11.0.3513 (SQL Server 2012 SP1 QFE) https://support.microsoft.com/en-us/kb/3045317
11.0.5613 (SQL Server 2012 SP2 QFE) https://support.microsoft.com/en-us/kb/3045319
---------------------------------------------------------------------------------------------------------
###### GDR branch updates
---------------------------------------------------------------------------------------------------------
11.0.2218 (SQL Server 2012 RTM GDR) https://support.microsoft.com/en-us/kb/2716442
11.0.3153 (SQL Server 2012 SP1 GDR) http://support.microsoft.com/kb/2977326/en-us
11.0.3156 (SQL Server 2012 SP1 GDR) https://support.microsoft.com/en-us/kb/3045318
11.0.5343 (SQL Server 2012 SP2 GDR) https://support.microsoft.com/en-us/kb/3045321
---------------------------------------------------------------------------------------------------------
Note, if you don't want to upgrade to latest service pack right now, we recommend you install the latest
Cumulative Update CU16 of SQL Server 2012 SP2.
Install the latest Cumulative Update (CU) of SP2: CU16, 


---------------------------------------------------------------------------------------------------------
--//You can upgrade to any of the following product(s):
---------------------------------------------------------------------------------------------------------
SQL Server 2014 Developer
SQL Server 2016 Enterprise
SQL Server 2016 Business Intelligence
SQL Server 2016 Standard
SQL Server 2016 Web
SQL Server 2016 Developer


For additional information about supported version and edition upgrades refer to:
https://technet.microsoft.com/en-us/library/ms143393(v=sql.120).aspx

Windows Open JDK 替代方案研究

$
0
0

前陣子分享過 Oracle 追討 Java 授權費議題的研究心得,原以為誤用同綁安裝的進階商業功能是主要陷阱,但依最近蒐集到的情資,發現對企業用戶而言,「General Purpose Computing」才是天大的坑。依據 Binary Code License(BCL),Java SE 8 只可免費用於「General Purpose Computing」,用於 Embedded Device 或 Other Computing Environment 就得付費。(參考 The current version of Java - Java SE 8 - is free and available for redistribution for general purpose computing. Java SE continues to be available under the Oracle Binary Code License (BCL) free of charge. JRE use for embedded devices and other computing environments may require a license fee from Oracle. )至於在企業內部的個人電腦安裝及執行 Java SE 是否屬於 General Purpose Computing 用途?BCL 主張「 The use of the software in systems and solutions provides dedicated functionality」就不能視為 General Purpose Computing ,除非鬧上法院,是不是 General Purpose Computing 誰說了算?你我的膝蓋都知道答案。

對企業而言,遠離付費爭議的最好方法是離 Oracle Java SE 愈遠愈好,網路上很多人推薦改用 OpenJDK,但 OpenJDK 官網只針對主流 Linux 提供安裝套件,建議 Solaris、Mac OS X、Windows 使用者去找 Oracle。(啊啊啊啊~)

爬文找到一些 Windows 使用 Open JDK 的解決方案:

  1. RedHat 版
    RedHat 從 2016 年 6 月開始提供 Windows 版 Open JDK,使用者可下載 MSI 在 Windows 安裝,其初衷是方便開發者在 Windows 開發與測試 JBoss Middleware 相關程式,下載前需先填寫資料註冊開發者會員,程序繁瑣了點。
  2. 開源社群編譯版本 ojdkbuild
    由開源社群熱心維護:https://github.com/ojdkbuild/ojdkbuild可直接下載安裝,且持續定期更新。
  3. JDK8 Reference Implementation
    官方提供的 JDK8 實作範例:https://jdk8.java.net/java-se-8-ri/,Linux、Mac OS X、Solaris 各平台都有,分 GPL 與 BCL 兩種授權。
  4. Azul System Zulu
    Azul System 這家公司推出基於 Open JDK 開發的開源 Zulu,標榜通過官方 TCK 認證,與 Java SE 完全相容,開放免費下載及使用,Azul 公司則靠技術支援服務營利。

經評估與測試,RedHat 版需註冊才能下載有點麻煩;Reference Implementation 只能算是 PoC 不會持續發展;開源社群版安裝檔下載與安裝都很方便,唯一的小缺點是 MSI 欠缺數位簽章可能被資安單位挑剔;Zulu 通過 TCK 認證且有廠商背書,感覺更可靠,但商業產品多少會隱含授權政策改變的風險。
【2017-03-11 更新】感謝蘇國鈞老師補充:「Reference Implementation」在 Java 這邊習慣上翻作「參考實作」,簡稱 RI。因為 Java 定義的規格 (JSR) 大家都可以實作,但是收費與否、License 限制各有不同,所以 RI 基本上是一個免費、提供大家學習與實作的一個參考版本。而且很多 RI 實作的嚴謹程度不只是 Proof of Concept,其實是做到 Production-Ready,並且是持續改進的。很多商業/非商業版本的 Application Server,因為必須實作出某些規格才能夠取得 Java EE 認證,或者是不想要/不需要重複發明輪子,常常也都會引用 RI,再加以擴充,來當作自己的實作版本。

最後選擇 ojdkbuild 版,下載 MSI 安裝完畢,小試了 HelloWorld,編譯與執行都沒問題:

接著來測試複雜一點的,我刻意挑戰了 Oracle 自己的 Oracle SQL Developer,選擇下載不含 JDK8 的安裝檔。在沒有安裝 Oracle Java SE JDK 的狀況下,程式啟動時會詢問 JDK 位置,將其指向 ojdk-build 的安裝路徑(留意路徑不需包含 bin):

薑薑薑薑~ Oracel SQL Developer 可以使用 ojdkbuild 版 JDK 執行沒問題。(補充:有些程式需依賴 JAVA_HOME 環境變數,ojdkbuild 不會主動設定,必要時需手動補上)

連 Oracle SQL Developer 用 ojdkbuild 版 JDK 都能過關,使用 Open JDK 執行 Java 應該不用太擔心。比較麻煩的是網頁內嵌 Java Applet 的應用情境,Oracle Java SE 提供與 IE、Firefox、Safari 等瀏覽器整合的 Plugin,如要改用 Open JDK 得自己想辦法。IcedTea-Web計劃以自由軟體形式推出允許瀏覽器執行 Java 的套件,但很遺憾沒有現成的 Windows 版實作(主要是 Firefox in Linux),相關研究極少,在 Github 找到一個未成形的 IcedTea-Web for Windows 專案,幫助不大,Zulu 也坦承沒有加入支援的計劃,加上 Java Applet 技術目前已是風中蟾蜍殘燭來日無多(Chrome 很阿莎力地從 42 版就不再支援),不指望有人再投注心力,這塊看來是無解了。要在 Windows 續用 Java Applet,只能回歸 Oracle Java SE,或許也可當成一項促使 Java Applet 開發商更快轉向 HTML5 的理由吧!

一開始順順利利解掉大半問題,最後卻在 Java Applet 挨了一記回馬槍,好有吃鍋貼的感覺~(補聲暗)

崇尚自由開放的 Java 被 Oracle 吹皺一池春水,而 C#/.NET 則逐步走向自由開放,還跨了平台,世界真奇妙。

【茶包射手日記】程式安裝與解除安裝疑難排解員

$
0
0

來了!來了!從山坡上輕輕地爬下來了。Visual Studio 2017 3/7 RTM囉~

家裡跟公司有好幾台機器要裝,照著小朱的教學文抓好離線安裝包(我選 Enterprise 英文版,全部安裝檔約 20.6 GB),避免逐台重複下載耗時費頻寬又不環保。按照慣例,身為茶包射手體質異於常人,安裝 Visual Studio 一次 OK 成何體統?(案例案例案例)是的,我又踩到水坑了~

本次遇到的問題安裝過程出現 Microsoft.VisualStudio.WebDeploy 安裝失敗,導致 .NET Core、.NET 桌面開發及 ASP.NET 與網頁程式開發裝不起來。

無法安裝套件 'Microsoft.VisualStudio.WebDeploy.Msi,version=15.0.26208.0,chip=x64'。
    搜尋 URL: https://aka.ms/VSSetupErrorReports?q=PackageId=Microsoft.VisualStudio.WebDeploy.Msi;PackageAction=Install;ReturnCode=1316
    受影響的工作負載
        .NET Core 跨平台開發 (Microsoft.VisualStudio.Workload.NetCoreTools,version=15.0.26208.0)
        .NET 桌面開發 (Microsoft.VisualStudio.Workload.ManagedDesktop,version=15.0.26208.0)
        ASP.NET 與網頁程式開發 (Microsoft.VisualStudio.Workload.NetWeb,version=15.0.26208.0)
    受影響的元件
        ASP.NET 與網頁程式開發工具 (Microsoft.VisualStudio.Component.Web,version=15.0.26208.0)
        Web Deploy (Microsoft.VisualStudio.Component.WebDeploy,version=15.0.26208.0)
        Windows Communication Foundation (Microsoft.VisualStudio.Component.Wcf.Tooling,version=15.0.26208.0)
    記錄
        C:\Users\Jeffrey\AppData\Local\Temp\dd_setup_…_Microsoft.VisualStudio.WebDeploy.Msi.log
    詳細資料
        MSI: C:\ProgramData\Microsoft\VisualStudio\Packages\Microsoft.VisualStudio.WebDeploy.Msi,version=15.0.26208.0,chip=x64\webdeploy_x64.msi,屬性:  REBOOT=ReallySuppress
        傳回代碼: 1316
        傳回代碼詳細資料: 指定的帳戶已存在。

爬文推測為 Web Deploy 套件之安裝資訊毁損,導致移除或升級失敗。試著手動解除安裝,看到一模一樣的「指定的帳戶已存在訊息」。

針對無法安裝或無法解除安裝的疑難雜症,微軟有個自動修復工具,能修復以下問題:

  • 64 位元作業系統的登錄機碼損毀
  • 控制更新資料的損毀登錄機碼
  • 無法安裝新程式問題
  • 無法解除安裝或更新現有程式問題
  • 無法由 [控制台]中透過 [新增或移除程式] (或 [程式和功能]) 解除安裝程式的問題

從沒用過,抱著姑且一試的心情試跑(命運之神會這麼輕易放過我嗎?),程式詢問要解除安裝的程式,Microsoft Web Deploy 3.6 不在其中,依提示選取「未列出」:

第一關來了,請填入產品代碼 GUID… 嗯,還好我對 GUID 也是略懂略懂,就算不知產品代碼,還是可以用暴力破解,估計試過 103 百萬兆次就會有 10 億分之一的成功機會:

別鬧了!身為 Windows 老鳥,很快在 Registry 搜尋關鍵字找出答案。

答案正確,程式成功找到「Microsoft Web Deploy 3.6」,詢問要解除安裝還是嘗試其他修正,選擇解除安裝。

之後經過「毁損的修補登錄機碼」(這啥?阿鬼,你還是說英文吧!)、「查看修補程式相關問題的登錄」、「嘗試使用下列項目解決問題:Microsoft Web Deploy 3.6」等過程,大功告成!

Microsoft Web Deploy 3.6 從解除安裝清單消失,重試一次,Visual Studio 2017 安裝完成,萬歲!

找到工具這麼快就把問題解了,還真不習慣,哈!(謎:是有沒有這麼賤骨頭啦?)

Windows 停用 TLS 1.0 之配套作業整理

$
0
0

開始之前,說說 TLS。

大家朗朗上口的 SSL(Security Socket Layer),最早源於 1995 年發表的 SSL 2.0(1.0 很雷,所以從沒公開過),隨後在 1996 推出 3.0 版,IETF 於 1999 年將 SSL 標準化,因版權考量改稱為 TLS(Transport Layer Security)。就技術而言, TLS 1.0 與 SSL 3.0 很相近,而 TLS 1.0 也支援降級改用 SSL 3.0。之後 IETF 分別在 2006、2008 年再推出安全強度更高的 TLS 1.1 與 TLS 1.2。

2014 年,Google 發現 SSL 3.0 存在致命安全漏洞,而攻擊者可藉由向 TLS 發送假的錯誤提示降級至 SSL 3.0,再利用 SSL 3.0 的漏洞竊取資訊,各家瀏覽器紛紛決定禁用 SSL 3.0。因此大家常說的 SSL,其實早已經被 TLS 取代。參考來源

TLS 1.0 因 CBC 加密模式設計不良,可能遭受 BEAST 攻擊導致加密內容被解密,便落入與 SHA1 相同的命運-被業界宣判限期下架,於是,「停用 SSL 3.0 與 TLS 1.0」成為資安檢核項目之一,建議系統管理人員早日關閉。

要在 Windows 停用 TLS 1.0、啟用 TLS 1.1、TLS 1.2,只需修改 Registry即可完成。且慢,先別急著動手,刺激的在後面… 以下是這陣子雞飛狗跳摸石頭過河後的心得整理,短短幾行卻血淚交織,價值不斐:

  1. 停用 TLS 1.0 後遠端桌面連不上
    「用遠端桌面登入主機,設好 Registry 重開機後就再也連不進去了」這種劇情很驚險刺激吧?
    Windows 7 及 Windows 2008 R2 需先安裝更新,遠端桌面程式需更新到新版才支援 TLS 1.1/1.2 連線。
  2. 停用 TLS 1.0 後 SQL Server 起不來
    「 資料庫主機停用 TLS 1.0 並重開機,SQL Server 就起不來了」這種橋段也扣人心弦吧?
    在事件檢視器可看到類似錯誤: 
    A fatal error occurred while creating an SSL client credential. The internal error state is 10013.
    MVP Aaron Bertrand 有篇 Blog詳細整理各版本 SQL Server 要支援 TLS 1.2 需要的最小版號,至於 SQL 詳細版本資訊的查詢方式,可參考前幾天的文章
  3. SQL Server 停用 TLS 1.0 後,.NET 程式無法連上資料庫
    SQL 更新並停用 TLS 1.0 後,原本使用 SqlConnection 連線 SQL 的 .NET 程式可能出現以下錯誤訊息:
    使用 Integrated Security=SSPI 以 AD 登入 - A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: Shared Memory Provider, error: 0 - No process is on the other end of the pipe.)
    使用 SQL 帳號登入 - A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
    是的,ADO.NET 也得更新,請參考微軟這篇 KB,依據使用的 .NET 版本安裝對應的更新。除了 .NET 4.6,.NET 2.0/3.5/4.0 到 4.5.2 都需更新才能以 TLS 1.2 連上 SQL。
  4. 連線 TLS 1.2 HTTPS
    .NET 客戶端使用 WebClient、WCF 以 HTTPS 連線遠端主機,也會涉及 TLS 1.0/1.1/1.2 版本議題,不同版本 .NET 的處理方式不同:
    .NET 4.6內建支援且預設使用 TLS 1.2
    .NET 4.5內建支援,但需透過 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 設為預設協定
    .NET 4本身不支援,但安裝 .NET 4.5 後即可使用 TLS 1.2,指定 TLS 1.2 的寫法為 ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
    (註:若不想修改 .NET 4/4.5 程式,也可透過 Registry修改預設安全協定)
    .NET 3.5需安裝 Hotfix 才能支援
        KB3154518 – Reliability Rollup HR-1605 – NDP 2.0 SP2 – Win7 SP1/Win 2008 R2 SP1
        KB3154519 – Reliability Rollup HR-1605 – NDP 2.0 SP2 – Win8 RTM/Win 2012 RTM
        KB3154520 – Reliability Rollup HR-1605 – NDP 2.0 SP2 – Win8.1RTM/Win 2012 R2 RTM
        KB3156421 -1605 HotFix Rollup through Windows Update for Windows 10.

事後來看遇上這些狀況合情合理,當初處理過程可不是這麼一回事:

遠端登入改完 Registry 重開機,之後遠端桌面連不進去、連進去發現 SQL Server 起不來、把 SQL 救起來後發現連 SQL 的 .NET 程式壞光光… 不知所以然胡搞瞎試搞完一回合,學新知還兼練心臟,好激刺呀~

以上是目前我蒐集到停用 TLS 1.0 所需的配套更新,未來如有發現再陸續補充更新。

客製靜態檔案 HTTP 404 訊息

$
0
0

同事報案,某組 Windows 2012R2 Web Farm 均已設定 web.config <customErrors mode="On" /> HTTP 404 網頁理應如下:

但 Web Farm 其中一台卻會顯示詳細錯誤,導致實體路徑資訊外洩:

最後同事找出原因,IIS Error Pages 設定有個 Edit Feature Settings,問題主機被設成「Detail Errors」:

心中對這組設定與 customErrors 的關係滿心狐疑,爬文後才驚覺自己寫過文章:ASP.NET 相關程式錯誤由 <system.web><customErrors> 控制,靜態檔案(html、gif、png、jpg、js、css)則由 <system.webServer> <httpErrors> 決定,不過一年多前的事竟忘得一乾二淨,特再撰文一篇加強印象,確保此生不忘,阿彌陀佛~


Dapper +Oracle 之 DateTime 注意事項

$
0
0

同事報案,我先前寫的 Dapper 共用程式庫有 Bug,當 WHERE 條件包含日期型別時,將 DateTime 寫入 Oracle Date 欄位,接著用同 DateTime 值做 WHERE 比對,竟找不到剛才寫入的資料。

用以下範例重現問題:

using (var cn = new OracleConnection(csOra))
{
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)", new { idx, dttm });
//重新該時間當WHERE條件比對,筆數為0(登楞!)
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm", new { dttm }).Count();
    Console.WriteLine($"Count={cnt}");
}

第一時間懷疑是 Oracle 的 Date 欄位精準度只到秒造成,元件配合 SQL 已使用好一陣子沒遇過此狀況,應是 SQL DateTime 欄位精準度可到秒以下,故同樣做法在 SQL 不會出錯:

using (var cn = new SqlConnection(csSql))
{
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (@idx, @dttm)", new { idx, dttm });
//重新用DateTime.Now當WHERE條件比對
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = @dttm", new { dttm }).Count();
//筆數為1,測試無誤
    Console.WriteLine($"Count={cnt}");
    Console.Read();
}

有趣的是,若 Oracle Date 搭配 .NET DateTime 有此陷阱,過去使用 ODP.NET 何以相安無事多年?於是我改用 OracleCommand、OracleParameter 做測試,也不會有日期比對不符的狀況:

using (var cn = new OracleConnection(csOra))
{
    cn.Open();
    var cmd = cn.CreateCommand();
    cmd.CommandText = "TRUNCATE TABLE JEFFTEST";
    cmd.ExecuteNonQuery();
    var dttm = DateTime.Now;
    var timeStr = dttm.ToString("yyyy/MM/dd HH:mm:ss");
    cmd.CommandText = "INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)";
    cmd.Parameters.Add("idx", OracleDbType.Decimal).Value = 1;
    cmd.Parameters.Add("dttm", OracleDbType.Date).Value = dttm;
    cmd.ExecuteNonQuery();
 
    cmd.CommandText = "SELECT * FROM JEFFTEST WHERE DTTM = :dttm";
    cmd.Parameters.RemoveAt(0);
    var dr = cmd.ExecuteReader();
if (dr.Read())
        Console.WriteLine(dr["DTTM"]);
else
        Console.WriteLine("No Data");
 
    Console.Read();
}

嗯,很好,所以這是 Oracle + Dapper 衍生出的新問題。弄一段程式碼驗證問題及解決方法,說明寫在註解裡,直接看 Code:

using (var cn = new OracleConnection(csOra))
{
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
    var timeStr = dttm.ToString("yyyy/MM/dd HH:mm:ss");
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)", new { idx, dttm });
//從資料庫取出剛才存入的日期備用
    var dttmInDb = cn.Query<DateTime>("SELECT DTTM FROM JEFFTEST WHERE IDX=1").Single();
//重新用DateTime.Now當WHERE條件比對,筆數為0
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm", new { dttm }).Count();
    Console.WriteLine($"使用原值WHERE比對 Count={cnt}");
//原因是Oracle DATE型別不包含毫秒
    Console.WriteLine($"DB {dttmInDb:HH:mm:ss.fff} vs C# {dttm:HH:mm:ss.fff}");
//把毫秒刪除試試
    var dttm2 = dttm.AddMilliseconds(-dttm.Millisecond);
    cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm2", new { dttm2 }).Count();
//登楞!筆數還是0
    Console.WriteLine($"AddMilliseconds修正後比對 Count={cnt}");
//換個方法,先轉字串再ParseExact轉回日期
    var dttm3 = DateTime.ParseExact(timeStr, "yyyy/MM/dd HH:mm:ss", null);
    cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm3", new { dttm3 }).Count();
    Console.WriteLine($"ToString+ParseExact轉換後比對 Count={cnt}");
//暗!這樣就找到得,為什麼?兩個時間看起來一樣,但CompareTo不同
    Console.WriteLine($"兩種修正結果看似相同 dttm2={dttm2:HH:mm:ss.fff} vs dttm3={dttm3:HH:mm:ss.fff}");
    Console.WriteLine($"dttm2.CompareTo(dttm3)={dttm2.CompareTo(dttm3)}");
//問題出在Ticks,二者Ticks不同
    Console.WriteLine($"關鍵在ms以下部分:dttm2={dttm2:HH:mm:ss.fffffff} dttm3={dttm3:HH:mm:ss.fffffff}");
//REF: http://stackoverflow.com/a/153014/4335757
    dttm2 = dttm.Trim(TimeSpan.TicksPerSecond);
    cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm2", new { dttm2 }).Count();
    Console.WriteLine($"Trim()取到秒再比對 Count={cnt}");
    Console.Read();
}

執行結果如下:

使用原值WHERE比對 Count=0
DB 21:22:22.000 vs C# 21:22:22.356
AddMilliseconds修正後比對 Count=0
ToString+ParseExact轉換後比對 Count=1
兩種修正結果看似相同 dttm2=21:22:22.000 vs dttm3=21:22:22.000
dttm2.CompareTo(dttm3)=1
關鍵在ms以下部分:dttm2=21:22:22.0004700 dttm3=21:22:22.0000000
Trim()取到秒再比對 Count=1

簡單來說,關鍵在 DateTime.Now 秒之下還有 Milliseconds 跟 Ticks,存入 Oracle Date 時只存到秒,而 Dapper 預設使用 Oracle Timestamp 型別(精準到 0.000001 秒)對應 C# DateTime 型別,實際的 WHERE 條是 Date 與 Timestamp 相比。一開始修正問題,以為 AddMilliseconds(-dateVar.Millisecond) 就夠,忘記更底下還有 Tick,比對還是失敗,改用 ToString 再 ParseExact 反而成功。最後我在 stackoverflow 找到漂亮的 Trim(TimeSpan.TicksPerSecond)函式完整截去秒以下部分,才算搞定。

關於 Dapper 將 C# DateTime 對應成 Oracle Timestamp 的行為,延伸到另一個更嚴重的議題:DateTime 被轉成 Timestamp 與 Oracle Date 欄位做比較將無法藉由索引加速,導致速度慢上五倍!參考)換句話說,即使截去小數秒結果正確,也將因 Index Scan 效能低落。網路建議解法是改寫 SQL 語法,寫成「WHERE Col = CAST (:dttm AS DATE)」,但我想到一招:其實大可不必花功夫捨去小數秒,因為在 OracleCommand 範例中,DateTime 搭配 OracleDbType.Date 就沒有小數秒差異問題。在一篇點部落文章看到 SqlMapper.AddTypeMap(typeof(DateTime), DbType.Date) 做法,要求 Dapper 將 DateTime 對應成 OracleDbType.Date,就完全不用煩惱小數秒差異囉!這應該是最簡潔有效的做法,經測試驗證,成功!

using (var cn = new OracleConnection(csOra))
{
//修改Mapping設定
    SqlMapper.AddTypeMap(typeof(DateTime), System.Data.DbType.Date);
 
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)", new { idx, dttm });
//重新用DateTime.Now當WHERE條件比對
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm", new { dttm }).Count();
//筆數為1,測試無誤
    Console.WriteLine($"Count={cnt}");
    Console.Read();
}

【結論】

  • Dapper 搭配 Oracle 時,C# DateTime 預設會對應成 OracleDbType.Timestamp,當資料庫端欄位為 Date 型別,會因小數秒差造成 WHERE 比對不符,另外會因無法套用索引造成查詢效率不彰。
  • 在查詢語法中使月 CAST(:dateParam AS DATE) 轉型可以克服上述問題。
  • 若資料庫以 Date 為主未用到 Timestamp,透過 SqlMapper.AddTypeMap(typeof(DateTime), DbType.Date) 將 C# DateTime 改對應至 DbType.Date,可更巧妙避開問題。

【Coding4Fun】注音符號輸入字盤及國字轉注音解決方案

$
0
0

小木頭國文實力有點虛,看在常靠國文騙吃騙喝的老爸眼裡不免焦急。結果,皇帝不急急死太監,我跟著看課文、自己出測驗題,忙得不亦樂乎,但求力挽狂瀾…

但每次出題要輸入一串純注音符號總叫我抓狂,先前沒學到好方法在電腦快速輸入「純注音符號」?我只會用注音輸入法再選字,操作步驟略嫌繁瑣,加上平日用倉頡對注音輸入操作不熟,輸入速度跟繡花有得拼。

靈機一動,想到好久沒寫 WPF ,不如就寫個小工具練練功好了。拉個Gid,動態塞入注音符號按鈕,再用 KeyUp 攔截按鍵事件對應注音輸入法的相對按鍵,將輸入結果顯示到 TextBox.Text,按 Enter 將輸入結果複製到剪貼簿,再貼到 Word,一個滑鼠鍵盤兩用的注音符號輸入字盤就完成了,出題速度從此加快五倍呢!(新武器就位,小木頭發抖中… XD)

程式碼已放上 Github,有興趣的朋友請自行參考。

昨天在 Facebook 貼了影片,收集到好多網友熱心回饋的替代做法,一併整理如下:

  1. 使用「通用輸入法」自訂輸入字根對應注音符號(通用輸入法介紹)… 感謝 Ammon Lin 提供
  2. 使用純注音字型,示範影片… 感謝 Esmond Wang 提供
  3. 教育部中文轉拼音查詢  中文到注音轉換工具… 感謝 Jethro Yu 提供

好方法很多,但自已寫的永遠是最棒的,呵~

本日成語:敝帚自珍

自家的破掃帚,卻視如千金之寶。比喻極為珍惜自己的事物

VS2017 無法載入專案,出現 compiler could not be created 訊息

$
0
0

從 VS2017 RTM 起我就一律改用它開發專案,還算順利沒啥問題。今天則遇到一起小錯誤,某個從未用 VS2017 開啟過的專案,開啟時發生專案載入錯誤,出現以下訊息:

Project 'Blah' could not be opened because the Visual C# 2017 compiler could not be created. An item with the same key has already been added.

重試兩次狀況依舊,爬文求解。

找到不少相關討論,但案例集中在 ASP.NET 專案,推論與 IIS Express 有關,靠重設 IIS Express 設定可排除。但我的專案是 Windows Form 專案,情境明顯不同。在 stackoverflow 一則 ASP.NET 案例討論串我找到改用管理者模式開啟 Visual Studio 排除問題的經驗分享,雖然與其解決原理不符,還是抱著死馬當活馬醫心態一試,結果一改用管理者身分執行 Visual Studio 問題就消失了,之後改回一般身分執行,專案開啟編譯均正常。

無法重現問題無從深入調查,留個記錄供參。

OracleParameter 型別不符導致 ORA-03111 通訊中斷錯誤

$
0
0

記錄在 Oracle 遇到的古怪錯誤。

Oracle Server 版本 10.2.0.4 64bit,Client 端用 Managed ODP.NET 12.1.24160719(取自 NuGet),某段程式碼誤傳 Varchar2 OracleParameter 與 DATE 欄位進行比對,預期應出現型別不符錯誤,但得到錯誤訊息為 ORA-03111 在通訊通道上收到中斷訊號(Break received on communication channel):

該資料表有其他 DATE 欄位,將 WHERE 條件換成其他 DATE 欄位,也會觸發 ORA-03111。

另一個資料表也有類似 DATE 欄位,換資料表再測,錯誤訊息變回 ORA-01861 literal does not match format string,這才是字串日期型別不符應出現的訊息。

回到問題資料表,如果不比對 DATE 欄位,改成 to_date('20121221','YYYYMMDD') = :d,則訊息變成 ORA-01861。

將 Managed ODP.NET 換成 Unmanaged ODP.NET(2.112.1.0,取自 NuGet),得到的也是 ORA-01861:

結論:由以上推測這是一個 Managed ODP.NET 的 Bug,只在特定資料表誤用 VARCHAR2 OracleParameter 比對資料表 DATE 欄位時發生,出錯時傳回 ORA-03111 通訊中斷錯誤,而非預期的 ORA-01861 日期字串轉換錯誤。

前後測試了多個資料表,只有一個資料表能重現問題,判斷屬於「人品觸發式茶包」,嚴重等級不高。但由此經驗,未來若遇到 ORA-03111 錯誤,宜留意是否為其他錯誤造成。

由 Dapper 傳回 dynamic 物件取得欄位清單

$
0
0

不用預先宣告強型別,查詢資料表後直接傳回 dynamic 是 Dapper的強項,例如:var list = cn.Query("SELECT Col1,Col2 FROM T).ToList(); 將傳回 List<dynamic>,用 list.Fisrt().Col1 就能讀取欄位內容,簡潔又方便。

最近有個花式應用,想用通用函式接收 Dapper 查詢結果,自動列舉其中包含屬性(資料庫欄位)。一開始依循 System.Refelction 思維,想說用 GetType().GetProperties() 就可以搞定,不料踢到鐵板,Dapper 傳回的 dynamic 物件 .GetType() 結果為 null:

進一步追查,Dapper 傳回的 dynamic 骨子裡其實是個 DapperRow 型別,特別的是它實做了 IDictionary<string, object> 介面:

還記得既然要動態就動個痛快 - ExpandoObject介紹過 IDictionary<string, object> 的妙用,既然 DapperRow 是個 IDictionary<string, object> 一切好辦,轉型後用 .Keys 取回屬性清單,接下來就簡單了。

staticvoid Main(string[] args)
        {
string sql = @"
SELECT 1 AS ColNum, SYSDATE AS ColDate, 'HELLO' AS ColString FROM DUAL UNION
SELECT 2 AS ColNum, SYSDATE AS ColDate, 'WORLD' AS ColString FROM DUAL
";
using (var cn = new OracleConnection(cs))
            {
                var res = cn.Query(sql);
                StringBuilder sb = null;
foreach (dynamic rec in res)
                {
                    var d = rec as IDictionary<string, object>;
if (sb == null)
                    {
                        sb = new StringBuilder(string.Join("\t", d.Keys.ToArray()));
                        sb.AppendLine();
                    }
                    sb.AppendLine(string.Join("\t",
                        d.Keys.Select(n => 
                            d[n] == null ? string.Empty : d[n].ToString()).ToArray()));
                }
                Console.WriteLine(sb.ToString());
 
                Console.Read();
            }
        }
 

經實測可成功列舉欄位名稱及內容:

COLNUM  COLDATE COLSTRING
1       2017/3/22 下午 09:07:30 HELLO
2       2017/3/22 下午 09:07:30 WORLD

最後,回到 DapperRow.GetType() 傳回 null 的謎團上,照理來說,GetType() 源自 Object 物件,查過 DapperRow 的原始碼並沒有覆寫 GetType(),想不出傳回 null 的理由。於是把問題丟上 stackoverflow,很快就有高手出面解惑,原來 DapperRow 實做了 DynamicMetaObject,攔截所有 dynamic 形式的成員存取,只要遇到非欄位名稱就傳回 null,因此不只 GetType(),呼叫 AsEnumerable() 也會傳回 null,所有謎團都解開了,結案!

Viewing all 428 articles
Browse latest View live