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

RSA 非對稱金鑰加解密與數位簽章筆記

$
0
0

用 .NET 加解密已是老生常談,.NET 內建 MD5、SHA1、RSA、AES、DES... 等雜湊及加密演算法,寫來易如反掌,網路上的文章也很多。但沒有自己整理過一次,每回要用都要爬文找半天。有些基本功不能省就是不能省,所以,我的 RSA 私房筆記來了。

程式範例 1 包含:產生隨機 RSA 金鑰、匯出公私鑰、對一小段文字加密、產生數位簽章。第二階段則包含匯入私鑰、解密加密內容、驗證數位簽章,並試著偷改內容驗證簽章是否因此失效。

staticvoid RSAEncDec()
{
//建立RSA公私鑰
    var rsaEnc = new RSACryptoServiceProvider();
//Key長度384-16384, Win8.1+最小512
//預設1024,可new RSACryptoServiceProvider(2048)指定不同大小
    Console.WriteLine($"KeySize={rsaEnc.KeySize}");
 
//匯出公鑰(用於解密,檢驗簽章),XML格式
    var pubKey = rsaEnc.ToXmlString(false);
    Console.WriteLine($"PubKey={pubKey}");
//匯出公私鑰
    var rsaKeys = rsaEnc.ToXmlString(true);
    Console.WriteLine($"RSAKeys={rsaKeys}");
 
//加密小段文字(用公鑰)
    var rawText = ".NET Rocks!";
    var rawData = Encoding.UTF8.GetBytes(rawText);
//第二個參數指定是否使用OAEP提高安全性
    var encData = rsaEnc.Encrypt(rawData, true);
//產生數位簽章
    var stream = new MemoryStream(rawData);
    var signature = rsaEnc.SignData(stream, 
new SHA1CryptoServiceProvider());
 
//** 解密 ** 需要公私鑰
    var rsaDec = new RSACryptoServiceProvider();
//從XML還原公私鑰
    rsaDec.FromXmlString(rsaKeys);
//使用私鑰解密
    var decData = rsaDec.Decrypt(encData, true);
    var test = Encoding.UTF8.GetString(decData);
    Console.WriteLine($"解密結果: {test}");
 
//** 驗章 ** 只需公鑰
    var rsa4Sign = new RSACryptoServiceProvider();
    rsa4Sign.FromXmlString(pubKey);
//檢驗數位簽章
    var valid = rsa4Sign.VerifyData(decData, 
new SHA1CryptoServiceProvider(), signature);
    Console.WriteLine($"數位簽章: {(valid?"PASS":"FAILED")}");
//測試修改一個Byte讓簽章無效
    decData[0]++;
    valid = rsa4Sign.VerifyData(decData, 
new SHA1CryptoServiceProvider(), signature);
    Console.WriteLine($"篡改版數位簽章: {(valid ? "PASS" : "FAILED")}");
}

執行結果:

KeySize=1024
PubKey=<RSAKeyValue><Modulus>q+SlvTWJ...+4BW7j0=</Modulus>
<Exponent>AQAB</Exponent></RSAKeyValue>
RSAKeys=<RSAKeyValue><Modulus>q+SlvTWJ...+4BW7j0=</Modulus>
<Exponent>AQAB</Exponent><P>ztDGDTc...d3ST3ow==</P>
<Q>1MW/8rq...ZrA3+Kxgnw==</Q>
<DP>cE4mfh6WruasI...IKsn/UiQ==</DP>
<DQ>dY81OPWZH...qtGoZ0MXQ==</DQ>
<InverseQ>cPTkfCrpSy...XmOH5qiu982pw==</InverseQ>
<D>IOtWDmld...+V3VeU=</D></RSAKeyValue>
解密結果: .NET Rocks!
數位簽章: PASS
篡改版數位簽章: FAILED

RSA 加解密只適用小段資料內容,資料長度不能超過其金鑰長度減去 Header、Padding 長度,以 2048 位元 RSA 只能加密 256 - 11 = 245 Bytes(參考: RFC2313 The length of the data D shall not be more than k-11 octets, which is positive since the length k of the modulus is at least 12 octets.) 實務上加密大量內容還是得靠對稱式加密(例如: DES、3DES、AES),RSA 則用來加密對稱式加密的金鑰。

程式範例 2 展示使用 RSA + AES 聯手處理 488MB 的 zip 檔的加解密以及數位簽章:

staticvoid RsaEncDecFile()
{
//建立RSA公私鑰
    var rsaEnc = new RSACryptoServiceProvider(2048);
 
//匯出金鑰
    var pubKey = rsaEnc.ToXmlString(false);
    var rsaKeys = rsaEnc.ToXmlString(true);
 
//建立AES Managed時產生隨機Key及IV,不用另行指定
 
    var aes = new AesManaged();
    var encAesKeyIV = aes.Key.Concat(aes.IV).ToArray();
 
    var aesKeyEncrypted = rsaEnc.Encrypt(encAesKeyIV, true);
 
byte[] signature;
    Stopwatch sw = new Stopwatch();
    sw.Start();
//準備加密Stream
using (var encFile = 
new FileStream("D:\\Encrypted.bin", FileMode.Create))
    {
using (var outStream = new
            CryptoStream(
                encFile, aes.CreateEncryptor(), CryptoStreamMode.Write))
        {
//讀取約500MB檔案寫入加密Stream
using (var fs = new FileStream("D:\\Source.zip",
                FileMode.Open))
            {
//REF: Buffer Size 64K CPU clock 較少 
//https://goo.gl/UAuPyt
                var buff = newbyte[65536];
int bytesRead = 0;
while ((bytesRead = fs.Read(buff, 0, buff.Length)) > 0)
                {
                    outStream.Write(buff, 0, bytesRead);
                }
            }
 
        }
    }
    sw.Stop();
    Console.WriteLine($"加密耗時: {sw.ElapsedMilliseconds}ms");
byte[] srcHash = SHA1.Create().ComputeHash(
new FileStream("D:\\Source.zip", FileMode.Open));
    signature = rsaEnc.SignHash(srcHash, CryptoConfig.MapNameToOID("SHA1"));
 
    var rsaDec = new RSACryptoServiceProvider();
//從XML還原公私鑰
    rsaDec.FromXmlString(rsaKeys);
//解密出AES Key
    var aesKeyIV = rsaDec.Decrypt(aesKeyEncrypted, true);
    aes = new AesManaged()
    {
        KeySize = 256,
        Key = aesKeyIV.Take(32).ToArray(),
        IV = aesKeyIV.Skip(32).Take(16).ToArray(),
        BlockSize = 128
    };
    sw.Restart();
//準備解密Stream
using (var decFile = new FileStream("D:\\Decrypted.zip", FileMode.Create))
    {
using (var encFile = new FileStream("D:\\Encrypted.bin", FileMode.Open))
        {
using (var decStream = new CryptoStream(encFile,
                aes.CreateDecryptor(), CryptoStreamMode.Read))
            {
                var buff = newbyte[65536];
int bytesRead = 0;
while ((bytesRead = decStream.Read(buff, 0, buff.Length)) > 0)
                {
                    decFile.Write(buff, 0, bytesRead);
                }
            }
        }
    }
    sw.Stop();
    Console.WriteLine($"解密耗時: {sw.ElapsedMilliseconds}ms");
 
//印出解密檔案Hash與原始檔比對是否相同
byte[] decHash = SHA1.Create().ComputeHash(
new FileStream("D:\\Decrypted.zip", FileMode.Open));
    Console.WriteLine($"Source SHA1={BitConverter.ToString(srcHash)}");
    Console.WriteLine($"Decrypted SHA1={BitConverter.ToString(decHash)}");
 
//檢驗數位簽章
    var valid =
        rsaDec.VerifyHash(decHash, CryptoConfig.MapNameToOID("SHA1"), signature);
    Console.WriteLine($"數位簽章: {(valid ? "PASS" : "FAILED")}");
}

實測 AES 加密 488MB 檔案需 12.3 秒,解密需 13.5 秒,速度蠻快的。

加密耗時: 12251ms
解密耗時: 13522ms
Source SHA1=34-F4-75-C1-81-7E-F4-25-4F-97-28-43-0C-7A-9D-5F-10-BD-2F-11
Decrypted SHA1=34-F4-75-C1-81-7E-F4-25-4F-97-28-43-0C-7A-9D-5F-10-BD-2F-11
數位簽章: PASS

另外,實務上不建議讓金鑰以文字檔形式曝露在外,多半會用金鑰容器保存 RSA 金鑰,詳情可參考官方文件,以下是簡單筆記:

