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

【茶包射手日記】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-Powered-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的限制。以上小技巧與大家分享~

【2018-02-09 補充】

若不想每次敲參數,可建立一個 proj-ignore.txt 檔,輸入:(一個項目一行)
bin
obj
packages

之後用 7z a MyProject.7z MyProject –mx7 –r –xr@proj-ignore.txt 即可重複使用排除清單。(感謝網友 agrozyme 回饋)


【茶包射手日記】老 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

TIPS - 限定 Visual Studio 使用的 C# 語言版本

$
0
0

同事通報:無法編譯簽入的 C# 專案,部分程式碼存在語法錯誤,細查發現是 Visual Studio 2015 Update 3 不認得以下寫法:

publicstring Id 
{
    get => RawId.ToString();
    set => RawId.Parse(value)
}

原以為這是 C# 6 語法,VS2015 理應支援,進一步確認是 C# 7 才有的新功能(Expression Body Property Accessor),難怪 VS2015 不認得。(其實專案安裝 Microsoft.Net.Compilers 套件後可用 VS2015 編譯跟執行,但必須忍受程式碼有一堆紅色毛毛蟲,更多細節可參考小朱的文章:[C#] 在 Visual Studio 2015 使用 C# 7 - 小朱® 的技術隨手寫)

語法糖不吃不會死,自己想吃糖而逼人升級太傲嬌,乖乖配合大家採用通用版本才不會被蓋布袋是好隊友。

爬文學到一招,VS2015 / VS2017 專案屬性的 Build 設定有個 Advanced 鈕,開啟 Advanced Build Settings 後有地方可以指定專案採用的語言版本:

將語言版本調成 6.0,VS2017 會將 C# 7+ 才有的寫法標示為錯誤並顯示相關提示,如此可確保專案使用 C# 語法與較早版本的 Visual Studio 相容,減少開發團隊合作時的困擾。


【茶包射手日記】IE11 localStorage 為 null

$
0
0

測試一陣子的專案接到回報,某網頁在某台電腦用 IE11 檢視有錯,用 Chrome 或其他電腦的 IE 則無問題。

起初懷疑是 IE 相容模式問題,幸好使用者在同一辦公室,得以親自借用電腦開 F12 查案,追出關鍵點在於問題 IE 的 localStorage 不知何故為 null,而網頁裡的 JavaScript 程式假設網頁會在 IE9+ 或 Chrome 執行,未考慮瀏覽器不支援 localStorage 出錯。

依我所知,IE8+ 即支援 localStorage,而爬文得知,IE 遇 file:// 會禁止存取 localStorage,但此種狀況 localStorage === undefined (如下圖所示),跟觀察到 localStorage === null 的狀況又不盡相同。

靈機一動,改朝「有沒有可能 localStorage 被 IE 停用?」方向追查,果然,在「網際網路選項/進階」有個「啟用 DOM 儲存」選項,預設勾選啟用,在問題 IE 卻是停用狀態,重新啟用後一切恢復正常:

其實,不管 IE、Chrome、還是 Firefox 都有開關可以停用 localStorage/sessionStorage,只是寫網頁多年都沒想過它可能被使用者停用而疏漏了檢查,錯失在第一時間提供偵辦線索,經此事件長了知識,下回會納入考量。

後記:好奇詢問報案人,怎麼會想到要去停用 DOM 儲存。答案不出所料,他沒動過設定,電腦是前人移交的,為什麼會這樣沒人知道。一如往例,每個茶包總會存在某些不可考的謎,除錯人生才如此有趣。 XD

VS2015 Update 3 TypeScript 語法標示失效

$
0
0

Visual Studio TypeScript 版本相容問題又來了~ Orz

同事 Visual Studio 2015 Update 3 的 TypeScript 版本仍在 1.8,配合專案要升級到 2.3 (相容 2.4 版需修改程式), 沒留神裝成 TypeScript for Visual Studio 2015最新版( 2.7.2,應點開 Detail 才能下載歷史版本), 之後發現版本不對再安裝了 TypeScript for Visual Studio 2015 2.3.3。

