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

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();
    }
};

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

【茶包射手日記】T470p 藍牙失效疑雲

$
0
0

小黑 T470p忽然連不上藍牙滑鼠,控制台顯示藍牙已關閉,而啟用停用藍牙的開闗也不見了。

裝置管理員裡的藍牙項目消失,「通用序列匯流排控制器」則冒出一個「未知的 USB 裝置」,研判電腦忽然不認得藍牙裝置搞的鬼。

嘗試更新「未知的 USB 裝置」驅動程式,系統顯示已是最新版本,重新開機亦不見改善。到官網重新下載藍牙驅動程式安裝無效,但我注意到一件事 - Intel 8265 Wireless Bluetooth Driver,T470p 藍牙使用的是 Intel 晶片。

改用 Lenovo 自家自動更新軟體發現另一條線索 - 下午系統有自動更新 Intel 8265 Wireless LAN Driver,其使用的晶片也是 8265,加上更新時間與滑鼠故障的時機相近,涉嫌重大!

這下可好,如果是藍牙與無線網路驅動打架,難不成要移除網卡更新退回舊版?此時想起一事,下午曾瞄到系統閃過更新 Intel Management Engine Firmware 的提示,當時在做事略過沒理會,而這個提示前陣子似乎就跳過,莫非二者有關。

檢查更新,果然有個 IME Firmware 11.8 可裝,安裝更新重開機,藍牙就回來了。

問題排除,繼續上工。

Hangfire 筆記1 – 使用 SQLite

$
0
0

Hangfire是一套支援在 ASP.NET MVC 站台跑背景作業或排程的好用程式庫,可以將作業丟到背景執行、延遲執行或排定時間定期啟動,並且網頁管理介面、支援失敗重試等功能。如使用資料庫作為任務儲存區,即使網站重啟工作也不會遺失,功能十分強大。Hangfire 開源且可免費用於商業用途,付費版 Hangfire Pro則多了批次作業中斷接續執行、批次作業流程設計、Redis 支援、效能監控計數器等進階功能。關於 Hangfire 的基本使用 MVP Bruce 有篇淺顯易懂的介紹文:KingKong Bruce記事- 使用Hangfire處理ASP.NET MVC-Web API長時間與排程工作,在此不多贅述。

Hangfire 的架構很靈活,儲存資料來源被拆成獨立介面,支援各式資料庫。SQL Server Storage 被包在官方專案裡,算是預設的儲存資料庫選擇,且支援 SQL Server 2008 以上的各版本 SQL (包含免費版 SQL Server Express LocalDB 以及 SQL Azure )。不過即便用 LocalDB 還是有 SQL Server Express 的安裝部署需求,想起最近在幾個小專案用得很開心的 SQLite,查了一下已有好心人寫好 Hangfire.SQLite 套件,嘿,來試試 Hangfire + SQLite 好了。

NuGet 上的 Hangfire 程式包很完整,會自動帶入所有必要程式元件,安裝挺無腦的。

由於打算用 SQLite 當作 Hangfire 資料庫,需要額外加裝 Hangfire.SQLite。

我選擇不碰 Global.asa.cs 而是在 App_Start 資料夾加入 OWIN Startup類別設定 Hangfire。(延伸閱讀:開發筆記-OWIN 是什麼?)

using Hangfire;
using Hangfire.Dashboard;
using Hangfire.SQLite;
using Microsoft.Owin;
using Owin;
using System.Web.Hosting;
 
[assembly: OwinStartup(typeof(MyApp.Startup))]
namespace MyApp
{
publicclass Startup
    {
privatestaticstring SqliteDbPath = 
            HostingEnvironment.MapPath("~/App_Data/Hangfire.sqlite");
 
publicvoid Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration
//記得連線字串結尾要補上「;」
//不然會被視為 config 檔連線設定名稱
                .UseSQLiteStorage($"Data Source={SqliteDbPath};");
 
            app.UseHangfireDashboard();
            app.UseHangfireServer();
        }
    }
}

有兩件注意事項:

  1. 連線字串結尾需加「;」,不然會被當成 web.config 的 ConnectionString 設定名稱。
  2. SQLite 資料檔如要放在 App_Data,記得要開寫入權限。

如果一切順利,輸入 /hangfire/ URL 會看到 Hangfire 儀表板,大功告成:

Hangfire 第一次執行時會自動在資料庫建好 Schema,用 DB Browser for SQLite 開啟 App_Data/Hangefire.sqlite,可以看到資料表都已建好。

題外話: 原本我慣用的 SQLite Manager Firefox 套件已不相容 Firefox 57+,看了 Github 討論,似與 Firefox 修改 API 規格有關,作者也無力回天。現在可改用另一套開源工具 - DB Browser for SQLite

儀表板有個小問題,介面的簡體中文看起來很不習慣,經調查它是依據 Thread CultureInfo 自動載入對應的語系資源檔,有個 bin\zh\Hangfire.Core.resources.dll 提供中文字串資源(不分繁簡,但內容為簡體)。若不想看簡體,有幾種解決辦法:

  1. 補上繁體中文版 Hangfire.Core.resouces.dll
    這是最正統的解法,但我沒找到現成版本
  2. 刪除 \bin\zh\Hangfire.Core.resources.dll
    沒有中文資源檔就會變回英文,缺點是每次重新編譯會再冒出來
  3. 修改 web.config 將 UI 語系調成英文
    <system.web>
    <globalizationuiCulture="en-US"/>
    </system.web>

    缺點是會影響整個網站的語系設定