staticvoid RsaKeyContainer()
{
//在個人RSA容器區建立金鑰容器並存入RSA金鑰
//一個KeyContainerName對應一把金鑰
    var csp1 = new CspParameters();
    csp1.KeyContainerName = "RSALab";
    var rsa1 = new RSACryptoServiceProvider(csp1);
 
//如果要從外部匯入金鑰,先建立RSA再FromXmlString()
    var csp2 = new CspParameters()
    {
        KeyContainerName = "RSALab"
    };
    var rsa2 = new RSACryptoServiceProvider(csp2);
    rsa2.PersistKeyInCsp = true;
    rsa2.FromXmlString("...");
 
//若同名金鑰容器已存在,自動取回上次存入的金鑰
    var csp3 = new CspParameters()
    {
        KeyContainerName = "RSALab"
    };
    var rsa3 = new RSACryptoServiceProvider(csp3);
 
//要刪除金鑰,先取消PersistKeyInCsp再Clear()
//金鑰容器也會一併被刪除
    var csp4 = new CspParameters()
    {
        KeyContainerName = "RSALab"
    };
    var rsa4 = new RSACryptoServiceProvider(csp4);
    rsa4.PersistKeyInCsp = false;
    rsa4.Clear();
}

2017 台北馬

$
0
0

告別 2017 的最後一場馬拉松 - 2017 台北馬。

大家都知道我挑選馬拉松比賽的原則是「鍾情小而美,不愛大拜拜」,會跑台北馬自己也意外。一來是上回參加已是 3 年前,去年改了賽道加繞中正紀念堂、總統府、南門、西門,聽說變得頗不一樣,很有城市味值得一試;二來則是台北馬要抽籤,隨手登記抽中正取,原以為 7000 個名額沒啥好搶,卻驚聞不少人想跑卻抽不中,莫名覺得到嘴肥肉不能放,腦波一弱就... 睽違三年,台北馬我來了!

今年「跑馬天氣好運」延續到最後一場,週末的雨勢週日早晨開始趨緩,氣溫只有 11 度,跑馬怕熱不怕冷,氣溫低又不淋雨就是好天氣。

7000 人的比賽真的不同凡響,人好多啊啊啊啊~

台北馬擁有不獨家特色:捷運提早發車、每 10KM 一道晶片感應、通過晶片感應點即時簡訊通知... 等。不過最令我印象深刻的是嚴格的分區管制,各區用鐵柵欄分隔,檢查號碼布才放行。

內含 101 的起跑照,GET!

改良版路線果然不同凡響,把東門、中正紀念堂、南門、總統府、西門町、北門、台北車站都巡了一圈,比起過去快快把跑者趕進河濱更符合「台北馬」之名,只是對那些被交管擋下,一臉焦急又無奈的騎士與路人有些抱歉。

賽道旁有不少熱情加油團體,還有弦樂與二胡演奏,照片右上角是民生慢跑加油團兼私補站,熱騰騰的咖啡與薑茶彌補了官方水站無熱食的遺憾,感謝! 左下角整排騎著耀西的加油團好有趣,但回家看照片才覺得怪怪的,他們騎的不知是恐龍還是蜥蜴來著? XD

跑完約 16K,菁英選手領先群已在對岸剩下不到 10K,這差不多是 NBA 跟國中籃球校隊的差距吧? XD

年老體衰,連晨跑計劃都常被周公打亂,體能近況與年初跑渣打馬時不可同日而語,破 PB 什麼的就甭想了,前面 20K 大約維持 540 配速,後半馬漸漸掉到 6 分速以下,今天抓個 430 完賽就好。

全馬必備的 32K 照,暖身結束,比賽正式開始,但,拎杯已經乏了... 此時又感受到大拜拜跟小而美的差異。七百多人的小比賽,後期跑者間距拉大,偶爾會出現前不見古人後不見來者的孤寂畫面;而七千人的比賽,只要油門不小心踩輕了,身旁涮涮涮三台車就超過去,其中一位還是女生。如果七百人比賽平均整場會被五十個人刷卡,七千人比賽就要被五百人刷卡啊啊啊~ 後段跑慢了一路被狂刷卡,一開始還有警覺想著不行不行我要加速,刷到最後天冷背也刷麻了,索性拋下羞恥心,美眉、大嬸、老杯杯、小鮮肉們,你們要超就超要刷就刷吧...

離開河濱爬上麥帥二橋再急轉下橋,進入長長的地下道,來到最後 2K。地下道的抽風機組轟隆作響,不知是貼心為跑友通風提高強度,還是平時就如此只是開車經過不會察覺,風超大! 原以為空氣會很悶,其實不會。

走完地下道上坡,最後 1K 跑步帶殺聲趕了一段,穩穩達成 430 目標,4:27:37 再添一馬。此時雨勢轉大,幸運躲過,嘿!

賽後發的物資不少,可惜小 7 便當有點空虛。

附上完賽獎牌照,很有設計感,掛帶的顏色好看!

 

除蟲筆記 – Thread 執行時機與 Closure

$
0
0

同事的 .NET 程式抓到一隻有趣的 Bug。以範例程式重現如下:

staticvoid DoProcess(int idx)
{
while (StartFlag)
    {
        Thread.Sleep(1000);
        Console.WriteLine(
            $"{DateTime.Now:mm:ss} Thread {idx} is running.");
    }
}
 
staticvoid Main(string[] args)
{
 
for (int i = 1; i <= 4; i++)
    {
        var thd = new Thread(() =>
        {
            DoProcess(i);
        });
        thd.Start();
    }
    StartFlag = true; 
//三秒後關閉StartFlag
    Thread.Sleep(3000);
    StartFlag = false;
    Console.ReadLine();
}

程式跑迴圈啟動四個 Thread,各 Thread 以 while (StartFlag) { … } 持續執行,沒什麼事要做就每隔一秒 Console.WriteLine() 時間與序號充數。這段程式犯了一個錯,沒在 Thread.Start() 前把 StartFlag 設好,跑完迴圈才 StartFlag = true,導致 DoProcess 什麼都沒做就收工。但如果只是這樣,Bug 馬上會被掀出來,也不會有這篇筆記了。

有趣的現象是 4 條 Thread 中還是有一條 Thread 會跑,使人被「為什麼明明起了 4 條 Thread 卻只有一條執行?」所迷惑:

這個現象源自多執行緒平行執行的時機問題,Thread.Start() 後,主線程式碼會繼續跑下去,而另起 的 Thread 隨後啟動。故推敲實際狀況應為:跑迴圈啟動第一條 Thread,因 StartFlag 為 false 直接結束,啟動第二條 Thread、第三條 Thread 也直接結束,直到第四條 Thread.Start(),進入 DoProcess 之前,主執行緒結束迴圈繼續往下跑執行 StartFlag = true,接著第四條 Thread 才進入 DoProcess() 執行 while (StartFlag),此時 StartFlag 已是 true,因此只有最後一條 Thread 成功運作。

要修正問題,StartFlag = true 應移至 for 迴圈之前:

staticvoid Main(string[] args)
{
    StartFlag = true; //Thread開始前應設定好
 
for (int i = 1; i <= 4; i++)
    {
        var thd = new Thread(() =>
        {
            DoProcess(i);
        });
        thd.Start();
    }
 
//三秒後關閉StartFlag
    Thread.Sleep(3000);
    StartFlag = false;
    Console.ReadLine();
}

修改後,四條 Thread 都起來了,但有個問題,編號怎麼是 3 3 4 5,不是 1 2 3 4?

多執行幾次,會發現數字非固定值,有時會是 3 3 5 5。

這一樣與各 Thread DoProcess() 執行時機有關,for 的過程 i 值會歷經 1 2 3 4 5 五種狀態,端看 DoProcess(i) 執行的當下 i 是多少而定。

staticvoid Main(string[] args)
{
    StartFlag = true; //Thread開始前應設定好
 
for (int i = 1; i <= 4; i++)
    {
//另外宣告變數,形成Closure
        var idx = i;
        var thd = new Thread(() =>
        {
            DoProcess(idx);
        });
        thd.Start();
    }
 
//三秒後關閉StartFlag
    Thread.Sleep(3000);
    StartFlag = false;
    Console.ReadLine();
}