之後就是一連串打怪的過程:

最開始是 Visual Studio 2015 錯誤清單冒出大量 TypeScript 錯誤無法編譯,推測是 TypeScript 引擎被換成最新版。移除 TypeScript for Visual Studio 2015 2.7.2 後可成功編譯。

移除 2.7.2 後專案可以編譯沒錯,但 TypeScript 的 IntelliSense 出了問題,跳出的都是以 JavaScript 角度解析的提示項目。
懷疑 2.7.2 是換掉 Visual Studio 使用的 TypeScript 元件,移除 2.7.2 時元件也被移除導致功能異常。

接著我嘗試移除並重裝 2.3.3,沒想到結果更慘,這下子連 TypeScript 語法標示(Syntax Highlighting)都不見了,IntelliSense 徹底消失,只剩文字檔編輯功能。

知道是版本問題,但要用什麼關鍵字爬文毫無頭緒。多試幾下,在專案屬性視窗 TypeScript Build 找到較明確的訊息:

An error occurred trying to load the page.
No exports were found that match the constraint:
  ContractName  Microsoft.CodeAnalysis.Editor.TypeScript.ScriptContexts.ITypeScriptProjectProvider
  RequiredTypeIdentity  Microsoft.CodeAnalysis.Editor.TypeScript.ScriptContexts.ITypeScriptProjectProvider

用關鍵字在 TypeScript Github 基地查到可怕的討論串,發生在 TypeScript Tools for VS2015 2.0.2 版,一堆人連 VS2015 都重裝了還搞不定... 在討論串下方發到一線生機,找到另一則討論,有網友找到解決:

1.刪除 %localappdata%\Microsoft\VisualStudio\14.0\ComponentModelCache 資料夾
2.以管理者身分開啟 Developer Command Prompt for VS2015 執行 devenv /setup

如法炮製,終於 VS2015 的 TypeScript 編輯器恢復正常。

當心 Request.Url.ToString() 傳回無效 URL

$
0
0

抓到一個誤用 Request.Url.ToString() 造成的 Bug。

由使用者回報錯誤,追出某段程式擷取 Request.Url.ToString() 加工產生的連結有誤。 原本 URL 參數包含另一頁網址,依規定做了 UrlEncode,如: b=http%3A//blog.darkthread.net%3Fm%3D1%26a%3D456 。ASP.NET 程式抓取 Request.Url.ToString() 修改後串接額外參數"&a=ABC",得到的結果卻是 b=httq://blog.darkthread.net/?m=1&a=456&a=ABC [註: 此處改成 httq 是為防止被當成可連結網址],也因此,接收網頁抓取的 a 變成 456,ABC。

實測後發現,我一直誤認 Request.Url.ToString() 傳回的會是 URL 原始字串(就是瀏覽器地址欄顯示的字串內容),事實不然,來看一個測試:

依據 MSDN 文件

The value returned by this property differs from ToString and AbsoluteUri. ToString returns the canonically unescaped form of the URI. AbsoluteUri returns the canonically escaped form of the URI.

要將 Uri 型別轉為字串,ToString()AbsoluteUriOriginalString都是選項,但意義與用途不同。

Uri.ToString() 傳回的是 URI 以標準方式未逸出處理(Unescaped)過的版本
Uri.AbsoluteUri 傳回的則是 URI 以標準方式逸出處理(Escaped)過的版本
Uri.OriginalString 則是未經處理的原始版本(OriginalString 序列化後不會保留)

換言之,OriginalString 是建構物件時所提供的字串參數,原汁原味連不符規範的部分也都保留;ToString() 及 AbsoluteUri 則為標準化處理過的版本, ToString() 為方便人類閱讀將 %XX 編碼還原回字元,但可能因此不符合 URL 規範,用於瀏覽器可能會出錯(就如本案例); AbsoluteUri 產生的結果則力求符合規範,遺漏 UrlEncode 的部分也會補上,例如以下範例:

var uri = new Uri("http://blog.darkthread.net?a=a c"); //參數空白未編碼 
Debug.WriteLine(uri.AbsoluteUri); //會轉為http://blog.darkthread.net/?a=a%20c

在本案例中,AbsoluteUri 是較佳選擇,而實務上應避免使用 ToString(),以免產生無效 URL。

SQL 最大伺服器記憶體設定值研究

$
0
0

SQL Server 預設會用光主機所有記憶體(預設上限為 2PB = 2048TB),除非整個資料庫容量小於總記憶體,否則把記憶體當成 Cache 能提升效能、減少磁碟 I/O 耗損,絕對利多於弊。如果同主機有多個資料庫執行個體,或是要與其他應用程式、服務分享記憶體,就必須透過 Max_Server_Memory設定伺服器可用記憶體上限,保留一部分供其他程式使用。

最近被問到一個問題,針對專屬資料庫用途的 SQL Server 主機,剩餘記憶體的監控警戒值該怎麼抓?若依通用慣例抓 90%,主機應該 7x24x365 都處於紅色警戒,警告信件簡訊滿天飛,明顯不可行。那麼:針對專屬 SQL 主機,Max Server Memory 該設多少? 記憶體警戒水位該怎麼抓?

關於 SQL 最大伺服器記憶體(Max Server Memory)的估算,我查到 SQL MVP Jonathan Kehayias 一篇常被普遍的指南 - How much memory does my SQL Server actually need- - Jonathan Kehayias

SQL 最大伺服器記憶體基本規則:

總記憶體 4-16GB 時,保留 1GB 給 OS,剩下全撥給 SQL Server。總記憶體超過 16GB 時,每增加 8GB 再多留 1GB 給 OS。

要估許是否能撥更多記憶體給 SQL 可觀察 Memory\Available Mbytes 效能指標,目標是可用記憶體最起碼維持 150-300MB
即可。( 低於96MB Windows 會發警報 LowMemoryResourceNotification,>256GB 的主機建議保留可用 1GB 以上較保險 )
另一版更詳細的計算公式為:

總記憶體 – 執行緒堆疊用量[最大工作執行緒數目 * 堆疊大小(x86 512K、x64 2MB)] – OS保留量(1-4GB) – 其他應用程式保留量 – SQLCLR/Linked Server 等使用量

官方文件也提到類似估算方法:

  • 從 OS 總記憶體中保留 1GB - 4GB 的記憶體給 OS 本身。
  • 減去等於不受 [最大伺服器記憶體] 控制的潛在 SQL Server 記憶體配置,該配置的算法為堆疊大小 1 * 計算得出的最大背景工作執行緒數 2 + -g 啟動參數 3 (若未設定 -g,則預設為 256MB)。 餘數即為單一執行個體安裝的 max_server_memory 設定。

文章冑並提供一個判斷記憶體是不是給太多的方法 – 觀察以下三個效能計數器:

  1. SQL Server:Buffer Manager\Page Life Expectancy
  2. SQL Server:Buffer Manager\Page Reads/Sec
  3. Physical Disk\Disk Reads/sec

若 Page Life Expectancy 持續上升永不下降而 Page Reads/Sec 及 Disk Reads/Sec 偏低,象徵記憶體過剩,可試著減少 Max Server Memory 再觀察指標變化,設法讓 Page Life Expectancy 落入合理值。(依 Jonathan 的建議,PLE合理值 = 資料Cache GB數 / 4GB * 300,每 300 秒平均淘換掉 4GB 的 Cache 內容,時間過短將導致讀寫 IO 偏高)