我先修改 web.config 應急,改完儀表板即變回英文。但理論上補上繁體中文資源檔才是王道,感覺也不算太難,過陣子再當成 Coding4Fun 題目練習好了。

另外,Hangfire 很聰明,會自己靠 Relection 尋找專案有無啟用 NLog、Log4Net、Elmah... 等 Log 機制。如果有就自動輸出 Log,完全不需設定,這個巧妙設計讓我小小驚喜了一下,值得學起來。


Hangfire 筆記2 - 執行定期排程

$
0
0

想用 ASP.NET Hangfire 跑定期排程,有一個前題是「需確保網站永遠處於執行狀態」,先推薦幾篇相關文章:

摸索過程我發現更簡單的新做法,實測可行,整理設定步驟如下:

  1. IIS AppPool 進階設定
    啟動模式設 AlwaysRunning (註: 記得確認已安裝「應用程式初始化」)

  2. 在 IIS 管理員站台或應用程式的進階設定啟用「預先載入已啟用」(Preload Enabled)

    註: 如想在預先載入時呼叫特定網址可使用 web.config 設定。參考: Use IIS Application Initialization for keeping ASP.NET Apps alive - Rick Strahl's Web Log
    <system.webServer>
    <applicationInitializationremapManagedRequestsTo="Startup.htm"
    skipManagedModules="true">
    <addinitializationPage="ping.ashx"/>
    </applicationInitialization>
    </system.webServer>

  3. Hangfire 官方出了一個 Hangfire.AspNet 套件,可簡化 IIS 設定及自己實作 IRegisteredObject 跟 IProcessHostPreloadClient 介面的程序,依據 Github 上的說明,這個新做法未來將取代現有官網所建議的安裝步驟(This package aims to replace the documentation article Making ASP.NET application always running.) 相關文件會晚一點才釋出... (眼看兩年過去了文件還沒好,但身為開發者,我懂,呵)
    沒有文件無妨,直接參考 Github 上的範例專案,我琢磨調整完的 Startup 類別如下:
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading;
    using System.Web;
    using System.Web.Hosting;
    using Hangfire;
    using Hangfire.Logging;
    using Microsoft.Owin;
    using Owin;
    using Hangfire.SQLite;
     
    [assembly: OwinStartup(typeof(MyApp.Startup))]
    namespace MyApp
    {
     
    //REF: https://github.com/HangfireIO/Hangfire.AspNet
    publicclass Startup : IRegisteredObject
        {
    public Startup()
            {
                HostingEnvironment.RegisterObject(this);
            }
     
    privatestaticreadonlystring SqliteDbPath = 
                HostingEnvironment.MapPath("~/App_Data/Hangfire.sqlite");
     
    privatestatic BackgroundJobServer backJobServer = null;
     
    publicstatic IEnumerable<IDisposable> GetHangfireConfiguration()
            {
                GlobalConfiguration.Configuration
                    .UseSQLiteStorage($"Data Source={SqliteDbPath};");
     
                backJobServer =  new BackgroundJobServer(
    new BackgroundJobServerOptions
                    {
                        ServerName = 
                        $"JobServer-{Process.GetCurrentProcess().Id}"
                    });
    yieldreturn backJobServer;
            }
     
    publicvoid Configuration(IAppBuilder app)
            {
    //改用UseHangfireAspNet設定Hangfire服務
                app.UseHangfireAspNet(GetHangfireConfiguration);
                app.UseHangfireDashboard();
     
                ScheduledTasks.Setup();
            }
     
    //ApplicationPool結束時會呼叫
    publicvoid Stop(bool immediate)
            {
    //Thread.Sleep(TimeSpan.FromSeconds(30));
    //Github範例等待30秒,會影響AppPool停止及回收速度
    //這裡改為直接呼叫backJobServer.Dispose()
    if (backJobServer != null)
                {
                    backJobServer.Dispose();
                }
     
                HostingEnvironment.UnregisterObject(this);
            }
        }
    }

設定排程部分我寫成另一顆物件,範例如下。這段程式每次啟動網站都會執行,故 AddOrUpdate() 時要指定排程名稱,排程已存在就只更新不新增,才不會新增一堆重複排程。實務上如求彈性,也可採用資料庫或設定檔管理排程。

using Hangfire;
 
namespace MyApp
{
publicclass ScheduledTasks
    {
privatestatic NLog.ILogger logger = 
            NLog.LogManager.GetLogger("SchTasks");
 
publicstaticvoid Setup()
        {
//REF: https://en.wikipedia.org/wiki/Cron#CRON_expression
            RecurringJob.AddOrUpdate("PerMinute", () => DumpLog(), 
                Cron.Minutely);
        }
 
privatestaticint Counter = 0;
 
publicstaticvoid DumpLog()
        {
            logger.Debug(Counter++.ToString());
        }
    }
}

實測 Hangfire.SQLite 發現一個問題,原本一分鐘跑一次的排程莫名每一分鐘執行 20 次,經調查應為 Bug,Hangfire 預設會開 20 條 Worker Thread,時間一到每個 Worker 都跑了一次。將 Worker 數調為 5 就變成跑 5 次。這問題在 Github 上也被網友被提報為 Issue,作者建議先將 Worker 數設成 1 避開。