要解決這個問題,我們可另外宣告一個變數 idx,透過 Closure 技巧讓四次迴圈中的匿名方法 () => { DoProcess(idx); } 擁有專屬變數,與 i 的變動脫鉤。(延伸閱讀:Closure in C#

抓一隻 Bug 溫習兩種觀念,很划算,呵~

全文檢索筆記–Windows Search SQL經驗談

$
0
0

因應專案需要,先前研究過 Lucene.Net。Lucene.Net 功能強大效能佳,又提供極高客製彈性,但缺點是得自己處理從 PDF、Word/Excel/PowerPoint 檔提取文字、管理索引排程,瑣碎工作不少。最後,我選擇到超市買牛奶而不自己養牛,決定借用 Windows Search 功能實作網站內容全文檢索,建個目錄把檔案放進去(txt、html、pdf、docx、xlsx、pptx 都成),將其納入索引範圍,在 .NET 程式建個 OleDbConnection,就可以下 SELECT ... FROM SYSTEMINDEX WHERE ... 指令完成全文檢索,很簡單吧?

網路上前人的教學文不少,我也樂得乘涼(感謝),但還是多少踩了一些坑,整理筆記如下。

【參考資源】

【基本範例】

privatestaticvoid TestMSSearch()
{
using (var cn = new OleDbConnection(
@"Provider=Search.CollatorDSO;Extended Properties=""Application=Windows"""))
    {
        cn.Open();
        var cmd = cn.CreateCommand();
        cmd.CommandText = @"
SELECT 
System.ItemName,
System.ItemPathDisplay,
System.ItemDate,
System.Search.AutoSummary
FROM SystemIndex 
WHERE SCOPE ='file:C:/WWW/Files' 
AND CONTAINS('關鍵字', 1028)";
        var dr = cmd.ExecuteReader();
while (dr.Read())
        {
            Console.WriteLine($"{dr[0]}({dr[1]}) @{dr[2]:MM/dd HH:mm}");
            Console.WriteLine($"{dr[3]}");
        }
    }
    Console.Read();
}

【實作眉角】

  1. 當 SQL 語法出錯,會遇到 E_FAIL(0x80004005) 錯誤,它跟存取被拒的系統錯誤代碼很像,不要被混淆了。欄位名稱敲錯還有「資料行不存在。」之類的提示,更嚴重的語法錯誤,如括號引號不對稱、無效的符號指令等,只會出現「發生一或多個錯誤」這種模糊提示,請仔細挑出 SQL 語法的毛病,不要花時間往權限方向偵錯。
  2. Windows Search 雖然可用 SQL 語法查詢,但支援程度有限,不要天真以為各式 T-SQL 語法都能搬來用。建議參考以下文件:
    Querying the Index with Windows Search SQL Syntax (Windows)可支援的 SQL 語法
    SQL Features Unavailable in Microsoft Windows Search (Windows)不支援的 SQL 語法
  3. Windows 10 1703 版的 Search Service 有個 Bug,DataReader.Read() 讀完最後一筆會噴出 0x80004005(-2147467259) 錯誤,未修正前的 Workaround 是用 try … catch … 把 DataReader.Read() 包起來,攔截 0x80004005 錯誤碼視為已無資料。
  4. 比對文字有兩個選擇 CONTAINS() 跟 FREETEXT(),以查詢"黑暗"為例,CONTAINS() 要文件出現"黑暗"字彙才算吻合,FREETEXT() 比較寬鬆,有"黑"也有"暗"就算數。
  5. 有時內容明明有關鍵字卻查不到,可能與分詞有關,但 Windows Search Service 不比 Lucene.Net 能任意客製調整,得把它當成傻瓜相機,方便但有極限。
  6. 我在中文版 Windows 10 測試 COTAINS("中文") 成功,移到英文版 Windows 2008 R2 時,發現用 ASP.NET 查不到中文關鍵字,查英文才有結果,直接用 LINQPad 跑同樣程式碼卻正常。最後我的解決方法是在 CONTAINS() 加上 LCID參數,改成 CONTINS('中文', 1028) 避免語系設定造成影響。(註: 1028-Chinese, Taiwan, 1033-English, United States, 2052-Chinese, China)

ASP.NET 眼中的 ".\Blah.sqlite" 在哪裡?

$
0
0

同事報案,在ASP.NET 存取 SQLite 資料庫,路徑誤用 ".\Blah.sqlite" (一般多使用 Server.MapPath("~\App_Data\Blah.sqlite"),參考:App_Data的隱身特性 ),程式可以運作,以為 Blah.sqlite 檔案會出現在 bin 目錄但沒有,使用 Everything 搜尋也找不到蹤跡。

這裡有個迷思,依直覺 ASP.NET 伺服器端程式編譯成的 DLL 以及參照的程式庫 DLL 都是放在 bin 目錄,感覺 ASP.NET 的根目錄(或者說工作目錄,".\")應該也在 bin。但事實上,ASP.NET 網站編譯成的組件以 DLL 形式存在,需依附宿主程序(Hosting Process)才能執行,使用 Visual Studio 執行時預設為 C:\Program Files (x86)\IIS Express\iisexpress.exe,若掛在 IIS 則是 C:\Windows\System32\Inetsrv\w3wp.exe。

同事的案例是 VS 偵錯時發現問題,最後在 C:\Program Files (x86)\IIS Express 資料夾找到 Blah.sqlite;若是使用 IIS,理應寫入 C:\Windows\System32\Inetsrv,但 IIS 不像 IIS Express 用當時登入的 Windows 帳號執行,而是限極小的 AppPool 專屬帳號,幾乎都會權限被拒出錯,更容易發現問題。

最後調查本案的另一疑點:為何 Everything 沒有找到 Blah.sqlite?同事回想,是前些時候為了測試增加 exclude 條件導致系統檔案路徑被排除,陰錯陽差之下增添了懸疑性。

全案偵結。

補充小常識:DLL 需依附其他程序執行,故工作目錄預設以程序 EXE 所在位置為準。如果是 Console Application EXE,"." 目錄總會是 EXE 所在資料夾了吧? 倒也不一定,還是有例外。如求萬無一失,可以在程式中執行 Directory.SetCurrentDirectory(Path.GetDirectoryName(                   Assembly.GetExecutingAssembly().Location)); 以求保險。

【茶包射手日記】TypeScript 出現大量 is not assignable to 錯誤

$
0
0

同事報案,在沒動 TypeScript 的情況下,專案爆出大量 TypeScript 錯誤導無法編譯。 錯誤訊息滿是各式各樣的 A is not assignable to parameter of type B。


目擊證人指出,問題出現在 VS2017 安裝更新後,VS 更新成為最大嫌疑犯。深入調查後案情逆轉,發現 TypeScript 2.3 版本被移除,專案屬性設定 TypeScript 原指定 2.3 版,目前顯示為 2.3 (Unavailable)。

延伸閱讀:檢查 TypeScript 安裝版本

進一步檢查,問題開發機在新裝 2.5 時移除了 2.3,懷疑 Visual Studio 找不到 2.3 改用 2.5,因而造成問題。重裝 2.3,再將 TypeScript 編譯版本改回 2.3 即不再出錯。

但有個詭異狀況,在另一個擁有相同 TypeScript 的專案測試,改用 2.5 版 TypeScript,卻能編譯成功,證明既有 TypeScript 仍與 2.5 版相容。深入比對,發現問題專案有個 tsconfig.json,移除後用問題專案就能 2.5 編譯了。

爬文得到以下結論:

故事是 TypeScript 2.4提高了泛型檢查的嚴謹性(Improved checking for generics),
許多舊程式無法通過新標準必須修改,由於此一 Break Change 可能讓大量 TypeScript 一夕之間「就地違法」,哀鴻遍野,故 TypeScript 留了一條活路。有個 --noStrictGenericChecks
參數允許 TypeScript 2.4+ 沿用較寬鬆的舊標準,以確保原有程式在升級時不用全面修改。

猜測使用專案屬性設定 TypeScript 編譯參數時預設啟用 noStrictGenericChecks,一旦改用 tsconfig.json,noStrictGenericChecks 預設則關閉,因而導致問題。

不過,我試著在 tsconfig.json 加入  "compilerOptions": { "noStrictGenericChecks": true },卻無法克服使用 TS 2.5 編譯出錯的狀況。詳細原因與 tsconfig.json 運作原理目前對我都是謎,留待日後研究,目前的解決之道是升級 2.5 時避用 tsconfig.json 改以 csproj 屬性設定 TypeScript 編譯參數。

VS2017 還原 NuGet 失敗:The given key was not present in the dictionary.

$
0
0

由 Github 抓回開源專案研究,用 Visual Studio 2017 編譯出現錯誤,貌似還原 NuGet Package 出錯導致,錯誤訊息為 "The given key was not present in the dictionary":

Restoring NuGet packages...
To prevent NuGet from restoring packages during build, open the Visual Studio Options dialog, click on the Package Manager node and uncheck 'Allow NuGet to download missing packages during build.'
Error occurred while restoring NuGet packages: The given key was not present in the dictionary.
1>------ Build started: Project: mvp-api, Configuration: Debug Any CPU ------
1>C:\Program Files\dotnet\sdk\2.0.2\Sdks\Microsoft.NET.Sdk\build\Microsoft.PackageDependencyResolution.targets(323,5): error : Assets file 'C:\WorkRoom\blah\src\mvp-api\obj\project.assets.json' not found. Run a NuGet package restore to generate this file.
1>C:\Program Files\dotnet\sdk\2.0.2\Sdks\Microsoft.NET.Sdk\build\Microsoft.PackageDependencyResolution.targets(165,5): error : Assets file 'C:\WorkRoom\blah\src\mvp-api\obj\project.assets.json' not found. Run a NuGet package restore to generate this file.
1>Done building project "someapi.csproj" -- FAILED.
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

錯誤訊息看到 dotnet sdk project.assets.json 等字眼,推測與 .NET Standard有關。很好,又被迫要玩新東西了。爬文很快找到是 VS2017 15.4.3 之前版本的 Bug,我的 VS 2017 版本還停在 15.4.0,中標不意外。將 VS2017 升級到 15.5.2,錯誤隨風而逝~

解完問題有些感慨:隨著開源風氣日益蓬勃,這年頭想置身潮流之外是愈來愈難了。軟體開發總要站在巨人的肩膀上,免不了要參考 Github 專案、引用第三方元件,而開源社群的腳步何其快速,一不小心就被逼著更新開發工具、學習新技術。

熱血小新肝們開源社群向來不介意擁抱改變,甚至就是技術不斷翻新的動力來源。這年頭想不碰新東西只靠舊玩意兒度日,比起擁抱新工具新技術還需要強大的心理素質。依我看,內心深處得坐著慈禧太后才頂得住... Orz

【茶包射手日記】Win10 IIS 無法啟用 32 位元模式(HTTP 503)

$
0
0

在工作機 IIS 測試 ASP.NET 網站,得到「An attempt was made to load a program with an incorrect format /試圖載入格式錯誤的程式」,這是經典問題,一看訊息就知是 32/64 位元版本不對,好發於在 x64 Windows 使用 32 位元 Oracle Client 的情境。基本上只需在 IIS AppPool 進階設定啟用 32 位元模式即藥到病除。這回狀況不同,啟用 32 位元後網站徹底掛點,顯示 HTTP 503,事件檢視器可觀察到 AppPool 因連續出錯被關閉,錯誤訊息指出與無法載入 C:\Windows\System32\inetsrv\appnetcore.dll 有關。

爬文得知是 Win8.1 升級 Win10 的後遺症,asnetcore.dll 區分 x86 跟 x64 版,分別位於 "%windir%\system32\inetsrv\aspnetcore.dll" 及 "%windir%\syswow64\inetsrv\aspnetcore.dll",但在升級 Win10 過程 syswow64 下的 aspnetcore.dll 被意外移除,據說在 Windows 10 Insider Build 15002 已修復,這種小問題靠更新 OS 修復感覺很蠢,決定另尋他法。

找了幾台 Windows 10 也沒看到 \syswow64\inetsrv\aspnetcore.dll,自行補檔的計劃告吹。

查詢 IIS 模組設定還真的有個 AspNetCoreModule,試過將其從站台移除無效,下一步應該試試從根網站或 IIS 完全移除模組,但有點治跛腳要截肢的 fu,我也不愛。

討論串提到修復「Microsoft .NET Core 1.0.0 – VS 2015 Tooling Preview 2」可以解決問題,值得一試:

很不幸地,在我的電腦上,套件無法修復或解除安裝,移除項目後重裝也告無效。

最後,我採用 MVP Rick Strahl 建議的解法,修改 C:\Windows\System32\inetsrv\config\applicationHost.config,找到 <add name="AspNetCoreModule" image="%SystemRoot%\system32\inetsrv\aspnetcore.dll" preCondition="bitness64" /> 加上 preCondition="bitness64",指定只有 64 位元模式才載入 aspnetcore.dll,IISRESET 再試一次,IIS 網站總算正常了。


閱讀筆記:人工智慧簡史

$
0
0

讀到保哥推薦的好文章: 從人工智慧、機器學習到深度學習,你不容錯過的人工智慧簡史 - INSIDE 硬塞的網路趨勢觀察

這一年來,人工智慧、機器學習、深度學習等名詞無所不在,一直開啟慈禧太后模式視若無睹充耳不聞也不是辦法,身為連院子都沒踏進的門外漢,讀完這篇文章算是有了概念摸到電鈴,未來聽人豪洨提及這些名詞不再一臉茫然。

簡單摘要如下:

  • 「人工智慧」( Artificial Intelligence, AI )分為強人工智慧(會思考有意識,像科幻片裡會決定幹掉人類的那種)以及弱人工智慧(只模擬人類思維方式,無意識)
  • 1950 年代,第一台通用電腦 EDVAC 問市十年,AI 概念首度在研討會上被提出引發熱議,在小說電影創作界也蔚為風氣,大家樂觀地預期 20-30 年內就能發展出與人腦智能程度相同的 AI
  • 當年的 AI 受限電腦運算能力,侷限於用已知數學模型、演算法尋求答案,無法回答人類自己不知如何解答的問題
  • 1970 年代,一些知名 AI 研究計劃結果不如預期,熱潮退去,政府企業研究單位紛紛收手,AI 第一次泡沫化
  • 電腦硬體的運算能力依循摩爾定律(每兩年增強一倍)突飛猛進,比起 30 年前強大 100 萬倍,開始有機會實現以往不可能做到的事,例如: 機器學習
  • 「機器學習」涵蓋電腦科學、統計學、機率論、博弈論,指用機器(電腦)分析大量資料(大數據)從中找出規律的「學習」過程,由於以資料為本,機器學習也是「資料科學」( Data Science )的熱門技術之一
  • 機器學習理論有很多門派,例如:支援向量機(SVM,在垃圾信分類應用大放異彩)、決策樹( Decision Tree )、AdaBoost、隨機森林,其中有一派「類神經網路」( Neural Network )於 1980 年興起
  • 類神經網路的概念是用電腦模擬人腦的運算模型,1986 年學者 Hinton 提出反向傳播算法可降低類神經網路所需的複雜運算量,但存在神經網路一旦超過3層就失效的瓶頸,跟 SVM、隨機森林等簡單有效的做法相比,類神經網路形同廢柴,一度被學術界鄙夷
  • 儘管外界無人看好,Hinton 對神經網路不離不棄 30 年,在 2006 找到限制玻爾茲曼機(RBM)模型成功訓練多層神經網路,終於突破瓶頸。但因為神經網路的名聲之前臭掉了,Hinton 將多層神經網路( Deep Neural Network )重新命名為「深度學習」( Deep Learning ),把 SVM 等歸類為淺層學習( Shallow Learning )一吐怨氣
  • 2006 年代深度學習使用 CPU 進行運算,速度不理想,跑一個模型要 5 天,發現有問題改一下又要再等五天,等訓練好不知等到猴年馬月。深度學習真正吸引世人目光,是 2012 年的事。
  • ImageNet 圖像識別比賽自 2007 起年年在史丹佛舉辦,起吸引 Google、微軟、百度等大型企業角逐,歷年冠軍的錯誤率一直維持 28%-30% 難有突破,直到 2012 年 Hinton 帶領兩名學生推出具深度學習能力的 SuperVision 以 16.42% 錯誤率狂勝第二名的 26.22%,震驚世界
  • 2013年 Google 人才收購了 Hinton 與他的學生,各家企業開始爭相投入深度學習領域。2015 年ImageNet 比賽由微軟拿下冠軍,3.5% 的錯誤率甚至比人類的 5% 還低
  • 深度學習需要大量矩陣運算,SuperVision 2012 突破的關鍵在於採用「深度學習 + GPU」,原因在於 NVIDIA 於2006年推出全新運算架構CUDA,讓開發者有能力寫程式運用顯卡上的數百顆 GPU 進行運算,NVIDIA 自 GeForce 8 起全面支援 CUDA,GPU 運算能力被廣泛應用在深度學習、VR 及比特幣挖礦,股價一飛沖天。

2017 歲末雜記

$
0
0

2017 最後一天,聊點風花雪月生活瑣事當作句點。

馬口魚

去年底拿塑膠收納箱搞起生態池,種水草養螺不過癮,元旦跑去溪邊撈了小魚回家養。由於裝備業餘技巧拙劣,能抓到的盡是 1 到 1.5 公分不等的幼幼魚,回家爬文依其特徵判斷為馬口魚(之前曾在 FB 亮相)。不知不覺己整整養了一年,留下一尾「藍波」,身長約十公分。生態池僅能俯拍加上魚兒動作敏捷,只拍到模糊照片充數! 

孔雀魚

生態池養魚養出興趣,結果是屋內莫名冒出這種東西 - 2 尺超白背濾缸配 T5 單燈管,裡面種了水蕨、銅錢草、金魚藻、水蘊草、小榕,四尾黑殼蝦配上十來隻孔雀魚終日穿梭,清道夫中隊(椎實螺上百枚)四處巡邏,右側的幼兒園(外掛式繁殖箱)則有小孔雀魚數十隻。

夏末新手上路時不太順利,狀況很多,遇過入缸隔天離奇失蹤,早上花一個小時才在離缸兩公尺的餐桌下尋獲木乃伊(跳缸 Orz);也曾經新進十隻新魚不出週全數陣亡(柱狀病、立鱗),全缸只剩一尾獨撐的慘烈局面。隨著天氣轉涼才漸入佳境,成功享受將剛出生的小魚養到成魚的成就感,擺脫「居然連孔雀魚都養不活」的沮喪。

香水檸檬

去年香水檸檬砍掉重練後產量大爆發,一次結了近十顆。採收後維持定期施肥修剪(為此還研發了長柄摘心專用剪),卻大半年沒消沒息,抽枝發芽不旺,但偶有稀疏小白花。直到入秋果實一顆顆在樹上現身,才驚喜今年收成並不差,還出現兩對雙胞胎、一組四胞胎,加上先前變黃搶收過一批,年產量共計 15 顆,再創新高。

偽 ‧ 馬蓋先

採收香水檸檬時園藝剪不慎掉在樓下遮雨棚上,隔著鐵窗,手伸再長仍有一公尺之遙。園藝剪已老舊鏽蝕,正常人都會直接放棄再買一把,但,茶包射手豈能輕易投降?

翻出伸縮桿、廢棄網路線(速度只到 10M 的古董貨)、原子筆管(居然是 TechDays 的 XD)、五根束線帶,就地取材廢物利用(馬蓋先音樂請下:登登登 登登登~ …),一枝黑暗版套環長桿就這麼橫空出世!

利器登場,只試了一次就成功,出奇好用! 害我開始猶豫要不要去申請專利。(謎: 你夠了哦)

 

祝大家 2018 新年快樂!

【茶包射手日記】用 USB 安裝 Win10 找不到媒體驅動程式

$
0
0

2018 開春第一包。

元旦在家當工具人,幫小閃光的 Toshiba 小筆電重灌 OS。Windows 10 1709 的 ISO 檔超過 4.8 GB 燒不進 DVD... (登楞) 被逼著第一次體驗用 USB 行動碟裝機。製作開機 USB 有個超好用的工具 Rufus,閉著眼睛亂點都能搞定,出奇順利。

一帆風順之際,忽然卡在以下畫面:

安裝程式抱怨找不到所需的媒體驅動程式:

電腦所需的媒體驅動程式遺失,這可能是 DVD、USB 或硬碟驅動程式,如果您有包含驅動程式 CD、DVD 或 USB 快閃磁碟機,請立即插入。注意:如果 Windows 安裝媒體位於 DVD 光碟機或 USB 磁碟機中,在此步驟可放心地將它移除。
A media driver your computer needs is missing. This could be a DVD, USB or Hard Disk driver. If you have a CD, DVD or USB flash drive with the driver on it Please insert it now. Note: if the installation media is in the windows DVD drive or on a USB drive, you can safely remove it from this step.

當下的直覺:該不會筆電用了特殊晶片抓不到硬碟? 依據老人家 Windows 2000 時代殘留的記憶,要在 RAID 裝 Windows,安裝步驟開始需要另外插入驅動程式「磁片」才能繼續,我把古今場景串聯在一起,跑去 Toshiba 官方網站尋找磁碟控制晶片驅動程式,發現家用級筆電根本不需要這種東西... Orz

最後,爬文找到一個神奇解法:(Windows 7 時代的經驗分享,但有人回應 Windows 10 也管用)

  • 如果是 DVD/CD 安裝,改用較低速燒片再試 (MS KB 也有類似建議)
  • 如果是 USB 行動碟安裝,取消載入驅動程式,回到安裝或修復畫面時,將行動碟換插其他 USB 孔再按「立即安裝」

遵循古法,在文章開始的畫面按【取消】放棄載入驅動程式,再按右上的紅 X 確定結束:

取消後回到安裝開始畫面,這時將 USB 行動碟換個孔插再按【立即安裝】:

薑! 薑! 薑! 薑~~ 就醬,我進入神祕傳送點,繼續工具人大冒險,呵!

閱讀筆記:Intel CPU 漏洞問題

$
0
0

整理一下這兩天的超大條資訊新聞 – Intel 這十年來製造的 CPU 都存在一個漏洞,可能讓攻擊者偷走存在記憶體裡的機密資料,不管你是用 Windows、Mac 還是 Linux 都會中獎,當今之計是靠更新作業系統補救,但要付出電腦變慢 5% – 30% 的代價。

Google Project Zero 團隊發現當今 CPU 採用的「推測執行」(speculative execution)技術有個漏洞,在最糟糕的情況下攻擊者可以任意讀取虛擬記憶體,這問題在 Intel、AMD、ARM 都可能存在[1],只是受波及的型號不同[5]。去年 6 月發現後已通報 CPU 及作業系統廠商,但最近媒體及社群已開始報導及猜測,Google 決定提前公開。Google 歸納出三種變形漏洞,全球安全研究人員已協力完成六種攻擊概念展示(PoC),已驗證攻擊者可以在 Linux 以一般使用者身分讀取 Intel Haswell Xeon 及 AMD PRO 核心虛擬記憶體;Meltdown 展示攻擊者可以打破應用程式彼此間、程式與作業系統間的隔離界面,拿到不該被讀取的資料;Spectre 展示如何打破不同應用程式介面(A 程式去讀 B 程式的專屬資料),實現難度高但更難防範。[5]

Intel 承認這十年來製造的 CPU 都有存在此一安全漏洞,Intel 主張 AMD、ARM 也可能會遭受類似攻擊,但 AMD 否認,主張其晶片設計與 Intel 不同遇到類似問題的機率為零。[2]另外公開事件的研究人員也認為 AMD CPU 不受影響。[3]

在大型數據中心(雲端運用)此一漏洞顯得格外嚴重,原因是攻擊者可使用 A 帳號登入後看到客戶 B 帳號放在記憶體內的資料(個資、交易內容、密碼...) [3],比起入侵單一主機更容易接觸到原本摸不到的資料,但能獲取什麼資訊要看攻擊當下記憶體內容而定,未必如想像中容易。

由於問題出在 CPU 硬體,全面召回換新是不可能的,故現今的解決之道是修改作業系統防堵(加入「內核隔離」功能),Linux 已逐步修正,Windows 也已從去年 11 月起進行修補,而社群就是在 Linux 原始碼發現可疑調整消息才洩漏(基於安全,Linux 程式碼相關修改日誌已經打上馬賽克了) [3],至於 Mac 的處理進度尚不清楚,但肯定不修不行。[4]  從作業系統層次加上防護的代價是電腦的執行效能將下降 5% - 30% 不等。

目前微軟已釋出 Windows 更新(Windows 10 將自動更新,Windows 7/8 需手動下載或等下週二更新),大部分 Azure 服務也已經完成修補,有些服務則需要重啟 VM 才能生效。[6]

心得:
1) 未來幾個月網管會很忙(要更新的機器數量應該很驚人)
2) 所有使用 Intel CPU 的人要在電腦變慢跟資料被偷之間二選一