除此之外,專屬 SQL Server 有時也會拿來跑 SSIS、SSAS,這部分不算在 SQL Server 的記憶體用量,要記得歸類為其他應用程式計算。

最後,回到記憶體警戒水位如何決定。只跑資料庫引擎的 SQL 主機,若無意外會將撥給它用的記憶體吃好吃滿,只留 1GB 給 Windows, 故系統剩餘可用記憶體用量會依 Windows 及其他程式用剩多少而訂,改抓 Memory: Available MBs 指標會比百分比更容易性,依 Janathan 建議可抓 >300MB 以求保險( 256GB 以上的主機抓 1GB ),應是不錯的參考值。

CODE - C# 推算檔案相對路徑

$
0
0

最近在資料夾比對工具遇到一個需求:要以某個資料夾為基準推算檔案的相對路徑。例如,若基準資料夾為 C:\Folder,則 C:\Folder\SubFolder\Test.txt 的相對路徑為 SubFolder\Test.txt。

類似需求以前加減寫過,說穿了全是字串比對的功夫,靠將路徑前方相同部分移除取得相對路徑。如果相對路徑需支援 ..\..\Test.txt 這種寫法,邏輯會複雜一些,若還再考慮英文大小寫的話...

總之,計算相對路徑說難不難,自己寫瑣碎細節倒也不少。於是我想起前幾天談到的 .NET Uri 類別,恰巧有個 MakeRelativeUri() 方法,研究後找到坐享其成的省事解法。

直接用範例程式展示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;

namespace UriTest
{
    class Program
    {
        static void Main(string[] args)
        {
            //標準測試
            Test(@"C:\Folder\Test.txt", @"C:\Folder\");
            Test(@"C:\Folder\SubFolder\Test.txt", @"C:\Folder\");
            //上層資料夾
            Test(@"C:\Folder\Test.txt", @"C:\Folder\SubFolder");
            //相鄰資料夾
            Test(@"C:\Folder\SubFolderA\Test.txt", @"C:\Folder\SubFolderB");
            //忽略大小寫差異
            Test(@"c:\folder\subfolder\Test.txt", @"C:\FOLDER\SUBFOLDER");
            //限定basePath子目錄下,不允許上層或相鄰目錄(安全考量)
            try
            {
                Test(@"C:\Folder\Test.txt", @"C:\Folder\SubFolder", true);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Error: {e.Message}");
            }
            try
            {
                Test(@"C:\Folder\SubFolderA\Test.txt", @"C:\Folder\SubFolderB", true);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Error: {e.Message}");
            }

            Console.ReadLine();
        }

        static void Test(string path, string basePath, bool limitSubfolder = false)
        {
            Console.WriteLine(new string('=', 40));
            Console.WriteLine($"Path: {path}");
            Console.WriteLine($"Base Path: {basePath}");
            Console.WriteLine(
                $"Relative Path: {GetRelativePath(path, basePath, limitSubfolder)}");
        }

        public static string GetRelativePath(string fullPath, string basePath, 
            bool limitSubfolder = false)
        {
            if (!basePath.EndsWith(@"\")) basePath += @"\";
            Uri fp = new Uri(fullPath);
            Uri bp = new Uri(basePath);
            var relPath = bp.MakeRelativeUri(fp).ToString().Replace("/", @"\");
            if (relPath.Contains(@"..\") && limitSubfolder)
                throw new ApplicationException("path must be under basePath!");
            return relPath;
        }
    }
}

結果如下,只需幾行程式搞定,推算"..\"上層目錄、英文大小寫有別都難不倒它。

補充兩個小地方:

  1. Uri 傳回的路徑依循 RFC 規範用"/"作為目錄分隔字元,在 Windows 環境可將"/"置換成"\"以求一致。
  2. 實務上有時會限制檔案必須放在指定目錄下,因此我在 GetRelativePath() 加了 limitSubfoler 參數,以便在跳脫範圍時抛出例外。
Viewing all 428 articles
Browse latest View live