所幸,又到了見識 Open Source 奇蹟的時刻,既然是 Open Source,遇到 Bug 自己查自己修也是很合理滴。花了點時間查出原因試著修正,也送了 PR,希望這個問題在未來的版本會被修復。

另外,實測 Hangfire.SQLite 跑定時排程還有另一個問題,當設成每分鐘整點執行,啟動時間並非 100% 精準。例如以下每分鐘一次的排程,每分鐘執行時點卻在 01-15 秒區間移動,為什麼是 15 秒?推測與預設 SchedulePollingInterval = 15 秒有關。

試著改用 SQL Server 或 Memory Storage 則沒發現類似問題,我懷疑這與 SQLite 執行速度不夠快有關,在一篇國外文章也提到類似的觀察。總之,如果系統對執行時間精準度要求很高,使用 SQLiteStorage 前應審慎評估。

徹底移除 IIS Response Header 版本資訊

$
0
0

從 IIS Reponse Header 移除 Server、X-AspNet-Version、X-Power-By 等版本資訊,可降低因曝露資訊被鎖定攻擊的機率,被視為提高資安防護的手段(效果高低見仁見智,但有些資安掃瞄將此列為弱點,不做也得做)。這已算是個老話題,網路上有不少討論與參考文章:

綜觀常見的幾種做法,不管是用 IHttpModule 或 Global.asax.cs 在 PreSendRequestHeader() 將 Server Header 移除,都只對 ASP.NET WebForm 或 ASP.NET MVC 有效,攻擊者只要改下載 HTML/JS/CSS/JPG/PNG 等靜態檔案,甚至隨便想個不存在的 html,HTTP 404 Reponse 冒出 Server: Microsoft-IIS/10.0 當場破功,白忙半天。

這是因為靜態內容由 IIS 直接處理,不會經過我們設計的機制(延伸閱讀:system.web 與 system.webServer)。

有個笨方法,設定 <modules runAllManagedModulesForAllRequests="true"> 將所有靜態檔案也導入 ASP.NET Pipeline,雖然管用,但原本由 IIS 輕巧做掉的工作通通被導進為複雜情境設計的笨重程序,對效能很傷。

Server Header 是當中最棘手的項目,IIS Manager HTTP Response Headers 或 URL Rewrite Module 可以改寫或清空 Server Header,但無法移除,而 UrlScan 可以清除 Server Header 只支援到 IIS 7。

最後我找到一個不錯的解決方案 - StripHeaders。一個 C++ 開發的開源模組,使用 WIN32 API 在 IIS 核心執行,能涵蓋靜態內容,核心模組的 Overhead 低,加上原生程式執行效能遠比 .NET 程式快,較不用擔心效能問題。

IIS 原生模組的安裝程序蠻多,不過 StripHeaders 提供 MSI 安裝檔,大大簡化安裝步驟。目前最新版 iis_stripheaders_module_1.0.5.msi 於 2016-11-19 推出,支援 Server 2016。

安裝程式在背後做了一堆事:

  1. Installs stripheaders.dll
  2. Registers the Native-Code module with IIS using the appcmd.exe command
  3. Extends the IIS configuration schema to allow setting of headers to remove
  4. Adds default settings to the IIS configuration to remove the common "Server", "X-Powered-By" and "X-Aspnet-Version" response headers
  5. Adds a registry setting to remove the "Server: Microsoft-HTTPAPI/2.0" response header.

理論上重開機後就會生效,如果你不想重開機,可以使用 net stop http 重啟底層 HTTP 服務再手動啟動 IIS 及其他相依服務。不過我實測時停用 HTTP 失敗(處於停用中的狀態,一直關不掉),最後只能重開機。但我遇的狀況是重開完也沒生效,最後參考 Github 的安裝程式原始碼(Open Source 萬歲!),手動註冊 StripHeadersModule 才解決問題:

安裝妥當後,如下圖應該要在 IIS 模組清單看到 StripHeadersModule:

StripHeaders 預設會移除 Server、X-Powered-By、X-AspNet-Version 等 Response Header,不需修改 web.config 就會生效。如需移除額外 Header,則可在 web.config  system.webServer/stripHeaders 中設定。

以 css 實測,未啟用 StripHeaders 前:

啟用後,Server、X-Powered-By 消失,成功!

2018 渣打馬

$
0
0

跑過三屆渣打馬,每次都有精彩回憶。

2014 年第一次跑渣打馬,初嚐起跑與終點不同的玩法,勉強擠進 SUB5。

2016 年遇上 44 年難得一見的霸王級寒流,頂著 4 度低溫,整場跑了五次廁所,排隊尿尿排到我都崩潰了,所幸保住 430。

2017 年絕對是最難忘的一場,是我跑馬生涯的里程碑。不知吃錯什麼藥,拎杯居然跑進許多跑者心中的強者門檻 - SUB4,還一舉突破傳說中的「陳冠希障礙」,以 3:55:04 完賽。

不過,近期的體能與去年相去甚遠。記得去年賽前每天晨跑輕踩油門配速就上到 5:10/KM,最近則只在 5:30-5:45 的水準,破 SUB4 機會不大,但基於過去的輝煌記錄也不好太漏氣,決定不揹背包不帶相機專心跑。

氣象預報 17-21 度,但早上 6 點下雨機率 70%,9 點之後上升到 90%,帶了 Y 拖備用,到會場陰天無雨,賭雨勢不會太快降臨來,穿跑鞋上陣。