【參考資料】

  1. Google:CPU漏洞影響不只英特爾,還有AMD與ARM – iThome
  2. Intel認了CPU有安全漏洞 指AMD、ARM也有問題 - 財經 - 自由時報電子報
  3. Intel 這次的漏洞不得了,微軟、蘋果都得改寫系統才能修 – TechOrange
  4. 出大事了 英特爾CPU漏洞修復將削弱Mac性能
  5. CPU「推測執行」漏洞已有6種概念性驗證攻擊出爐 – iThome
  6. CPU漏洞:微軟釋出Windows、Azure安全更新 - iThome

野人獻曝 - 極簡風格 .NET Stopwatch 計時法

$
0
0

在 .NET 要測量執行時間,Stopwatch 是最簡單直覺的做法,像這樣:

Stopwatch sw = new Stopwatch();
sw.Start();
//...執行要測試的動作
sw.Stop();
//將測得秒數輸出到Console、Debug或Log檔
Console.WriteLine($"Time={sw.ElapsedMilliseconds:n0}ms");

說起來不複雜,但一但測量對象變多,專案將充斥大量 Stopwatch 建立、開始、結束以及記錄時間的程式碼。遇到大範圍要計時,內部也要分段計時的需求,還得宣告多個 Stopwatch 物件並注意命名不能重複。再則,若想透過 config 統一啟用或停用所有計時功能,在每一段計時程式都要多加 if,很醜。

