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

CODE-使用 Stack + yield 取代遞迴

$
0
0

分享最近學到的遞迴邏輯的替代寫法。

舉個實例比較容易說明,假設公司組織樹狀結構以部門資料物件形式呈現:

publicclass Dept
    {
publicstring Name;
public List<Dept> Children = new List<Dept>();
    }

組織架構範例如下:

{
"Name": "總經理",
"Children": [
    {
"Name": "行政部",
"Children": [
        { "Name": "人資組" },
        { "Name": "總務組" }
      ]
    },
    {
"Name": "資訊部",
"Children": [
        { "Name": "網路組" },
        { "Name": "研發組" }
      ]
 
    },
    {
"Name": "業務部",
"Children": [
        {
"Name": "海外組",
"Children": [
            { "Name": "海外一科" },
            { "Name": "海外二科" }
          ]
        },
        {
"Name": "通路組",
"Children": [
            { "Name": "行銷科" },
            { "Name": "電銷科" }
          ] 
        }
      ] 
    }
  ] 
}

若要列舉所有部門名稱,過去我慣用遞迴(Recursive,在函式中呼叫自己)來解,身為程式老鳥,不爬文不查書徒手寫遞迴是基本功,難不倒我:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
 
namespace LinqRecursive
{
class Program
    {
staticvoid Main(string[] args)
        {
            var root = JsonConvert.DeserializeObject<Dept>(File.ReadAllText("Org.json"));
            var deptNames = new List<string>();
            ExploreOrg(root, deptNames);
            Console.WriteLine(string.Join(",", deptNames.ToArray()));
            Console.Read();
        }
 
staticvoid ExploreOrg(Dept dept, List<string> list)
        {
            list.Add(dept.Name);
            dept.Children.ForEach(o => ExploreOrg(o, list));
        }
    }
 
publicclass Dept
    {
publicstring Name;
public List<Dept> Children = new List<Dept>();
    }
 
}

執行成功:

最近再遇到相同案例,突發奇想有沒有更巧妙的做法? 爬文查得 LINQ 美技一枚– 為 IEnumerable<T> 新增擴充方法 Traverse(),參數為可取得子物件 IEnumerable 集合的 Lambda 運算式,即可產出樹狀結構下所有節點的 IEnumerable<T> 集合繼續串接 LINQ 方法,十分巧妙:

publicstaticclass LinqExt
    {
//REF: https://stackoverflow.com/a/20975582/288936
publicstatic IEnumerable<T> Traverse<T>(this IEnumerable<T> items,
            Func<T, IEnumerable<T>> childSelector)
        {
            var stack = new Stack<T>(items);
while (stack.Any())
            {
                var next = stack.Pop();
yieldreturn next;
foreach (var child in childSelector(next))
                    stack.Push(child);
            }
        }

有了萬用 Traverse 擴充方法,不必花時間另寫遞迴函式,程式更簡潔,而傳回型別為 IEnumerable<T>,能與其他 LINQ 操作無縫接軌,極為方便。

staticvoid Main(string[] args)
        {
            var root = JsonConvert.DeserializeObject<Dept>(File.ReadAllText("Org.json"));
            Console.WriteLine(string.Join(",", 
new List<Dept>() { root }
                .Traverse<Dept>(o => o.Children)
                .Select(o => o.Name).ToArray()));
            Console.Read();
        }

回頭看 Traverse 方法,利用 Stack<T> 資料結構,一方面取得子元素集合推入 Stack<T>,另一方面則從 Stack<T> 取出元素查詢其子元素,堆與取之間把所有元素都巡過一輪。還有一個巧妙處在於 yield,可以無腦地在迴圈裡遇到條件符合就 return 結果,交由 .NET 在背後蒐集結果彙整成 IEnumerable<T>。若不用 yield 也可以,替代做法是準備一個 List<T> 蒐集結果最後再傳回,像這個樣子:

publicstatic IEnumerable<T> Traverse<T>(this IEnumerable<T> items,
            Func<T, IEnumerable<T>> childSelector)
        {
            var stack = new Stack<T>(items);
            var results = new List<T>();
while (stack.Any())
            {
                var next = stack.Pop();
                results.Add(next);
foreach (var child in childSelector(next))
                    stack.Push(child);
            }
return results;
        }

由此可見 yield 簡化邏輯的效果,收入工具箱,未來遇類似情境多了新武器可用。對 yield 的原理有興趣深入了解的同學,推薦安德魯的系列文:


【茶包射手日記】Windows 10 每天十點多固定醒來

$
0
0

家裡 Windows 10 的使用率不高,平時長期處於睡眠狀態,但偶爾會發現無故醒來,我知道有部分 Windows 排程具有喚醒電腦能力,正常情況醒來做完事閒置一陣子會再回去睡覺,發現醒著多半是閒置休眠機制失靈,倒也沒特別調查。今天心血來潮挖了一下,發現一個祕密—原來我的 Windows 10 固定每天早上十點多都會起床夢遊,有趣的是,起床時間還不固定,甚至有最遲 10:57 才起來的記錄:

檢視事件詳細內容追到一個排程 NT TASK\Microsoft\Windows\rempl\shell

在「工作排程器」Microsoft、Windows、rempl 資料夾的確有個 shell 排程,被設定成「喚醒電腦以執行此工作」,而上次執行時間 10:57:30,距離電腦被喚醒時間不到 30 秒。罪證確鑿,豈容狡辯! (怒拍驚堂木) 來人吶...

至於醒來時間不是十點整,每天不固定亂跳是怎麼回事?是洋葱,是排程有延遲 0 到 1 小時的隨機設定。

確認喚醒行為後,下個疑問是「rempl/shell 排程」是什麼鬼?可以關掉嗎?

以下是我找到幾篇相關文章:

MS Answers 論壇文章 windows task wakes up computer
SuperUser 文章 如何停用排程的喚醒能力

研究心得如下:

  1. rempl/shell 是 Windows 10 1607 更新加入的每日固定檢查,跟 Windows Update 更新有關。每天把在睡覺電腦挖起來檢查更新很符合資安精神,有無矯枉過正倒見仁見智。 (由事件檢視器觀察 rempl/shell 喚醒電腦後多半只有幾分鐘的活動記錄,懷疑它有處理完就叫電腦回去睡的能力,但僅為猜測尚未實驗證實)
  2. 檢查目前有沒有排定的喚醒時程,可使用指令 powercfg /waketimers
    下圖兩次執行結果為啟用 rempl/shell「喚醒電腦以執行此工作」選項前後的差異
  3. 要停用排程的喚醒能力,可以從電源管理下手
  4. 要檢查哪些排程被設成可喚醒電腦,有個好用 PowerShell 指令 - Get-ScheduledTask | where {$_.settings.waketorun}

【茶包射手日記】預訂五十年後執行的排程

$
0
0

近來異常充實,專案火燒屁股,大小茶包報案照常受理,生活好不精彩。遇到一枚奇妙茶包,追了好一會兒,謎底卻令人莞爾,為枯躁生活平添一絲趣味,特記上一筆。

同事報案,表單系統在歸檔時有個錯誤重試機制,出錯時自動休眠 30 分鐘再試,另外,系統亦接受程式指定於特定時間(稱為喚醒時間)重試。

監看報表出現多筆詭異喚醒時間,排定在 2058、2075、2052、2081、2068、2057、2055... 等 40-70 年後的日期,預訂於遙不可及的未來執行。嘗試分析詭異喚醒時間與初次執行的關聯,找不出任何規律。

初步檢查歸檔程式,確實有一段邏輯在系統出錯時傳回 DateTime.Now.AddMinutes(120),明確指定兩小時後重試,喚醒時間沒理由亂跳。同事甚至一度懷疑是資料庫錯亂導致,但僅有單一欄位資料不對,其餘資料正常的機會微乎其微(機率應不會比 GUID 碰撞高)。

從 Log 檔找到歸檔程式傳回 2058-06-27 20:00:54 的鐵據(俗話說:Log 寫得好,除錯沒煩惱 :P ),直接排除資料庫涉案的嫌疑,辦案重心回到歸檔程式本身,仔細翻找,看到一段程式碼,忍不住笑了出來:

if (theDate != DateTime.Today)
    {
        Random rnd = new Random(Guid.NewGuid().GetHashCode());
//當日非營業日,休眠至次日並稍作延遲再嘗試重新歸檔
        wrc.Sleep(theDate.AddSeconds(rnd.Next()), 
"非營業日,將於次日嘗試重新歸檔:" + 
            theDate.ToString("yyyy/MM/dd"));
        Response.End();
    }

程式原意是遇非營業日延後一天再試並加上亂數延遲,避免大量排程擠在同一時間執行,但此處犯了兩個錯誤:第一是漏了 AddDays(1) 未將時間延後一天,第二是製造延遲誤用 Random.Next(),誤以為 Random.Next() 會比照 VBScript Rnd() 或 Math.random() 傳回介於 0 與 1 間的小數,但 .NET Random.Next() 傳回的是 0 – int.MaxValue(約20億) 間的隨機正整數,變成延遲 0 - 63 年不等,就是 2058、2081 等神奇未來年份加無規律的由來。程式寫好多年,因非營業日執行的機率極低,這支 Bug 才得以隱身至今。

回到程式碼,可改寫如下:

if (theDate != DateTime.Today)
    {
//建構式不需傳入種子,直接以當下時間隨機決定
        Random rnd = new Random();
//當日非營業日,休眠至次日並稍作延遲再嘗試重新歸檔
//使用 Random.Next(N) 產生大於等於0但小於N的隨機整數
//另外也可用 Random.Next(M, N) 產生大於等於M但小於N的隨機整數
        wrc.Sleep(theDate.AddDays(1).AddSeconds(rnd.Next(3600)), 
"非營業日,將於次日嘗試重新歸檔:" + 
            theDate.ToString("yyyy/MM/dd"));
        Response.End();
    }

就醬,結束一次被茶包逗樂的難得經驗。

CODE-從 JSON 提取文字內容

$
0
0

從檔案萃取文字部分建立索引是全文檢索的必要程序,先前介紹過為 PDF、Office 檔案產生文字索引的做法,實際開發則遇到為 JSON 建立文字索引的需求。借用上回遞迴文章的組織資料當實例,假設 JSON 格式如下:

{
"Name": "總經理",
"Children": [
    {
"Name": "行政部",
"Children": [
        { "Name": "人資組" },
        { "Name": "總務組" }
      ]
    },
    {
"Name": "資訊部",
"Children": [
        { "Name": "網路組" },
        { "Name": "研發組" }
      ]
 
    },
    {
"Name": "業務部",
"Children": [
        {
"Name": "海外組",
"Children": [
            { "Name": "海外一科" },
            { "Name": "海外二科" }
          ]
        },
        {
"Name": "通路組",
"Children": [
            { "Name": "行銷科" },
            { "Name": "電銷科" }
          ] 
        }
      ] 
    }
  ] 
}

其中 Name、Children 是屬性名稱而非內容本體,不應納入搜尋範圍,故建索引時只需提取「總經理、行政部、人資組、總務組...」等純部門名稱就好。理論上應該找得到現成的 IFilter 或程式庫,但感覺原理不難,與其花時間尋找,不如自已寫個「JSON 文字資料抽取函式」練練功也好。(對啦對啦,我就是手癢愛亂寫啦,來咬我呀)

最早的想法是採遞迴 (或是上回的 Stack<T> + yield 奇技) 巡遍每一個 Property 讀取成字串。遇到物件就探索所有屬性、遇到陣列就跑完每一個元素,層層遞迴抓出所有內容。試寫了一陣子,閃過一個好點子,何不把 JSON 轉成 XML 再讀取 InnerText?秒殺搞定!!

Json.NET 果然不負所望,內建的 JsonConvert.DeserializeXmlNode Method (String, String)方法可將 JSON 字串直接轉成 XmlNode,得來全不費功夫~

staticstring ExtractJsonText(string raw)
        {
            var node = JsonConvert.DeserializeXmlNode(raw, "Root");
return node.InnerText;
        }
 
staticvoid Main(string[] args)
        {
            var json = System.IO.File.ReadAllText("Org.json");
            Console.WriteLine(ExtractJsonText(json));
            Console.Read();
        }

簡單幾行程式得到結果如下:

總經理行政部人資組總務組資訊部網路組研發組業務部海外組海外一科海外二科通路組行銷科電銷科

但有個問題。文字是提取出來了,卻全黏在一起,不利於分詞器正確切詞。有個簡單的改善做法設法在各屬性值間插入空白,我想到改用 InnerXml 加 Regex 置換 XML 標籤的做法可以實現。另外還有個狀況是 DeserializeXmlNode() 會將整個 JSON 視為一整個 Object,必須以 { 開頭 } 結尾,遇到 JSON 以 [、] 開頭及結尾會出錯,但這也難不倒我,偵測到主體是陣列時,外面再套一層 { "Array": [ … ] } 即可搞定。

改良版本如下:

staticstring ExtractJsonText(string raw)
{
    var json = raw.TrimStart(' ', '\t', '\r', '\n');
if (json.StartsWith("["))
        json = $@"{{ ""Array"": {json} }}";
    var node = JsonConvert.DeserializeXmlNode(json, "Root");
return//將Element Tag換成空白,再去除連續空白
string.Join(" ",
            Regex.Replace(node.InnerXml, "<[^>]+>", " ")
                .Split(newchar[] {' '}, StringSplitOptions.RemoveEmptyEntries)
                .ToArray());
}

執行結果:

總經理 行政部 人資組 總務組 資訊部 網路組 研發組 業務部 海外組 海外一科 海外二科 通路組 行銷科 電銷科

完美! Json.NET 好威啊~

CODE-C# 程式讀取 Exchange 共用行事曆

$
0
0

工作遇到新需求:辦公室自動化服務希望讀取使用者行事曆,整合顯示於個人資訊頁。

EWS Managed API封裝了複雜又囉嗦的 Exchange Web Service SOAP 細節,改以 .NET 程式庫形式提供電子郵件、連絡人、行事曆、公用資料夾的存取管道,是 C# 開發 Exchange 相關程式的首選。(意外發現 EWS Managed API 從 2014 起轉為 Github 開源專案,有原始碼在手,搞不懂走不通都有救,用起來格外讓人放心,微軟真的愈來愈開放)

官方文件有篇詳細介紹文,示範如何使用 EWS Mananged API 查詢行事曆取得個人約會資訊。基本原理是利用 CalendarFolder.Bind() 連上個人行事曆,接著建立一個 CalendarView 指定查詢期間、取回筆數,CalendarView.PropertySet 則傳入要讀取的欄位(主旨、開始/結束時間… 等),接著呼叫 CalendarFolder.FindAppointments() 就取得 Appointment 的集合,很簡單,這部分可直接參考官方範例。好消息是 EWS Managed API 用 NuGet 就可以下載:

不過我的需求多了一些變化,系統會使用統一中間程式查詢不同使用者的行事曆,而我們不可能為此要使用者交出 AD 帳號密碼。因此,透過行事曆共用是較可行做法。

行事曆共用可使用 Outlook 操作, 找到自己的行事曆(一般會有兩份,要選名稱有個人郵件地址位於 Exchange Server 的那一份),右鍵選單開啟內容:

程式使用 EWS Managed API 會以特定 AD 帳號執行(測試時可用開發者自己的帳號,正式營運則會申請專用帳號),要開放行事曆供程式存取,使用者必須授與該帳號讀取權限,授權完成可使用 Outlook 檢查是否看到測試對象的約會做為驗證。但有一點要留意,除了授權讀取空閒/忙碌時間、主旨、地點外,其他區域有個「可看到資料夾」一定要勾選。未勾選時 Oulook 可查看共享行事曆,但 EWS Managed API 不行,我花了不少時間才發現這個眉角。

若要求省事,也可請共用行事曆的使用者直接選取「權限等級:檢閱者」;若使用者很介意約會細節外流,只想透露主旨、地點,甚至只打算開放/空閒忙碌資訊,只勾選自己想開放範圍也成,但記得一定要勾選「可看到資料夾」,不然程式沒戲唱:

程式範例如下。查詢他人行事曆時,有個關鍵是使用 new FolderId(WellKnownFloderName.Calendar, "對方的Email") 取得資料夾代號,若對方未授與「可看到資料夾」權限,程式會在 FindAppointments 時噴出找不到資料夾的錯誤。

privatestaticvoid QuerySharedCalender()
{
    var ewsUrl = "https://the-exchange-server/ews/Exchange.asmx";
    ExchangeService ews = new ExchangeService(ExchangeVersion.Exchange2007_SP1);
    ews.Credentials = new WebCredentials(userId, pwd, domainName);
    ews.Url = new Uri(ewsUrl);
    FolderId folderToAccess = 
new FolderId(WellKnownFolderName.Calendar, "someone@company.com");
//指定日期區間與資料筆數
    var view = new CalendarView(
new DateTime(2017, 12, 10),
new DateTime(2017, 12, 17), 
        1024);
    view.PropertySet = new PropertySet(
                AppointmentSchema.Subject, 
                AppointmentSchema.Start, 
                AppointmentSchema.End);
    FindItemsResults<Appointment> apps = ews.FindAppointments(folderToAccess, view);
foreach (var app in apps)
    {
        Console.WriteLine(
            $"{app.Start:MM-dd HH:mm} - {app.End:MM-dd HH:mm} {app.Subject}");
    }
}

假設行事曆如下:

測試成功!

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)
  7. System.Search.AutoSummary 欄位是檔案一開始的一段純文字內容,並非出現關鍵字的片段,如果規格要求預覽包含關鍵字的文字內容,必須另外擷取成純文字再找出關鍵字,一切得 DIY。

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]。去年 6 月發現後已通報 CPU 及作業系統廠商,但最近媒體及社群已開始報導及猜測,Google 決定提前公開。

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% 不等。

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

【參考資料】

  1. Google:CPU漏洞影響不只英特爾,還有AMD與ARM – iThome
  2. Intel認了CPU有安全漏洞 指AMD、ARM也有問題 - 財經 - 自由時報電子報
  3. Intel 這次的漏洞不得了,微軟、蘋果都得改寫系統才能修 – TechOrange
  4. 出大事了 英特爾CPU漏洞修復將削弱Mac性能

野人獻曝 - 極簡風格 .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);
 
            }
        }

測試成功!

Viewing all 2429 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>