六點準時開跑,拍完起跑拱門收起手機認真跑。除了開始的兩公里人多跑不快,之後配速維持在 5:25 - 5:45。一路撐到 32K,耗時 3 小時 03 分,我陷入抉擇...

評量體力狀況,大小腿疲累度還好,靠意志力把自己逼上懸崖,在 57 分內跑完剩下十公里是有可能的;另一方面,依據經驗前面只是暖身小菜,32K 以後才是真正的比賽,屆時嘴上說可以身體卻很誠實,撞牆、體力崩盤、抽筋... 樣樣都會來。

我該衝一波還是該放手呢?此時,安西教練的話在我耳邊不斷迴響...

嗯,「好的,這次比賽就到這邊告一段落,謝謝大家觀賞,我們明年見...」

人生苦短,年近半百的老人還學人家拼什麼 SUB4 呢?於是,人雖然還在賽道,我心卻已打烊,肚子正餓,改為路跑跑走走,四處找水站吃早餐和拿出手機拍照。(32K 里程牌也是回程補拍的)

小蛙你怎麼旅行到這裡來了?

九點整落下一陣大雨,雨勢還不小,一度擔心最後 10K 會很狼狽,所幸只下了兩三分鐘雨就停了。

拍完這張照片沒多久,我的 Nokia Lumia 920 古董機當機,停在 10:18 的鎖定畫面怎麼按都沒反應,長按重置也無效,害我錯過了終點時鐘照,最後是 4:24:57 完賽。

領了獎牌毛巾又胡弄了一陣子,過了幾分鐘手機忽然自己重開,這才折回去終點補拍照片。

搭接駁車回總統府的路上下起大雨,說來老天爺還挺幫忙的,又一場順利的比賽。

補上獎牌照:

 

【笨問題】在 Windows 檔案總管切換音樂/相片資料夾之詳細資料檢視

$
0
0

在使用 Windows 檔案總管我常遇到以下狀況 – 遇到滿載 MP3 或 JPG 檔的資料夾,選擇「詳細資料」檢視時檔案總管會自動切成音樂或照片專用的檢視,像是這樣:

遇到音樂顯示標題、演出者、專輯名稱,遇到照片可以帶出拍攝日期、尺寸,對一般消費者來說是很貼心的設計,可依據媒體特性方便排序管理。但對想靠檔案日期及大小查問題的茶包射手來說,這些預設欄位沒半個有用。過去我常用的解決方法是在欄位標題按右鍵新增修改日期、大小欄位:

最近學到一招,其實可以要求 Windows 將這個資料夾切回成一般檔案的詳細檢視。方法是在資料夾按右鍵選內容,在「自訂」頁籤切換「最佳化此資料夾(T)」選項,改回「一般項目」:

這樣就能回到熟悉的標準詳細檢視介面囉~

使用 Razor 產生客製化 Email 內容

$
0
0

多年下來,寫程式發 Email 通知的需求做過 N 回,其中寄給客戶的通知為求美觀常需採用 HTML 格式,而客戶姓名、通知內容等要隨客戶動態改變,嚴格來說也是一種套表。過去我慣用一套自己發明的「特別註記+Replace」做法,例如:

var tmpl = "<span>[$Name$]</span> 您好,您的等侯順位為<span>[$SeqNo$]</span>";
var dict = new Dictionary<string, string>()
{
    ["Name"] = "Jeffrey",
    ["SeqNo"] = "007"
};
var res = System.Text.RegularExpressions.Regex.Replace(
    tmpl, @"\[\$(?<n>.+?)\$\]", m =>
    {
        var n = m.Groups["n"].Value;
return dict.ContainsKey(n) ? dict[n] : "<" + n + ">";
    });
Console.WriteLine(res);
Console.Read();

土砲做法雖然簡陋,不能 IF ELSE 也沒法跑迴圈,倒也淺顯易懂,就這麼一用十幾年 XD

最近專案又有類似需求打算重操舊業,轉念一想,一帖老方子從 VB6 寫到 C# 6 未免太不長進,該想想有沒有更好的方法。接著馬上有個念頭浮現腦海 - 寫了那麼多 ASP.NET MVC 見識過 CSHTML 的威力,既然信件內文也是 HTML,為什麼不用 Razor 來套版呢?

爬文很快找到順手的兵刃 – RazorEngine,一個允許在任何 .NET 專案(不必是 ASP.NET MVC)使用 Razor 的程式庫:

來個實例,假設我有個中獎通知函要動態改變中獎者姓名、頭銜、獎項內容與日期,若用 Razor 來寫會像這樣(MailTemplate.cshtml):

@model RazorMailTmpl.Models.MailData
 
<!DOCTYPEhtml>
 