基於上述考量,我會寫成共用函式或程式庫集中程式碼,但用起來還是有點囉嗦,直到最近我想到一個好點子,將計時函式寫成實做 IDisposable 的專用物件,建構時建立 Stopwatch 並 Start() 開始計時,在 Dispose() 時 Stop() 並輸出計時結果:

/// <inheritdoc />
/// <summary>
/// 執行時間測量範圍(自動使用Stopwatch計時並寫Log)
/// </summary>
publicclass TimeMeasureScope : IDisposable
    {
privatereadonly Stopwatch stopwatch = new Stopwatch();
privatereadonlystring _title;
 
publicstaticbool Disabled = false;
 
/// <summary>
/// 建構式
/// </summary>
/// <param name="title">範圍標題</param>
public TimeMeasureScope(string title)
        {
if (Disabled) return;
            _title = title;
            stopwatch.Start();
        }
 
/// <inheritdoc />
publicvoid Dispose()
        {
if (Disabled) return;
            stopwatch.Stop();
//TODO: 實務上可將效能數據寫入Log檔
            Console.WriteLine(
                $"{_title}|{stopwatch.ElapsedMilliseconds:n0}ms");
        }
    }

如此,使用 using 關鍵字可控制 TimeMeasureScope 生命週期以及計時起點與終點,從 using 開始大括號「{」開始計時,遇到結束大括號「}」截止並輸出計時結果。這樣子,只需將要測量的程式碼片段用 using (var scope = new TimeMeasureScope()) 包起來就能自動計時,而且支援巢狀套用,例如以下這個無聊範例:

唯一的副作用是 using {} 會改變範圍內宣告變數的有效範圍,無法供外部叫用,部分變數可能需調整宣告位置,但問題不大。

用起來很簡單吧,實際在專案用過感覺不錯,野人現曝一下~

Request.Url.Host 偽造實驗

$
0
0

我有個 IIS 網站同時繫結多個 IP,想做到依據連上的伺服器 IP 授與不同權限,例如: 有些功能開放外網 IP 連入使用,某些功能限定內網及 localhost IP 才能用。設立兩個站台繫結不同 IP 及 Port 但共用同一份 ASP.NET 程式碼是一種解法,但我貪圖共用 Process 及靜態物件的便利性,因此要研究正確識別 Request 伺服器來源 IP 的方法。