<html>
<head>
<metacharset="utf-8"/>
<title>Razor Mail Template Demo</title>
<style>
        li { color: #0000ff; }
        .due { color: orangered; }
</style>
</head>
<body>
<p>親愛的 @Model.WinnerName @Model.Title,</p>
<p>感謝您參加部落格讀者2017年終摸彩,在此恭喜您獲得以下獎項:</p>
<ul>
    @foreach (var prize in Model.Prizes)
    {
<li>@prize</li>
    }
</ul>
<p>
請在 
<spanclass="due">@Model.DueDate.ToString("yyyy/MM/dd")</span>
前連絡謎之聲領取獎項。
</p>
<p>再次恭喜您幸運中獎!</p>
<p>Regards,<br/>黑暗執行緒部落格抽獎小組</p>
</body>
</html>

接著宣告一個 MailData 資料型別,這樣在編輯 cshtml 時才能享受強型別與 Inetllisense 提示:

using System;
 
namespace RazorMailTmpl.Models
{
publicclass MailData
    {
publicstring WinnerName { get; set; }
publicstring Title { get; set; }
publicstring[] Prizes { get; set; }
public DateTime DueDate { get; set; }
 
    }
}

要用 RazorEngine 套表很簡單,先建好 MailData 物件,使用 Engin.Razor.AddTemplate() 載入範本,引擎內建 Cache 機制,接著呼叫 Run 或 Compile 以 Cache Key 取出範本進行編譯運算,很快就能得到套表後的 HTML 字串結果:

using RazorEngine;
using RazorEngine.Templating;
using RazorMailTmpl.Models;
using System;
 
namespace RazorMailTmpl
{
class Program
    {
staticvoid Main(string[] args)
        {
            var mailData = new MailData()
            {
                WinnerName = "Jeffrey",
                DueDate = new DateTime(2018, 2, 14),
                Title = "老司機",
                Prizes = newstring[]
                {
"32G USB 行動碟一支",
"Visual Studio 2017 紀念貼紙一組",
"法拉帝(Ferretti) 660 豪華遊艇(20米)一艘"
                }
            };
//將Template存入Cache以利重複使用
            Engine.Razor.AddTemplate(
"MailBody", // Cache Key
                System.IO.File.ReadAllText("MailTemplate.cshtml"));
//傳入Cache Key、Model物件型別、Model物件取得套表結果
            var result = 
                Engine.Razor.RunCompile("MailBody", typeof(MailData), mailData);
 
//除了RunCompile,也可Compile一次,Run多次以提高效能
            Engine.Razor.Compile("MailBody", typeof(MailData));
            Engine.Razor.Run("MailBody", typeof(MailData), mailData);
 
            System.IO.File.WriteAllText("Result.html", result);
        }
    }
}

薑!薑!薑!薑~ 完成。

官方文件的說明挺詳細,相信有 MVC cshtml 經驗的同學很快就能上手,祝大家套表愉快。

TIPS–7-Zip 壓縮時略過特定目錄或檔案

$
0
0

這是我常遇到的困擾 - 壓縮打包 .NET 專案時,packages、bin、obj 等目錄下的 DLL 檔常讓檔案大小暴增數十倍,而這些檔案可透過 NuGet Restore 或重新編譯產生,基本上不需要保留。如果能在壓縮時略過這些資料夾,預期可省下可觀的體積。

平時我都用免費且開源的 7-Zip壓縮解壓縮(題外話,如果你己經「免費試用」試用期只有 40 天的 WinRAR 了十幾年,可考慮改用 7-Zip),認真查了,7-Zip GUI 在壓縮時雖然有個 Parameters 欄位:

官方說明指出但該選項只能填入 -m 壓縮參數,調整壓縮率、壓縮演算法等,不支援排除特檔案或資料夾,由 Superuser 討論也確認無法由 GUI 排除,所以要回歸使用命令列工具 7z.exe。範例如下:

X:\Works\MyProject>7z a MyProject.7z MyProject -mx7 -r -xr!bin -xr!obj -xr!packages

其中 mx7 指定壓縮率、r 代表包含子目錄、xr!bin 代表排除所有名為 bin 的子目錄,以此類推。(完整命令列參數列表可參考 Command Line Switches)。

實測結果,原本未排除這些目錄前壓縮檔為 32MB,排除後只需 500KB,相差 64 倍。而大小不到 1MB,才不易踩中電子郵件的附件容量上限或禁止包含DLL、EXE的限制。以上小技巧與大家分享~

【茶包射手日記】老 Bug 新感受之 ODP.NET 版本問題

$
0
0

同事報案。某個使用 Managed ODP.NET 的測試網站吐出以下錯誤
Error: The type initializer for 'OracleInternal.Common.ProviderConfig' threw an exception.

同事一度懷疑跟 ODP.NET 版本有關,但依經驗,如為版本問題錯誤訊息會確指出所需元件全名、版號等資訊。為調查問題,我直接在 IIS主機現場撰寫 Tets.aspx 偵錯,測試程式一用到 new Oracel.ManagedDataAccess.Client.OralceConnection() 就吐出以下錯誤:

Configuration Error
Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.
Parser Error Message: An error occurred creating the configuration section handler for oracle.manageddataaccess.client: Could not load file or assembly 'Oracle.ManagedDataAccess, Version=4.121.1.0, Culture=neutral, PublicKeyToken=89b483f429c47342' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
Source File: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config    Line: 14

錯誤訊息提到 Oracle.ManagedDataAccess.Client 4.121.1.0,鐵證如山,證實是 Managed ODP.NET 版本不相容,而machine.config 的第 14 行正是

<section name="oracle.manageddataaccess.client" type="OracleInternal.Common.ODPMSectionHandler, Oracle.ManagedDataAccess, Version=4.121.1.0, Culture=neutral, PublicKeyToken=89b483f429c47342" />

由此推測,問題出在該主機有安裝 Managed ODP.NET 4.121.1,而專案升級到 ODP.NET 4.121.2,但在 web.config 遺漏 bindingRedirect將所有 Managed ODP.NET 版本導向 4.121.2的設定,解析 web.config 時先引用了 machin.config,提到需要 4.121.1 版,但 bin 目錄下卻是 4.121.2 版,便爆出版本不符錯誤。

但有個問題是,為什麼專案程式抛出的錯誤訊息跟 Test.aspx 測試不同?追查出錯的程式片段,發現它是透過 Autofac 取得資料來源物件,屬於動態載入,推測是造成導致錯誤訊息不同的原因,使用以下程式剖析 InnerException,可證明 configuration section handler 錯誤外層又包一層OracleInternal.Common.ProviderConfig 錯誤:

<%@ Page Language="C#" %>
<%
//var cn = new Oracle.ManagedDataAccess.Client.OracleConnection("...");
 
    var dp = MyAutofacFactory.Resolve<IEntityDataProvider>();    
try
    { 
        var cn = dp.CreateConnection(); //其中邏輯為 new OracleConnection後回傳 
    } 
catch (Exception ex) 
    { 
        Response.Write(ex.Message); 
        Response.Write("<hr />"); 
        Response.Write(ex.InnerException.Message); 
    } 
%>

測試結果如下:

•    The type initializer for 'OracleInternal.Common.ProviderConfig' threw an exception.
•    An error occurred creating the configuration section handler for oracle.manageddataaccess.client: Could not load file or assembly 'Oracle.ManagedDataAccess, Version=4.121.1.0, Culture=neutral, PublicKeyToken=89b483f429c47342' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config line 14)

最後回到「為什麼第一時間沒能看出版本不相容?」

問題出在該專案 try catch 了資料庫讀寫作業,但發生錯誤時只抛出 Exception.Message,卻沒保留或顯示 InnerException 或 Callstack 資訊(如要省事,改取 Exception.ToString() 即涵蓋所有重要資訊 ),遺失可供追查辦案的重要線索,是實務上使用 try  catch 易被疏忽的細節,此一經驗提供大家參考。


實戰小技巧 - .NET Exception Message、InnerException 與 ToString()

$
0
0

前篇文章提到 try catch 時若只保留 Exception.Message,可能遺失 InnerException 及 StackTrace 錯失破案重要線索。文章迴響顯示這是個值得介紹的實戰技巧,故再補充一篇。

在某些應用情境我們會選擇使用 try … catch 達成特定目的,例如:(註:Exception 的官方翻譯為例外狀況,這裡容我以用較口語化的「錯誤」取代)

  1. 捕捉可預期錯誤,進行補救並繼續執行程式
    例如:發現作業失敗時,Rollback 交易、寫 Log、通知管理員、退回前一步驟請使用者再試一次... 比程式直接 Crash 來得好。
  2. 捕捉可預期錯誤,改顯示較易懂的錯誤訊息
    例如: 補捉 KeyNotFoundException 傳回錯誤訊息「系統資料未包含您指定的選項,請連絡客服人員」,會比「指定的索引鍵不在字典中」更容易理解。
  3. 捕捉錯誤後改抛回自訂錯誤型別
    優點是上層呼叫端可以使用 catch (MyCustomException mce) 針對自訂錯誤執行特定邏輯。
    而這裡有個小技巧,Exception 有個屬性 InnerException,補捉錯誤並拋出自訂錯誤時要記得將原始 Exception 放入自訂錯誤的 InnerException(稍後將有範例),以便呼叫端追查真實錯誤原因。

關於使用 try … catch 的正確姿勢,微軟文件庫有份文件:例外狀況的最佳作法 - Microsoft Docs可以參考。

下面的程式示範如何捕捉錯誤並改抛回自訂 MyCustException。建構 MyCustException 時要將捕捉到的 ArgumentException 當成 InnerException 包進物件一併傳到上層。而 Main() 在 try catch 時犯了一個錯,它只顯示 MyCustException.Message 就交差:

class Program
    {
staticvoid Main(string[] args)
        {
try
            {
                Test();
            }
catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.Read();
        }
 
staticvoid Test()
        {
try
            {
                InnerCall();
            }
catch (Exception ex)
            {
thrownew MyCustException(
"InnerCall出錯", 
                    ex);               
            }
        }
 
staticvoid InnerCall()
        {
thrownew ArgumentException("明知故犯");
        }
    }
 
class MyCustException : ApplicationException
    {
public MyCustException(string message,
            Exception innerException) :
base(message, innerException)
        {           
        }
    }

如下圖所示,執行時只會看到:

至於為什麼 InnerCall 出錯,錯在哪一段程式,鬼才知道?

要挖出錯誤根源,應檢查 ex.InnerException 是否不為 null,則有內部錯誤資訊,再由 ex.InnerException.Message 取出底層錯誤訊息,但要留意 ex.InnerException 可能還會有 InnerException,真正的錯誤訊息藏在 ex.InnerException.InnerException.Message。換句話說,得寫段遞迴一路剝洋葱才能 100% 保證挖出真正出錯原因。另外,想像 ASP.NET 出錯畫面(YSOD,Yellow Screen of Death)顯示程式碼錯在哪一行,則要透過 Exception.StackTrace取得。

聽起來很麻煩,但有條捷徑,將 ex.Message 改成 ex.ToString() 就好了!

如上圖所示,ToString() 會包含 InnerException (黃字部分),以及 StackTrace (方法名稱與程式行數),該有的資訊都有。

【結論】try catch 時要保存或顯示完整錯誤資訊,建議改用 ToString(),別只用 Message 讓真相消失在風中。

IIS HTTP 強制轉 HTTPS 簡易做法

$
0
0