舉最簡單的例子,IIS 預設繫結到所有 IP 位址("*"),而若伺服器 IP 為 172.28.1.1,則使用者用 httq://127.0.0.1、httq://localhost、httq://172.28.1.1 連上的都是同一站台。

如果不花腦筋,Request.Url.Host 會依瀏覽器輸入的 URL 分別傳回 127.0.0.1、localhost、172.28.1.1,似乎可用。但仔細想想不對! Host 資訊由 Request 內容決定,可能會被偽造,萬萬不可做為資安或權限管控依據。

查了文件,IIS 提供的 ServerVariables 變數裡有個 LOCAL_ADDR:

Returns the server address on which the request came in. This is important on computers where there can be multiple IP addresses bound to the computer, and you want to find out which address the request used.

LOCAL_ADDR 位址由 IIS 決定,是較可靠的來源。

我寫了以下程式實測:

publicclass HomeController : Controller
    {
// GET: Home
public ActionResult Index()
        {
return Content($@"
Url.Host={Request.Url.Host},
LOCAL_ADDR={Request.ServerVariables["LOCAL_ADDR"]}");
        }
    }

使用瀏覽器測試,Url.Host 與 LOCAL_ADDR 都會依網址傳回不同結果,足以區別使用者連上的伺服器 IP:

不過,瀏覽器測試 OK 不代表不能做壞事,花一點點功夫寫幾行程式(註: 用 curl 工具甚至連程式都不必寫),靠著偽造 Host Header 輕鬆騙過 Rquest.Url.Host,想指定為任何字串都成:

經以上實驗證明,Request.Url.Host 不可信,不建議用於安全管控,ServerVariables LOCAL_ADDR、SERVER_PORT 是較好的選擇。

TIPS - C# 讀取 Oracle dbms_output.put_line 輸出資訊

$
0
0

使用 dbms_output.put_line() 列印執行資訊是常用的 Oracle Stored Procedure 偵錯技巧,以下 Procedure 範例在DELETE 及 INSERT 後透過 dbms_output.put_line() 印出影響資料筆數,概念跟在程式碼裡塞入一堆 Debug.Print、MsgBox、alert() 差不多,是執行期間追查問題的重要線索:

createor replace procedure JeffDBJobTest1 is
begin
deletefrom JEFFTEST where idx = 32;
  dbms_output.put_line(sql%rowcount || ' rows deleted');
insertinto JEFFTEST values (32, sysdate);
  dbms_output.put_line(sql%rowcount || ' rows inserted');
end;

使用 PL/SQL Developer 或 Toad 等 Oracle 資料庫工具執行 Procedure,軟體介面有地方可以檢視 dbms_output 的輸出訊息,除錯抓蟲時很有用。

這個技巧開發測試階段大家用得很順手,如果程式已經上線在正式環境,是否也有機會蒐集到這些珍貴偵錯情資呢?跟同事討論到這個問題,起初大家都覺得無解,認真爬文找到線索,經過一番摸索及踩坑,還真的可行。

整理重點如下:

  1. dbms_output.put_line() 所寫入的內容會被放在緩衝區( Buffer )中( 緩衝區容量預設 20,000 Bytes ),可透過 dbms_output.get_line() 或 .get_lines() 讀取,若光寫不讀會把緩衝區塞爆出錯。
  2. 緩衝區以 Session 為單位,依實務的角度,就是你必須在執行 Procedure 的 OracleConnection 執行 dbms_output.get_line() 才讀得到東西。像 Dapper 允許不必開啟連線就執行 .Execute()/.Query() (背後自動開啟、關閉),就可能因 Procedure 執行與 dbms_output 讀取使用不同連線( Session )而讀不到資料。
  3. dbms_output 預設為停用,記得要先呼叫 dbms_output.enable() ( 就是上圖有個 Enable Chceckbox 開關的意義 ),不然會做白工。
  4. dbms_output.get_line(line, status)有兩個輸出參數,每次讀取一列字串,line 為字串內容,status 傳回 0 表示還有下一筆,傳回 1 代表緩衝區已空;dbms_output.get_lines(lines, numlines)則一次取回字串陣列( CHARARR 型別 )及資料筆數。

講完原理來實際演練,我用 Dapper + ODP.NET 示範,用 get_line() 加 while 迴圈讀取,get_lines() 得取回字串陣列型別比較囉嗦,以後再試:

staticvoid Main(string[] args)
        {
using (var cn = new OracleConnection(cs))
            {
//**重要** 先開啟連線,確保後續執行在同一個Session
                cn.Open();
 
//**重要** 記得要啟用dbms_output
                cn.Execute("dbms_output.enable", 
                    commandType: CommandType.StoredProcedure);
 
//呼叫Stored Procedure
                cn.Execute("JeffDbJobTest1", 
                    commandType: CommandType.StoredProcedure);
 
//準備參數接收
                DynamicParameters p = new DynamicParameters();
                p.Add("line", dbType: DbType.String, 
                    direction: ParameterDirection.Output, size: 4000);
                p.Add("status", dbType: DbType.Int32, 
                    direction: ParameterDirection.Output);
 
int status;
do
                {
                    cn.Execute("dbms_output.get_line", p, 
                        commandType: CommandType.StoredProcedure);
                    Console.WriteLine(p.Get<string>("line"));
                    status = p.Get<int>("status");
                } while (status == 0);
 
            }
        }

測試成功!


C# 讀取 dbms_output 效能強化版

$
0
0

前文介紹過使用 C# 讀取 dbms_output 寫入內容,範例留了一個小尾巴,跑迴圈連資料庫犯了效能大忌,應改成一次執行或查詢取回才上道。

dbms_output.get_lines()允許一次取得多筆訊息,但傳回型別為 TYPE DBMSOUTPUT_LINESARRAY IS VARRAY(2147483647) OF VARCHAR2(32767); 讀取要費點手腳,Oracle 生手經過一番研究,試出四種不同做法,就當練功吧。

  1. 使用 ODP.NET OracleParameter 接收
    OracleParameter 有個 CollectionType 屬性,將屬性型別設為 OracleDbType.Varchar2,Size 為陣列大小,再設定 CollectionType  = OracleCollectionType.PLSQLAssociativeArray,ArrayBindSize 傳入 int[] 指定每筆訊息字串最大長度(12c 上限 32767,更早版本為 4000),可從 lines 參數取回字串陣列。
    這個做法的缺點是寫法複雜且高度依賴 ODP.NET(不易改寫成 Dapper 版),然後每次遇到要預估接收空間都讓我焦慮,太大怕浪費、太小取不完要分多次,好阿雜。
  2. 跑 PL/SQL 組裝成單一字串傳回
    寫一小段 PL/SQL 程式,呼叫 get_lines() 再跑迴圈將其串成單一字串傳回。優點是只需單一字串參數接收結果,缺點是可能卡到 4000 或 32767 的字串大小上限,感覺爆掉的機會不低,不甚實用。
  3. 自訂函式將結果轉成 Table 後以 SELECT 查詢讀取
    在 Stackoverflow 看到這招妙計
    create or replace function get_dbms_output
    return dbmsoutput_linesarray
    as
        l_output dbmsoutput_linesarray;
        l_linecount number;
    begin
        dbms_output.get_lines(l_output, l_linecount);
     
    if l_output.count > l_linecount then
            -- Remove the final empty line above l_linecount
            l_output.trim;
        end if;

    透過神奇的 get_dbms_output 自訂函數,跑 SELECT column_value FROM TABLE(get_dbms_output) 純查詢就能撈回 dbms_output 全部內容,算是最漂亮簡潔的解法,小缺點是需要資料庫部署自訂函數。
  4. 使用 Ref Cursor 讀取
    受 get_dbms_output 函數的啟發,我想到不用部署 Procedure 或自訂函數也能用 Ref Cursor 一次取回所有訊息的做法(Dapper 可支援 Ref Cursor),比 SELECT 法複雜,但不必動到資料庫就能用,算是次佳解。

附上四種做法的範例。

staticvoid Main(string[] args)
        {
using (var cn = new OracleConnection(cs))
            {
//**重要** 先開啟連線,確保後續執行在同一個Session
                cn.Open();
 
//**重要** 記得要啟用dbms_output
                cn.Execute("dbms_output.enable", 
                    commandType: CommandType.StoredProcedure);
 
//方法1,使用PLSQLAssociativeArray接回陣列
                cn.Execute("JeffDbJobTest1", 
                    commandType: CommandType.StoredProcedure);
 
                var cmd = cn.CreateCommand();
                cmd.CommandText = "dbms_output.get_lines";
                cmd.CommandType = CommandType.StoredProcedure;
                var pLines = cmd.Parameters.Add("lines", OracleDbType.Varchar2, 
                    ParameterDirection.Output);
                pLines.Size = 2000; //可容納的訊息字串筆數
                pLines.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
//指定每筆字串最大長度(Oracle 12可到32767)
                pLines.ArrayBindSize = Enumerable.Repeat(4000, pLines.Size).ToArray();
//numlines為雙向參數,執行前傳入lines可容納筆數,執行後傳回實際讀得筆數
                var pNumLines = cmd.Parameters.Add("numlines", OracleDbType.Int32, 
                    ParameterDirection.InputOutput);
                pNumLines.Value = pLines.Size; 
                cmd.ExecuteNonQuery();
 
                var rawLines = (OracleString[]) pLines.Value;
string[] lines =
//依numlines判斷資料筆數
                    rawLines.Take(((OracleDecimal) pNumLines.Value).ToInt32())
                        .Select(o => o.ToString()).ToArray();
                Console.WriteLine(string.Join("\n", lines));
//缺點: 程序較複雜,得先預估空間,若一次取不完還是得跑迴圈(但應很罕見)
 
//方法二,用PL/SQL指令組成字串一次傳回
//呼叫Stored Procedure
                cn.Execute("JeffDbJobTest1",
                    commandType: CommandType.StoredProcedure);
 
                var p = new DynamicParameters();
                p.Add("result", dbType: DbType.AnsiString, size: 32767, 
                    direction: ParameterDirection.Output);
                cn.Execute(@"
DECLARE 
    lines dbmsoutput_linesarray;
    numlines INTEGER;
    i INTEGER;
    msg VARCHAR2(32767);
BEGIN
    numlines := 32767;
    dbms_output.get_lines(lines, numlines);
    i := 1;
    WHILE i <= numlines 
      LOOP
        IF i = 1 THEN
            msg := lines(i);
        ELSE
            msg := msg || CHR(10) || lines(i);
        END IF;
        i := i + 1;
      END LOOP;
    :result := msg;
END;
", p);
                Console.WriteLine(p.Get<string>("result"));
//缺點: 取回字串有長度限制(12c 32767,更早版本只有4000)
 
//方法3,使用自訂函數轉成Table
                cn.Execute("JeffDbJobTest1",
                    commandType: CommandType.StoredProcedure);
                lines = cn.Query<string>(
"SELECT column_value from table(get_dbms_output)").ToArray();
foreach (var line in lines)
                    Console.WriteLine(line);
//缺點: 需在資料庫部署自訂函數
 
//方法4,用RefCursor
                cn.Execute("JeffDbJobTest1",
                    commandType: CommandType.StoredProcedure);
 
//使用 Dapper 接收 Oracle Ref Cursor 
//http://blog.darkthread.net/post-2017-04-17-dapper-ref-cursor.aspx
                var op = new OracleDynamicParameters();
                op.Add("res", dbType: OracleDbType.RefCursor, 
                    direction: ParameterDirection.Output);
                var m = cn.QueryMultiple(@"
DECLARE
    lines dbmsoutput_linesarray;
    numlines INTEGER;
BEGIN
    dbms_output.get_lines(lines, numlines);
    IF lines.COUNT > numlines THEN
        lines.TRIM;
    END IF;
    OPEN :res FOR SELECT column_value FROM TABLE(lines);
END;
", op);
                var data = m.Read();
                lines = data.Select(o => (string) o.COLUMN_VALUE).ToArray();
foreach (var line in lines)
                    Console.WriteLine(line);
 
                Console.Read();
            }
        }

2018 烏來峽谷馬拉松(Y拖初馬)

$
0
0

大概是去年把跑馬運氣用完了,今年的馬拉松行程在 16 度冷雨中揭開序幕 - 烏來峽谷馬。

氣象預報下雨機率 100% 心頭涼了半截,下雨天的山路馬對我根本是黑指甲保證班呀! 跑完第一件事就是檢查腳趾,然後開始「紅腫 -> 黑青 -> 指甲剝離 -> 脫落換新」的循環。

去年跑完扶輪馬後入手江湖傳說很好跑的母子鱷魚氣墊夾腳拖鞋,俗稱「Y 拖」,號稱黑指甲剋星,價格還相當實惠,一雙專業跑鞋的價錢可以買它 20 雙。入手兩個月開始試穿晨跑,逐步從 5K、10K 加長距離,最長已達 15K,計劃練到 25K 以上就穿著它跑全馬。

眼看烏峽馬要淋雨跑山路已成定局,但穿 Y 拖至今最多跑到 15K,拿來跑全馬天曉得後段會出現什麼狀況?天人交戰許多,最後心一橫決定衝了。出狀況頂多用走的,完賽七小時空間夠大。

大會的免費接駁服務挺貼心,有中正紀念堂、寶橋停車場、新店捷運站三個接駁點,班次時間、侯車地點及現場導引都很明確,之前其他賽事遇過趕到指定地點卻不確定是否找錯地方心慌慌,大會這次的接駁安排格外令我滿意。

四點二十搭上接駁車,大約五點抵達鳥來立體停車場。時間尚早但雨勢不小,跑友們全躲在停車場裡,室內萬頭鑽動。

有別一般賽事全馬比半馬早起跑的慣例,烏峽馬的半馬 6:00 起跑,全馬 6:20 才出發,很特別。半馬出發後人潮稍減我才去排隊上廁所,等待時偷看一眼補給品區,打量今天有什麼好吃的,瓦斯桶代表有熱食耶,嘿嘿嘿。(喂!)

起跑在即,大伙才心不甘樂情不願地走出戶外淋雨,此時雨勢轉小是好兆頭。望著腳上的 Y 拖,心中很是忐忑,當年跑初馬那種生死未卜感再上心頭。沒想到都跑了近五十場,還能重溫初馬的感受,哈!

全馬路線先下坡折返約 10K 回起點再往烏來峽谷前進。跑了 2K 身熱雨停便脫了薄風衣穿短袖上場,不料到 4K 雨勢轉大,只好路邊停車再把風衣穿回去。此時右腳背皮膚跟拖鞋磨擦傳來微痛,擔心再跑下去破皮出狀況,拿出塵封多年的祕密武器穿上 - 當年為初馬準備的五指襪。

平日穿 Y 拖練跑我沒穿過襪子,這是頭一遭,穿上襪子跑沒幾步,大驚!

穿襪子配 Y 拖的感覺好美妙呀!鞋子與腳的摩擦感消失,跑步時腳幾乎感受不到負擔,這就是傳說中的人鞋合一嗎?以前擔心襪子沾水會濕涼難受,但實測即使氣溫只有十幾度,運動期間腳部發熱並不會感覺濕冷,就算踩到水坑也因通風快乾。以往穿運動鞋總要一路留意避開水坑,千方百計保持鞋子乾燥,換成 Y 拖水坑都沒在怕的,勇敢踩過去濺得旁人一身水花就對了,哈哈哈。

之前跑 U-Lay 42看過藍天白雲版本的烏來峽谷,這回觀賞的則是雲霧繚繞版,別有一番風味。

一樣的是沿路有數不完的瀑布流水。

肥壯而有怒氣。

這場補給很一般,各水站大致相同,水、運動飲料、橘子、香蕉、蕃茄、巧克力球... 只有在一站吃到加菜(魯蛋與海帶),賽前看到的瓦斯桶只用在終點的薑湯與熱湯,水站沒有熱食,另外聽說有人在終點領到包子但我沒有(嗚...)。少了熱食有點美中不足,不過標準補給以外的好料都屬 Nice to Have,有吃到超開心,沒有也無妨,但是對大會的里程標示我倒頗有怨言。

賽前標榜賽道經 AIMS 丈量,但路上擺放的里程牌卻超級不準。由於途經隧道又在峽谷,Fenix 3 跟上回一樣發生 GPS 亂飄里程失準,逼得我得參考大會標示。但手錶已到 24K 時才看到 20K 牌子明顯有問題,而 35K 跟 40K 路牌與我手錶里程卻又只差 500 公尺左右,搞得我好亂呀。結果我誤判剩餘距離。原以為能穩穩跑進五小時,沒想到被牌子騙少估了近兩公里,最後 2K 有怎麼跑都跑不完的感覺,最終以 5:05:15 完賽。雖然就算里程標示正確也未必能 SUB5,但被里程牌耍了就是不甘心。

第一次跑完山路十趾完好如初(何況還下雨積水),沒有加領黑指甲當完賽獎牌。謝謝你,Y 拖!

大會在終點發放防寒鋁箔,是國內賽事罕見的福利。剛跑完不覺得冷我沒拿,從終點散步 2.2 公里回起點順便排乳酸,路上愈走愈冷才覺得後悔,哈。

會場有面姓名牆,印著所有參賽者的姓名,也是有趣的特色。

就以這張烏來瀑布照為本次比賽畫上完美句點吧。

附上完賽獎牌,不愧是設計大師的作品,美!

 

ASP.NET MVC 回傳 HTTP 400 Bad Request 並附加錯誤訊息

$
0
0

同事的專案遇到以下需求:依規格實作 WebAPI (考量開發彈性,使用 ASP.NET MVC Controller,未走 ApiController ),規格定義遇到某些狀況需抛回 HTTP 400 Bad Rquest 並以 JSON 格式回傳錯誤訊息。

一開始的寫法如下:

public ActionResult BadRequestFail()
        {
            Response.SetStatus(HttpStatusCode.BadRequest);
return Content(
"{ \"error\": \"朕不給的,你不能拿!\" }", "application/json");
        }

實測不成功。Response.SetStatus(HttpStatusCode.BadRequest) 雖然有傳回 HTTP 400,但 Body 無內容,return 的 Content() 消失無蹤。

經爬文與實驗後,獲得以下心得:

  1. 使用 Response.SetStatus(HttpStatusCode.BadRequest) 會中止 Reponse,導致 return Content() 被無視。由 System.Web.WebPages.ResponseExtensions 原始碼可證實此點:
    publicstaticvoid SetStatus(this HttpResponseBase response, int httpStatusCode)
    {
        response.StatusCode = httpStatusCode;
        response.End();
    }
  2. Response.StatusCode 屬性支援寫入,不用 SetStatus() 改成直接指定 StatusCode = 400 可避免 Response.End()。
  3. IIS 遇到 StatusCode 400 時預設是顯示自訂錯誤頁面,也會忽略 return Content() 內容。如要強制回傳結果,需加上 Response.TrySkipIisCustomErrors = true。

綜合上述結論,修改程式如下:

public ActionResult BadRequest()
        {
//用SetStatus()會有副作用,阻止傳回Content
            Response.StatusCode = 400;
//設定TrySkipIisCustomErrors,停用IIS自訂錯誤頁面
            Response.TrySkipIisCustomErrors = true;
return Content(
"{ \"error\": \"朕不給的,你不能拿!\" }", "application/json");
        }

HTTP 400 Bad Request 並傳回 error 訊息,測試成功!

另外,systen.webServer 有個 <httpErrors existingResponse="PassThrough" /> 設定也會影響上述行為。預設值為 Auto,由 Response.TrySkipIisCustomErrors 屬性決定是否使用 IIS 自訂錯誤頁面;若 existingResponse="Replace" 將永遠使用 IIS 錯誤頁面,設為 PassThrough 則永遠使用程式輸出結果。Stackoverflow 上有一則詳細解說,值得參考。

IIS HTML 檔 Cache 行為觀察

$
0
0

跟同事討論到:「IIS 在靜態檔案更新時會強制瀏覽器讀取新版本嗎?」

HTTP Header 有不少與 Cache 管理有關,協助瀏覽器用 Cache 減少網路傳輸量,例如:Cache-Control、If-Modified-Since、ETag… 等。要了解這些技術細節,推薦幾篇文章:

IIS 預設藉由 ETag 及 If-Modified-Since 讓靜態內容(HTML、JPG、PNG、GIF、CSS、JS...)平時可以被 Cache,但是檔案只要有更新就重新讀取。知道理論但沒親身觀察過,索性做個實驗證明一下。

實驗使用 Chrome 瀏覽器,關啟 F12 開發者工具 Network 頁籤觀察 HTTP Request 及 Response,並調整設定:取消 Disabled Cache、開啟 Preserve Log。前後讀取 Index.html 三次。開始前先清空 Cache,因此第一次 Chrome 只能由 IIS 取回完整內容,第二次 IIS 回傳 304 通知 Chrome 使用 Cache。接著修改 Index.html 加入一個空白字元並存檔,第三次 Chrome 便會自動讀到新版。

來觀察一下這背後是如何實現「檔案沒變讀 Cache,檔案有改重新抓」?

第一次 IIS 回傳 HTTP 200,HTML 內容共 5,880 Bytes,在 Resonse Headers 有兩個 Cache 相關設定,ETag: 36b43fcfa127d31:0 是依檔案內容產生的雜湊值,Last-Modified 的時間就是 Index.html 檔案的最後修改時間(跟檔案總管查到的時間一致)。

重新整理網頁,第二次 Chrome 再送出 Request 請求時,Request Headers 多了 If-Modified-Since 及 If-None-Match,If-Modified-Since 帶入的是上圖 Last-Modified 傳回的時間,If-None-Match 帶入的則是上圖 ETag 的內容。此時 IIS 比對檔案時間及 ETag,發現檔案沒異動,傳回 HTTP 304 Not Modified,告知檔案沒變,請瀏覽器安心使用 Cache 裡的版本。不用傳回 Index.html 內容,於是省下 5,880 Bytes 的傳輸量。

接著我們修改 Index.html,在 HTML 加入一個空白字元並存檔。再次重新整理網頁,第三次 Request Headers 仍帶有相同 If-Modified-Since 及 If-None-Match,但由於 Index.html 內容有變,IIS 不再傳回 304,而是 HTTP 200 傳回完整內容(5,881 Bytes,比先前多了一個字元),而 Response Headers 中的 ETag 變成 318893c7958ed31:0 與先前不同,而 Last-Modified 也變成修改存檔時間。

由以上實驗我們可以觀察到 IIS 如何協助瀏覽器實現「平時用 Cache,有修改就重抓」,而這是所有專業網站伺服器都有的基本功能。

不過你可能發現一件事:雖然有用到 Cache,瀏覽器每次還是都要發出 Request 跟 IIS 確認檔案是否更新?可以直接用 Cache 還是重新下載?如此只能節省傳輸頻寬,並沒有減少發出 Request 的次數。

有一種更積極的 Cache 策略是 IIS 傳回結果時在 Response Header 夾帶 Cache-Control: max-age=n(多少秒),如此瀏覽器在一段時間內都不會對 IIS 發出 Request 求證檔案是否更新,直接使用 Cache 內容,可以節省更多頻寬及 CPU 資源。但副作用是萬一檔案改版,使用者可能要等內容到期後才會讀取新版內容,不然要靠在 URL 加上 ?ver=n.n.n 之類的版號參數迫使瀏覽器讀取新版。

以 IIS 10 為例,由「HTTP 回應標頭/設定一般標頭」介面可指定內容到期時間,啟用 Cache-Control: max-age 方式應用 Cache:

除了使用管理介面,Cache 政策也可透過 web.confg 設定(參考:Client Cache -clientCache- - Microsoft Docs)。

以上就是關於 IIS 靜態內容 Cache 行為的一點心得,提供大家參考。

【茶包射手日記】Safari 回上頁時無法停用 Cache

$
0
0

使用者報案,專案網站使用 Safari 檢視,在切換頁面時殘留載入中訊息,但使用 Chrome/IE 則一切正常。

專案網站有個主目錄網頁,點選切換其他功能網頁前會 $.blockUI 顯示"網頁載入中,請稍侯..."訊息,由於頁面很快會被新網頁取代,故沒必要關閉載入中訊息。而網頁有加
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
確保網頁不被 Cache,故使用者回到主目錄一定重新載入,不該看到載入中訊息。

程式示意如下:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<metahttp-equiv="cache-control"content="no-cache">
<metahttp-equiv="expires"content="0">
</head>
<body>
<divclass="buttons">
<ahref="/FuncA">功能A</a>
<ahref="/FuncB">功能B</a>
</div>
<scriptsrc="https://code.jquery.com/jquery-git.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.js">
</script>
<script>
    $(".buttons a").click(function() {
      $.blockUI({ message: "網頁載入中,請稍侯..." });
    });
</script>
</body>
</html>

接獲報案後實測,Chrome 不管桌機或手機都沒問題,但 Safari 從功能A或功能B網頁以 history.back() 或 history.go(-1) 回到主目錄網頁確實會看到載入中訊息高掛,咦,主目錄不是已宣告網頁永不 Cache 嗎?花惹發?

爬文得到答案,這是所謂的 Back-Forward Cache (BF Cache) ,以上一頁下一頁巡覽時,瀏覽器會使用記憶體內的 Cache,記憶體內的 Cache 完整保留頁面元素渲染結果、JavaScript 變數,維持當初離開網頁的狀態。可想而知,BF Cache 可優化操作體驗,不但網頁切換迅速,還可節省不必要的網路或磁碟讀取,保留狀態也符合大部分情境的使用者預期 - 回上頁後繼續剛才未完成的操作。

然而,各家瀏覽器實作 BF Cache 的方式各有不同(這篇文章 浏览器前进-后退缓存(BF Cache) - Harttle Land有 Chrome/Safari/Firefox 桌機與手機版的差異比較),在某些情況下瀏覽器將停用 BF Cache,以 Firefox 為例

  • 網頁註冊 unload 或 beforeunload 事件
  • 網頁宣告 Cache-Control: no-cache、Expires: 0 (<meta> 或 HTTP Header)
  • 網頁尚未載入完全
  • 涉及 indexedDB Transaction
  • 網頁中 IFrame 內嵌有不允許 Cache 的網頁

由實測結果,Safari 顯然與其他瀏覽器的行為不同,遇到 Cache-Control: no-cache 或 Expires: 0 也不會停用 BF Cache。在網頁加掛 unload 事件(function() {}空函式即可) 是一種解法,我參考 Stackoverflow 的解答,決定攔截 pageshow 事件,檢查 event.persisted 屬性偵測網頁是否來自 BF Cache,若是則呼叫 $.unblockUI() 關閉載入中訊息。(以本案例,在 pageshow 不分青紅皂白 $.unblockUI() 也能解決問題,但演練實習 persisted 的應用也好)

window.onpageshow = function(event) {
if (event.persisted) {
        $.unblockUI();
    }
};

就醬,問題排除,而老狗又學到了新把戲~

Viewing all 428 articles
Browse latest View live