再遇到老題目:在 IIS 上如何將 HTTP 請求強制導向 HTTPS?

之前試過顯示說明網頁,倒數後透過 JavaScript location.href 轉向 HTTPS 的做法 - 設計賓至如歸的HTTPS強制導向網頁

但這有個缺點,如果不需要顯示導向提示,則先 HTTP 200 送回正常網頁再由瀏覽器另外發出請求連上 HTTPS 多耗費一次往返,不如直接回傳 HTTP 301/302 導向有效率,而連上 HTTP 時回應 HTTP 200 還可能會被搜尋引擎誤判為有效網址。

網路建議的解法多是使用 URL Rewrite 模組解決,例如保哥強迫網站轉向到 HTTPS 加密安全連線 ( IIS URL Rewrite )一文提到的做法。

之前的情境都是原本 HTTP 舊站啟用 HTTPS 後希望大家改連加密版,這回的狀況是網站一上線就只有 HTTPS,只需將誤連 HTTP 的使用者導向 HTTPS,不需將 HTTP 完整路徑對應成 HTTPS 版本。基於導向入口網頁不需完整對應的單純需求,我找到一個不用安裝 URL Rewrite 的簡便做法。

首先在 IIS 管理員確認勾選「SSL設定 / 需要SSL」,如此連上 HTTP 將得到 HTTP 403.4 錯誤。

接著在「錯誤網頁」指定 403.4 錯誤訊息網頁:

指定自訂錯誤網頁時使用「回應 302 重新導向」並輸入 https: 版首頁。

以上設定也可透過 web.config 加入:

<system.webServer>
<httpErrors>
<errorstatusCode="403"subStatusCode="4"
path="https://www.my-site.com.tw/"responseMode="Redirect"/>
</httpErrors>
</system.webServer>

這方法還有一個小瑕疵,它傳回的是 302(暫時搬移) 不是 301(永久遷移),可能影響搜尋引擎判斷。餏據爬文結果:1) 若原本 HTTP 網址沒有內容,不致有被 Cache 住遲遲沒更新的疑慮 2) 由於業界太多人把 302 當 301 用,Google 已經聰明到將 302 視為 301。參考:302 Redirect vs. 301 Redirect- Which is Better- - Hochman Consultants

【茶包射手日記】VS2017 錯誤清單出現 TypeScript 版本相關錯誤卻可編譯

$
0
0

之前處理過 VS2017 更新導致 TypeScript 出現大量 is not assignable to 錯誤問題,若暫時不打算改寫 TypeScript 升級 2.4+,確保專案 TypeScript 版本設定維持在舊版即可。但實際使用發現有個困擾:雖然編輯及使用都正常,但 VS2017 的錯誤清單視窗(Error List)冒出大量 is not assignable to 錯誤,一片紅通通很礙眼,其他程式有錯要查訊息如同大海撈針。

研究一陣子不得其解,不經意看到最下方兩則 Warning 才有點頭緒:

Your project is built using TypeScript 2.3, but the TypeScript language service version currently in use by Visual Studio is 2.5. Your project may be using TypeScript language features that will result in errors when compiling with this version of the TypeScript compiler. To remove this warning, install the TypeScript 2.5 SDK or update the TypeScript version in your project's properties.

JavaScript Language Service是 VS2017 推出的新服務,負責提供 JavaScript/TypeScript Intellisense,它會隨 VS2017 版本更新使用最新版 TypeScript Compiler,而專案編譯採用的 TypeScript Compiler 版本則由 .csproj 設定決定,因此就出現 Error List 認定有錯但編譯可過的矛盾現象。

爬文沒查到解法,腦海閃過偷改 TypeScript Language Service 編譯器版本設定的想法,但太髒太 Hacking 了,趕緊自打巴掌逼自己忘掉,最後乖乖調整 TypeScript 到 2.4+ 相容解決問題。所幸,靠外加 <any> 強轉型或是型別宣告 Union any 幾乎都能解決,還算好改,而改到相容到底才是治本之道。

捉鬼記 - Google 搜尋結果被穿插廣告

$
0
0

近來在筆電用 Chrome Google 查東西怪怪的,搜尋結果出現後一兩秒,最上方會冒出幾則 AdSense 廣告,廣告項目的樣式偽裝成一般查詢結果,還會隨機插穿於正常結果之間,閱讀結果時得自行剔除還常不小心點到,讓人肚爛到極點。

一度懷疑是 Google 網站改版想藉此增加廣告營收,但經過分析很快排除此一可能,理由如下:

  1. 搜尋結果先出現,廣告是被疊加上去的。有時還會先看到標準 AdSense 廣告框出現在最上方,接著廣告框消失,裡面的廣告項目改變 CSS 樣式混入正常的查詢結果中,事後加工痕跡超級明顯~ 如果是 Google 幹的,大可在伺服器端就將廣告混入搜尋結果,這種事後加料的手法比較像外掛搞鬼。
  2. Google 搜尋結果被混入廣告只出現在我筆電的 Chrome,改用 IE/Edge/Firefox 都正常,家裡跟公司 PC 的 Chrome 都沒這問題。
  3. 偷雞摸狗塞廣告干擾使用的手法太過拙劣,一樣不像 Google 的行事作風,Google 標榜 Don't be evil,深知這麼做肯定要被吐口水。

檢查筆電 Chrome 安裝的外掛,很快找到嫌犯 – Social Video Downloader。停用後,搜尋結被亂塞廣告的狀況立刻消失~

抓到嫌犯,再來就是派出 CSI 小組搜證起訴了。透過 F12 開發者工具,我找到來自可疑網域名稱(www. 9rue8ughjffo. xyz)的JS,裡面有將廣告混入結果網頁的程式碼:

往源頭追查,整理 Google 搜尋結果被加料的過程如下:

  1. 某支來自 Chrome 外掛的 analytics.js 在網頁注入 s3. amazonaws. com/js-cache/bc25…2d00.js
  2. bc25…2d00.js 在網頁注入 www. 9rue...jffo. xyz/script/d.php?uid=….
  3. d.php?uid=... 中的 JavaScript 程式負責在 Google 搜尋結果頁面塞入 AdSense 廣告並穿插進結果間

從 F12 工具查出 Chrome 外掛的識別代碼:

追到外掛所在資料夾 %appdata%\Local\Google\Chrome\User Data\Default\Extensions\amjcoehkcacocffpmhnefgoeanepjfkf,由 popup.html 確認是 Social Video Downloader 無誤。

[2018-02-24補充] 經網友 Alex Lion 提醒,檢查我安裝的外掛版本已不存在,猜想已被檢舉下架,而商店有另一個同名外掛並無此問題,推測我當初可能是誤裝了被惡意加料的山寨版。

回覆威武~~ (怒拍驚堂木) 山寨搜羞比笛歐擋漏的,你亂塞廣告,擾人爬文,減損國力,罪大惡極,本府判你鍘刀之刑,你可認罪?
Fake Social Video Downloader: I provide you such good service, it's absolutely reasonable to make some money…
大膽刁民,沒事撂英文是欺負本府不懂逆? 來人吶,開~~~鍘~~~

就這樣,我把外掛移了還得到一個心得:

Chrome 外掛好危險,使用之前宜細選。

元件開箱:Managed ODP.NET for Linux

$
0
0

.NET Core 版本已推進到 2.0,但對我而言,相關元件、程式庫的支援度才是能否用於工作的關鍵。最近有則好消息 – Oracle 在這個月推出 ODP.NET Core 12.2 Beta,感覺在工作專案使用 .NET Core 的日子又更近了~

工作環境常會存取 Oracle 資料庫,.NET Core 再好用,連不上資料庫也是白搭。ODP.NET Core 補上這塊拼圖,背後的另一層意義是 Oracle 也已正式將 .NET Core 納入支援平台!(雖然 Oracle 支援 .NET 的腳步向來不快,過去常需靠第三方程式庫墊檔,例如千呼萬喚始出來的 Oracle 官方版 Entity Framework姍姍來遲的好物 – Managed ODP.NET)

ODP.NET Core 12.2 Beta 目前尚未放上 NuGet,要測試需自行從官網下載安裝,ZIP 檔裡有一個 Oracle.ManagedDataAccess.dll 及一份 PDF 說明文件。不出意外的好消息是 ODP.NET Core 採 Managed ODP.NET,使用時不需安裝 Oracle Client。與 .NET Framework 版相比,ODP.NET Core 少了一些 API,部分來自於 .NET Core 本身限制,例如: ConfigurationManager、RegistryKey、EventLog、分散式交易、Code Access Security... 等,其餘功能如 Entity Framework(System.Data.Metadata.Edm)、DbProviderFactories、DirectoryServices 則計劃在 .NET Core 2.1 或之後加入。

期待以久的產品上市,當然要馬上開箱測試一下。用 Visual Studio 2017 建立一個 .NET Core Console App 專案:

使用 Add Reference 瀏覽找到剛才下載的 Oracle.ManagedDataAccess.dll:

加入後 Oracle.ManagedDataAccess.dll 應該要出現在 Dependencies\Assemblies 下:

Dapper老早就支援 .NET Core,用 NuGet 即可安裝。寫資料庫程式不必扯出一堆囉嗦的 OracleCommand、OracleParameter、OracleDataReader 就是爽!

using System;
using System.Linq;
using Oracle.ManagedDataAccess.Client;
using Dapper;
 
namespace OdpNetCore
{
class Program
    {
privatestaticstring cs = "data source=BLAH;user id=BLAH;password=***";
staticvoid Main(string[] args)
        {
            Console.WriteLine("ODP.NET Core Test");
using (var cn = new OracleConnection(cs))
            {
                var text = cn.Query<string>("SELECT :t AS T FROM DUAL", 
new { t = "Hurray"}).Single();
                Console.WriteLine(text);
            }
            Console.Read();
        }
    }
}

寫好程式,在 VS2017 按 F5 測試 OK!

重頭戲來了,一模一樣的程式搬到 Linux 也能跑,這才是 .NET Core 的價值所在。

使用 Publish 產生部署用的檔案。(較詳細的 .NET Core 部署說明可參考 試駕體驗-小工具程式 .NET Core 1.1 版試寫)

在 Linux 主機確認裝妥 .NET Core 2.0 (安裝步驟請參考官方文件),將上述檔案與 TNSNAMES.ORA 複製到同一目錄下,執行 dotnet AppName.dll,我們的 .NET 程式就從 Linux 連上 Oracle 資料庫,酷!

註:TNSNAMES.ORA 除了與程式放在同一目錄,也可放在共用目錄(例如: /etc/oracle/network/admin) 再設定環境參數 export TNS_ADMIN=/etc/oracle/network/admin

Viewing all 2311 articles
Browse latest View live