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

實例分析-彈出式視窗被瀏覽器封鎖

$
0
0

昨天剛在公司解完案例,今天又在日常生活遇到實例,老天爺這暗示明顯無比,趕緊來篇筆記,以防出門被雷劈!

最近迷上蝦皮拍賣。能跟 LINE 一樣跟賣家溝通超方便,尤其問完問題馬上接到賣家的實體照片真令人感動,不用出門與人面對面又保有臨櫃交談的即時性,真是阿宅的救星。遇到一個賣家很妙,凌晨四點多發訊息通知我補寄商品今天會到(不知對方有沒有早起的拎杯秒回「謝謝」嚇到? XD)。

在中華郵政網站輸入包裹號碼,按下「運輸資料」查詢鈕… 登楞!

JavaScript 試圖彈出視窗被 Chrome 瀏覽器封鎖了!

簡單來說,這是踩中「瀏覽器會封鎖不是使用者點擊直接觸發的 window.open」地雷。(詳細說明可參考: showModalDialog與IE快顯封鎖

無聊追進郵局包裹查詢程式幫忙 Debug 順便練功。網頁是用 Angular 寫的:

<a class="css_btn_class_gray ng-scope" ng-click="showInfo('Base64編碼');">運輸資料</a>

按鈕時會觸發 API 查詢

$scope.showInfo = function(b64Str){ $scope.queryAPI(b64Str); };

$scope.queryAPI 呼叫 AJAX 查詢,接收結果後呼叫 setTimeout(function() { $scope.createTRSView(); }, 500); 開啟結果視窗:

    $scope.queryAPI = function(b64Str){
        $scope.sendRecv("EB500100", "queryAPI", API_Url, vo,
function(tota, isError) {
//查詢處理(省略)
//延遲0.5秒
                setTimeout(function() {
                    $scope.createTRSView();
                }, 500);
            }
        });
    };
    $scope.createTRSView = function(){
var template = "<html><head><title>郵袋籃車查詢</title>…略…";
var mw = window.open("", "_blank");
        $scope.mw = mw;
if(mw) {
            mw.document.write(template.format($("#trs_template").html()));
        } else {
            alert("無法開啟查詢視窗");
        }
    };

由以上邏輯可發現,window.open 與 onclick 事件間隔了兩層非同步,第一層是呼叫 API 回傳結果(背後使用的是 $http Promise 機制),之後透過 setTimeout 又是另一層非同步,window.open 動作怎麼都不可能算成 onclick 的直接觸發行為,註定要被瀏覽器攔截!

遇到這種情境要怎麼處理呢?我想到幾種做法:

  1. 改為直接操作 XmlHttpRequest 以同步方式呼叫(網頁卡住等待伺服器傳回結果再繼續執行),收到結果後直接 window.open,避開 XHR 非同步執行、$http Promise 與 setTimeout 三層非同步,以符合 window.open 被包在 onclick 事件中的條件。 不過,這做法與 AJAX 精神背道而馳,我歸類為餿主意。
  2. 在 onclick 事件就 window.open 將結果視窗先開好(得先顯示「查詢中…」之類的動畫,不然會很乾),待 AJAX 呼叫完成後再將結果填入,但無法視 AJAX 呼叫結果決定要不要開視窗是一大缺點。
  3. 避用 window.open(),改以 IFrame 內嵌或直接在 DOM 建立 div 放入結果。

我個人偏好第 3 種做法,練功完畢。


初試 Bash on Ubuntu on Windows 10

$
0
0

心血來潮,想試試 Windows 10 的新玩意兒 – Bash on Ubuntu on Windows,依我個人看法,Bash on Windows 最重要的意義不是用 Linux Shell 換掉 DOS Shell,而在於用 Windows 10 直接跑 Ubuntu 原生程式,就像 iOS 可以跑 Android App 一樣,是令人雀躍的一大突破!

安裝 Bash on Windows 10 前要先確認 Windows 10 組建版本大於 14393,且必須為 64 位元版本。(這年頭應該沒人裝 32 位元了吧?)

由於 Bash on Windows 10 仍在 Beta 階段,使用前要進「設定/開發人員專用/使用開發人員功能」切換成「開發人員模式」:

到 Windows 功能安裝介面選取「適用於 Linux 的 Windows 子系統(搶鮮版(Beta))」

安裝完成後,開啟 DOS 視窗執行 bash 即開始下載及安裝。過程需設定 UNIX 使用者名稱與密碼,安裝完畢就直接進入 Bash Sell 環境:

平時要開啟 Bash Shell,有兩種做法:使用捷徑 Windows 上 Ubuntu 的 Bash:(若有需要可以釘選在開始畫面或工具列)

或是從 Windows 開始或 DOS 執行 bash:

簡單整理我的初步試用心得:

  1. 跟 Ubuntu 一樣,軟體或程式庫用 apt-get 就能安裝或更新,能在 Windows 直接跑 Ubuntu 原生程式,感覺超讚!
  2. 磁碟 C、D 在 Bash 被對映成 /mnt/c、/mnt/d,要注意 Bash 區分大小寫,輸入路徑時要改一下習慣。
  3. 可以顯示中文但無法輸入,中文顯示也有點問題,會吃字。(字串有幾個中文字,尾端就少幾個字元)
    以下圖為例,檔名「八個中文長度少八987654321」有 8 個中文,會顯示為「八個中文長度少八9」,最後 8 個字元消失。

    爬文找到一些關於 Bash on Windows 亞洲語系支援問題的討論,看來要等未來版本修正。

【茶包射手筆記】NUnit 發生 OutOfMemoryException

$
0
0

從 Github 取得 ServiceStack.Text想幫忙修 Bug。專案使用 NUnit 跑單元測試,為方便測試,在 Visual Studio 2017 安裝 NUnit 3 Test Adapter,安裝後可由 Test Explorer 直接執行測試。

不料,編譯後 Test Explorer 只找到一項測試,Output / Tests 則出現大量 OutOfMemoryException:

Exception System.OutOfMemoryException, Exception converting ServiceStack.Text.Tests.XmlSerializerTests.serialize_Territory
已發生類型 'System.OutOfMemoryException' 的例外狀況。

爬文找到解法,原因出在 NUnit 還不支援 .NET Core 使用的新版 Portable PDB 格式(未來的新標準),而 ServiceStack.Text.Tests 是一個 .NET Core 專案(同時支援 net45 與 netcoreapp1.0),.NET Core 版本的 PDB 導致問題。解決方法是在 csproj 中加入以下 PropertyGroup,指定當 Target 不等於 netcoreapp1.0 時 DebugType = Full 以產生 Windows 版 PDB:(預設 DebugType 為 Portable,Full 應源自 Full Framework,不是指完整版 PDB,參考

  <PropertyGroup>
    <DebugType Condition="'$(TargetFramework)' != '' AND '$(TargetFramework)' != 'netcoreapp1.0'">Full</DebugType>
  </PropertyGroup>

加入設定後,Test Explorer 順利找到測試項目並成功跑完,綠燈!

筆記-使用 Dns.GetHostEntry 解析 IP 位址

$
0
0

某排程使用以下程式碼產生 IEndPoint 以建立 Socket:

IPEndPoint pEndPoint = new IPEndPoint(Dns.GetHostEntry(remoteHost).AddressList[0], remotePort);

其中用了 Dns.GetHostEntry(),好處是不管 remoteHost 傳入的是主機名稱還是 IP,一律可轉成 IPAddress。

排程在正式及測試環境運作多時,今天將程式移到另一網段機器上執行,remoteHost 為 IP 位址(假設為 192.168.1.1),與原本設定相同,確認新主機與 192.168.1.1 間網路暢通,甚至用 telnet 192168.1.1 portNo 建立連線也成功,但程式一執行就出現以下錯誤:

[SocketException (0x2af9): No such host is known]
   System.Net.Dns.InternalGetHostByAddress(IPAddress address, Boolean includeIPv6) +2221072
   System.Net.Dns.GetHostEntry(String hostNameOrAddress) +6671028

認真看了 MSDN 文件,搞懂 GetHostEntry() 邏輯才恍然大悟。

GetHostEntry 的介面為:

public static IPHostEntry GetHostEntry(
    string hostNameOrAddress
)

其中 hostNameOrAddress 參數可以是主機名稱也可以是 IP 位址。當傳入 IP 時 GetHostEntry 假設程式想取得 IPHostEntry 的完整資訊-包含 AddressList, Aliases, 與 HostName,因此將執行以下動作:

  1. 嘗試解析 IP 位址,若 hostNameOrAddress 傳入的是有效 IP 位址字串,轉成 IP 位址物件不是問題
  2. 利用 IP 反查取得主機名稱存入 HostName
  3. 利用主機名稱查詢該主機的所有 IP 位址,存入 AddressList

今天出錯的關鍵在於程式原本所在的正式與測試主機與 192.168.1.1 隸屬同一網域,而問題主機則屬於另一個網域,兩個網域雖有信任關係,但 WINS 及 DNS 反查未打通,故在進行第 2 步以 IP 反查 HostName 時踏到鐵板,產生 No such host is known 錯誤。

知道原因就好辦,有幾種解決方法:

  1. 設定 system32/drivers/etc/hosts 讓 192.168.1.1 能反查到主機名稱
  2. 改用 FQDN,GetHostEntry() 改成用 FQDN 解析 IP(DNS 解析的功能是好的)
  3. 修改程式,當 remoteHost 為 IP 位址時不走 GetHostEntry(),改用 IPAddress.Parse()

最後採行方案 2,問題排除。

VS2017 Git SSL 憑證無效問題

$
0
0

少數人會遇到的冷門問題,使用 Visual Studio 連上 Github 或自有 Git 伺服器時發生 SSL 憑證錯誤:

Git failed with a fatal error. unable to access '…': SSL certificate problem: unable to get local issuer certificate

可能原因有二:

  1. 網站連線時 SSL 憑證遭網管設備置換,Windows 已設定信任網管設備的 CA 根憑證,但 Git 因屬不同體系,拒絕承認置換憑證有效性
  2. 私有 Git 伺服器使用自己簽發的 SSL 憑證,其根憑證未被信任

針對這類情況,解決方法也有二種,第一種是停用憑證檢查(省事但不安全)、第二種則是讓 VS2007 Git 信任該憑證。

方法1 編輯 c:\Users\你的帳號\.gitconfig,加入

[http]
    sslVerify = false

如此 Git 工具將一律忽略憑證無效的問題,風險是萬一網路被惡意人士攔截竊聽,你也不會發現。

方法2 指定 Git 信任特定憑證

先將要信任的 CA 憑證匯出成 CER,格式請選「Base-64 編碼 X.509」

匯出的 CER 檔是個文字檔,格式為 -----BEGIN CERTIFICATE----- 與 -----END CERTIFICATE----- 間夾著一段 Base64 編碼碼。

找到 C:\Program Files\Git\usr\ssl\certs\ca-bundle.crt,將它複製到 c:\Users\你的帳號 目錄下,將 CER 裡的文字加在最後面:

最後,修改 c:\Users\你的帳號\.gitconfig,加上 sslCAInfo 指向我們修改過的 ca-bundle.crt。

大功告成!

【延伸閱讀】

關閉 VS2017 Chrome 偵錯整合

$
0
0

使用 Visual Studio 2017 偵錯網頁,馬上發現不同:VS2017 增加了對 Chrome 的整合度。當選擇使用 Chrome 檢視網站,按下 F5 偵錯網站,VS2017 將另起一個獨立 Chrome 程序(過去會在既有 Chrome 程序開啟新分頁),歷經短暫的等待(畫面如下),VS2017 會經由 URL 注入程式,透過 Chrome DevTools Protocol與 F12 開發工具整合,允許在 VS2017 IDE 設定 JavaScript/TypeScript 中斷點、觀察變數、執行即時指令以及逐行偵錯;當 VS2017 停止偵錯,Chrome 也將自動關閉,整合度比照 IE。

不過,新功能跟我不投緣。一來是對 Chrome F12 開發者工具己經上手,其 JavaScript/CSS/AJAX 偵察除錯功能也比 VS 強大完整;二則是啟動整合偵錯後,每次啟動時間會拖長,累積的等待時間嚴重消耗中年程序員所剩不多的寶貴青春,好令人心焦… 最後,偵察 JavaScript 問題我習慣保留網頁,改完程式存檔或編譯後重新整理網頁,馬上就可接著測試,比起「結束偵錯關閉瀏覽器,下回再重新啟動」有效率。VS2017 的新功能導致 Chrome 反覆關閉重啟,有時還造成如上圖的「Chrome 未正確關閉」問題,有些惱人。

總之,試用一陣子後,決定把它關掉。

找到 Options / Debugging / General / Enable JavaScript debugging for ASP.NET (Chrome and IE),取消勾選,偵錯行為就回到以前的做法囉~

【延伸閱讀】

【茶包射手日記】網頁 Enter 鍵誤擊刪除鈕疑雲

$
0
0

同事報案,在「以 MVVM 清單實作資料編輯介面」的經典應用場景(Knockout版範例Angular版範例)遇見怪事。新増一筆資料後,將焦點移至 <input type="text"> 輸入欄位,若按下 Enter 資料會莫名消失,按一次消失一筆…

程式用了 jQuery、Bootstrape、Knockout、KendoUI,加上一堆自訂程式庫,無法斷定是誰造成,只好抽絲剝繭,以能重現問題為原則,將掛載的程式庫及 DOM 元素一一拆除。歷經一番功夫,最後竟發現是個 HTML 基本觀念,某自以為資深的網頁設計老鳥,乖乖上了一課。

用一個超精簡範例重現問題:Live Demo

將焦點停在 Input A,按下 Enter 鍵會觸發 Button A 的 onclick 事件;但同樣狀況則不會發生在 Input B 與 Button B 上,關鍵在於 Input A 與 Button A 被包在 <form></form> 之中,而 Input B / Button B 沒有:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>Enter on form</title>
</head>
<body>
<form>
<fieldset>
<legend>Inside Form</legend>
<inputtype="text"value="Input A"/>
<buttononclick="alert('Button A Clicked');return false;">Button A</button>
</fieldset>
</form>
<fieldset>
<legend>Outside Form</legend>
<inputtype="text"value="Input B"/>
<buttononclick="alert('Button B Clicked')">Button B</button>
</fieldset>
</body>
</html>

追究原因,「在輸入欄位按 Enter 送出表單」幾乎是所有瀏覽器的預設行為(感覺是網頁設計基本常識,但先前以寫 AJAX 跟 SPA 為主,碰 Form 的經驗不夠多,沒遇過還真就沒學到),Enter 送出表單可以理解,但瀏覽器送出前還幫忙按下 <form> 裡的第一顆按鈕倒是出乎意料。(在以上範例,Button A onclick alert 完要 return false,不然會觸發表單送出行為)再回頭觀察一開始的展示,按 Enter 被刪除的永遠是第一筆,由此可證。

至於解決方案,用 prevent enter submit form 可以 Google 到一大票詢問與討論,常見做法是攔截 document 或 input 的 keypress 事件,在遇到 Enter 鍵時取消動作。但本案例倒不用這麼麻煩,該段程式以 AJAX 方式運作,用不到 <form> ,是因為寫在 ASP.NET WebForm 才被包在 <form> 中,只需將該段 HTML 移至 <form> 之外,問題即刻消失。

撇開這次遇到的特殊狀況,回到「按 Enter 會送出 Form 」行為上,其實存在爭議。Stackoverflow 上有一堆人詢問如何取消,卻也有人強力主張不該改掉(The Enter Key should Submit Forms, Stop Suppressing it),我則喜歡 StackExchange 的這篇觀點,作者以 Facebook 提供 Press Enter to send 選項為例(過去式 ,現已改版),認為交由使用者決定才是最佳選擇,附議!

CODE - 數字轉英文 C# 函式庫

$
0
0

專案遇上數字轉成英文的需求,例如:1234 需轉為 one thousand two hundred thirty four。

網路上有許多解決方案,程式範例、現成函式庫都有,選擇眾多之下,透過 NuGet 即裝即用才是王道。我找到一個,在 NuGet 搜尋 NUT,第一筆 Nut, Number To Text Converter & Money To Text Converter:

程式為 Open Source, 使用說明與原始碼在 Github 上: https://github.com/emrahyumuk/NUT-number-to-text。使用說明略嫌簡略,「Use the source, Luke!」 所有的疑問看完程式碼全明白了。

Nut 程式庫在 long、int 及 decimal 加了擴充方法(所以要記得 using Nut),透過 ToText() 即可數字轉英文字:

using Nut;
using System;
 
namespace N2T
{
class Program
    {
staticvoid Main(string[] args)
        {
int i = 123456;
            Console.WriteLine(i.ToText("en"));
 
decimal n = 123456.78m;
            Console.WriteLine(n.ToText(Nut.Currency.USD, "en"));
            Console.Read();
       }
    }
}

測試結果如下:
one hundred twenty three thousand four hundred fifty six
one hundred twenty three thousand four hundred fifty six dollars seventy eight cents

轉換整數時 ToText() 可傳入語系參數轉成不同語言,程式支援英文、法文、俄文、西班牙文、土耳其文跟烏克蘭文。語系預設為英文,要轉英文不傳參數也可。含小數數字轉英文多用於表示金額,數字需用 decimal 型別,第一個參數傳入幣別決定幣值單元,例如美元是 dollar/cent、歐元是 euro/eurocent、俄羅斯是 ruble/kopek … 等,第二個參數可傳入語系。另外還有一些進階選項可設定第一個字母要不要大寫、零要不要顯示… 等,Github 原始碼附了一個 Nut.Demo 專案貼心提供選項展示及試玩。如果用得不開心,Fork 專案改成你要的樣子吧,充分體現 Open Source 精神~

愈來愈感受到 Github、NuGet 已讓程式開發產生變革,當常見需求幾乎都能快速找到現成且可修改的解決方案,排除這些瑣碎需求的牽絆,開發者更能專注核心商業邏輯,開發節奏就加快了。但負面效應也隨之而來-愈來愈短的開發時程要求、愈來愈難開口說「我做不出來」、後浪挾資訊流通之便來勢洶洶… 嗯,開發生涯好像也變幸福耶,哈!

【同場加映】阿拉伯數字轉中文大寫


部落格 1000 萬次點閱紀念暨抽獎活動

$
0
0

(灑花灑花再灑花)

2004 起開始寫部落格,2006/4/15 啟用 statcounter 計數器,歷經 4096 天,部落格點閱數正式突破 1000 萬次囉!

部落格一寫 13 年,熱血青年已成老鹹魚,學習能力與鑽研熱情早不復當年,與年輕小新肝們相比天差地別,唯一能佔便宜只剩多點經驗。算算這輩子是定型了,不是當將領的材料,頂多當個身經百戰的老士官長,沒本事帶兵攻城掠地,面對戰場種種倒是駕輕就熟,水坑沼澤獨木橋,三行四進挖戰壕,掃雷爆破拼刺刀,生火包紮吃野草,什麼挑戰都難不倒,是那種登場後能撐很久不領便當的角色,哈。也因為如此,近年來部落格少有高深的東西,多是瑣碎的知識經驗分享(其實很大一部分是為自己所寫,擔心下回遇上想不起來),謝謝大家對老士官長不離不棄一路相挺,分享之餘還常從讀者朋友們的回饋學到更多,在此致謝。

依照慣例,慶祝里程碑達成就,抽獎是一定要的。上回 Facebook 粉絲專頁破萬推出的爛木頭黑話紀念書籤佳評如潮,據聞黑市價格飆破兩百八十萬,有幸運得主為此換車買房,這次想不出梗再推出升級版紀念書籤,採不鏽鋼材質電蝕刻精印(電蝕刻做法可參考黃色小鴨除錯之原力升級版),保用百年金剛不壞,書都爛光了書籤還是好的!(謎:強調這點的用意是?)

前後試了幾種做法(其實是自己想玩),包含請廠商製作絹版模版、卡典西德雷射割字、感光藍油曝光顯影… 試了十來次,效果都不理想,猜想電解液用食鹽水太簡陋,追求效果恐得回歸腐蝕性或毒性較強的專業化學配方,但既然要彰顯土砲精神,就保持它質樸有缺陷的樣子吧!本次預計送出五枚書籤,片片不一致,個個有瑕疵,品質之荒唐令人瞠目結舌,但大家肯定能感受創作者想傳達的意念:每個人在世間都是獨一無二的,沒有任何人是完美的… 是充滿人文情懷的深刻作品~(謎:哇塞!好敢講,分明只是粗製濫造的破鐵片)

抽獎方式遵循古法(公平公正公開電腦抽獎法),想參加的捧油請在臉書抽獎貼文留言「抽」或「+1」報名~(以臉書留言為準,部落格文章留言恕不計入)報名截止時間(D日T時)以 JavaScript 取出名單,以「最新動態」排序,將名單送入程式輸入 D 日台股指數當亂數種子,取出 5 位得獎者。(照片中第一排左起為1、2,中間為3,最下排左起為4、5)

PS1:獎品很鳥,就不請律師公證了,大會保留任意修改及解釋抽獎規則的權利(靠,有沒有這麼蠻橫?),如果擔心規則不公權益受損,請勿參加,不要為了幾塊破鐵片受氣。
PS2:屆時將透過粉專訊息連絡中獎者,由於獎品將採郵寄方式,依慣例若中獎者住在海外、外星球或其他銀河系,恕只代寄到指定的台灣住址。

【成長歷程】

Visual Studio 開啟專案出現 SQL Server Express 未安裝警告

$
0
0

在公司開啟某些工作專案時,我的 Visaul Studio 常會彈出像這樣的警告訊息:

The Web project 'MyWeb' requires SQL Server Express LocalDB, whick is not installed on this computer.

To upgrade the project database to use latest SQL Server Express LocalDB, double-click the database file and follow the instructions. Note: After this upgrade, the project database can't be modified using earlier verions of Visual Studio.

大意是警告我這個網站專案需要 SQL Server Express,但我沒有安裝,要不就要升級新版,要不就安裝舊版… 然而,該網站所有資料都放在 SQL Server 或 Oracle,即使沒有 SQL Server Express 也可正常運作沒半點問題,就只是每次開專案會跳出來煩人警示。之前沒想太多點掉就算,但心中不免嘀咕,怎麼沒人嫌煩處理一下?(謎:巴望別人解決問題?說好的射手魂呢?)

最新接手的 sln裡面有三個專案都有這問題,每次開解決方案要點三次,加上初期編譯有誤需要反覆重開測試,於是惰性再強也壓不住竄升的煩躁感,驅使我尋找解決方法。

關鍵在 web.config 的這段連線字串,拿掉問題就解了!(前題:如果你沒有用到 ASP.NET 內建的使用者註冊及角色管理的話)

<connectionStrings>
  <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=aspnet-MYWEB-20120807153209;Integrated Security=SSPI" />

至於背後的故事,應該是 VS2010 或 VS2012 時代(依據問題專案的建立年代推敲)網站專案樣版預設加入的設定,供 ASP.NET Membership 機制使用(即使網站採 Windows 驗證)。VS2013/VS2015/VS2017 在開啟專案時一旦偵測到連線字串出現.\SQLEXPRESS 而主機沒裝,便會彈出警示。接著,我也想到為什麼大家不覺困擾,沒人處理-我重裝電腦後沒再安裝 VS2010/VS2012,同事們多半還有安裝較早 VS 版本,根本沒這問題,哈!

Windows 10 搜尋問題排除經驗二則

$
0
0

工作機由 Windows 8.1 升級至 Windows 10,過程挺順利,原本安裝的應用程式、環境設定幾乎都無痛移轉,午休時間升級完畢,下午打開 Visual Studio 就接著上工,算是一次良好體驗。(升級軟體有賺有賠,別人成功不保證你不會踩雷,升級前請詳閱公開說明書)

但搜尋上遇到一點小問題。

升級後手癢,想說新環境就該重新規劃一下磁碟分配,把 Windows 搜尋的索引位置從 C:\ProgramData\Microsoft 移到 X:\ProgramData\Microsoft,節省寶貴的 C: SSD 空間,又順手重建索引。

沒想索引重建後遇到狀況,Windows 搜索輸入關鍵字只能查到包含關鍵字的一般檔案,找不到已裝的應用程式(例如:Word、Excel…)。爬文得知,Windows 有個少為人知的資料夾,C:\ProgramData\Microsoft\Windows\Start Menu\ ,軟體安裝時會在其中建立資料夾跟程式捷徑,就是開始選單「程式集」項目的依據。而 Windows 搜尋可以找到已安裝的軟體,關鍵就在將 Start Menu 目錄納入索引範圍。但反覆調整索引項目、重建索引、停用再啟用 Windows 搜尋服務,甚至最後只留 Start Menu 索引,查不到就是查不到:

在某篇文章有人提到「重開機」… 是的,重開機後問題就消失了,白白浪費這麼多寶貴青春(如果我還有的話),筆記起來,下回處理索引問題要納入 SOP。

接著我遇到第二個問題,Windows 索引設定不認得 Outlook 2016 郵件,在選取位置上顯示為 mapi16://{S-1-5-21-32786… 格式的系統代碼:

爬文得知 mapi16 是 Outlook 自訂 Protocol,故問題應出在 Windows 不認得這個協定。網路上眾說紛云,重建索引、重裝 Outlook、重建郵件設定檔(Profile)、修復 Outlook、重裝 OS… 都有。我先選成本最低的修復 Office 2016 安裝,再「重開機」(這次有記住),重開機後在索引範圍看到的仍是 mapi16://{S-1-5-21-32786…,心灰意冷之餘開始亂試,取消勾選後再開啟設定 UI 發現 mapi16://{S-1-5-21-32786… 項目消失,又做了幾次,Microsoft Outlook 項目就莫名出現~

不知其所然,但還是留個記錄供遇到類似狀況的捧油參考。

最後補充一則發現,從 Outlook 2013 起移除直接使用 Windows Search 搜尋郵件的功能,Outlook 項目不會顯示在 Windows Shell 搜尋中 (例如,從 [開始] 功能表搜尋,或使用 Win+F),必須在 Outlook 中執行搜尋,覺得可惜但也只能接受。

關於 Shared Memory 的兩三事

$
0
0

參與古老系統的搬遷工程,其中使用 Shared Memory 實現跨 Process 溝通(例如:ASP.NET 呼叫 Window Service),也因而被迫了解這門對 .NET 開發者偏冷門的技術,特筆記備忘。

【Shared Memory 是什麼?】

跨 Process 溝通有個術語,Interprocess Communictaion(IPC),在 Windows 平台有以下選擇:參考

  • Clipboard
    程式 A 將内容貼進剪貼簿,程式 B 自剪貼簿取出内容。
  • COM
    OLE 複合文件(Compound Document)讓 Word 文件可以內嵌 Excel 工作表,點兩下還能叫出 Excel 進行編輯, OLE 的基礎為 COM 元件技術。
  • Data Copy
    程序 A 向程式 B 依約定的格式内容傳送 WM_COPYDATA 訊息
  • DDE
    DDE 是一種允許不同應用程式交換不同格式資料的通訊協定,可視為剪貼簿的沿伸,除了一次性抛轉,還能持續傳輸資料。(效能相對差,已不建議使用)
  • File Mapping
    File Mapping 意指將檔案模擬成 Process 中的一塊記憶體,當多個應用程式間透過共用 File Mapping 交換資料,稱之為 Named Shared Memory,在各種 IPC 方法中效能最佳,但必須透過 Mutex等同步機制防止讀寫衝突。
  • Mailslots
    單向溝通,Mailslot Client 送訊息給 Mailslot Server,訊息在 Server 讀取後删除,支援跨機器傳送,還可一對多廣播。(廣播訊息長度限制 400 bytes,一對一傳輸時訊息長度則由 Mailslot Server 建立時決定)
  • Pipes
    雙向傳輸,分為 Anonymous Pipe 及 Named Pipe。Anonymous Pipe 一般用於父程序與子程序間的標準輸入/輸出導向,雙向溝通要建兩條 Pipe,不能跨網路且限於有從屬關係的 Process;Named Pipe 則可用於任意 Process 間交換資料,並支援跨網路 Process 間傳輸。
  • RPC
    Remote Procedure Call(RPC) 允許應用程式呼叫其他應用程式提供的函式功能,並可跨網路呼叫。Windows RPC 符合 ISO DCE 標準,支援跨作業系統系統整合。
  • Windows Sockets
    基於 TCP/IP 或其他網路協定制訂的抽象通訊介面,底層透過網路連線進行資料交換。

Shared Memory 是 C/C++ 開發者常用的資料交換方式( Google 可以查到很多在 Linux 用 Shared Memory 實現 IPC 的範例),故 C/C++ 開發者在 Windows 平台也常選擇它做為溝通管道。

【Shared Memory 實作練習】

雖然用的人較少,但 .NET 內建 System.IO.MemoryMappedFiles 命名空間,要玩 Shared Memory 不是難事,幾乎跟操作檔案沒什麼兩樣,只要有 FileStream 相關操作經驗很快就上手,而我參考 MSND 範例也簡單練習。

我寫了兩隻程式,ProcessA 透過 MemoryMappedFile.CreateNew() 建立大小為 1024 Bytes 的空間,與另一隻 ProcessB 練習傳接球。由於 1024 Bytes 兩隻程式共用,我將前 512 規劃為 ProcessA 寫入 ProcessB 讀取,後 512 則是 ProcessB 寫 ProcessA 讀,程式中使用 CreateViewStream 傳入起始位址及長度指向自己專屬的區域。為了避免 ProcessA 及 ProcessB 存取 MemoryMappedFile 時出現讀寫衝突,我使用 Mutex 鎖定控管單一時間只有一個 Process 可以存取 MemoryMappedFile。測試過程為 ProcessA 建立 MemoryMappedFile,寫入訊息字串 –> ProcessB 讀取訊息字串並寫入回應字串 –> ProcessA 讀取回應字串,結束。

ProcessA 程式如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace ProcessA
{
class Program
    {
staticvoid Main(string[] args)
        {
//REF: https://msdn.microsoft.com/en-us/library/dd267552(v=vs.110).aspx
using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("DARKTHREAD", 1024))
            {
bool mutexCreated;
                Mutex mutex = new Mutex(true, "DarkthreadSharedMem", out mutexCreated);
using (var stream = mmf.CreateViewStream()) {
byte[] msg = Encoding.UTF8.GetBytes("Hello, World!");
using (BinaryWriter bw = new BinaryWriter(stream))
                    {
                        bw.Write(msg.Length); //先寫Length
                        bw.Write(msg); //再寫byte[]
                    }
                }
                mutex.ReleaseMutex();
                Console.Write("操作 Process B 進行讀取及回應,完成後按Enter");
                Console.ReadLine();
 
                mutex.WaitOne();
using (MemoryMappedViewStream stream = mmf.CreateViewStream(512, 512))
                {
using (var br = new BinaryReader(stream))
                    {
//先讀取長度,再讀取内容
                        var len = br.ReadInt32();
                        var msg = Encoding.UTF8.GetString(br.ReadBytes(len), 0, len);
                        Console.WriteLine($"回應={msg}");
                    }
                }
                mutex.ReleaseMutex();
                Console.ReadLine();
            }
        }
    }
}

ProcessB 程式如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace ProcessB
{
class Program
    {
staticvoid Main(string[] args)
        {
try
            {
                Console.Write("按 Enter 開始讀取及回應…");
                Console.ReadLine();
using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("DARKTHREAD"))
                {
                    Mutex mutex = Mutex.OpenExisting("DarkthreadSharedMem");
                    mutex.WaitOne();                    
using (MemoryMappedViewStream stream = mmf.CreateViewStream(0, 0))
                    {
using (var br = new BinaryReader(stream))
                        {
//先讀取長度,再讀取内容
                            var len = br.ReadInt32();
                            var word = Encoding.UTF8.GetString(br.ReadBytes(len), 0, len);
                            Console.WriteLine($"訊息={word}");
                        }
                    }
using (MemoryMappedViewStream stream = mmf.CreateViewStream(512, 512))
                    {
using (var bw = new BinaryWriter(stream))
                        {
                            var msg = Encoding.UTF8.GetBytes("朕知道了");
                            bw.Write(msg.Length);
                            bw.Write(msg);
                        }
                    }
                    mutex.ReleaseMutex();
                }
                Console.ReadLine();
            }
catch (FileNotFoundException)
            {
                Console.WriteLine("Memory-mapped file does not exist.");
            }
        }
    }
}

測試成功!

【補充技巧】

  1. 如何檢視 Windows 目前已開啟的 MemoryMappedFile?
    SystemInternals 有個 AccessChk 工具能列出 Windows 所有可存取的檔案、資料夾、Registry、物件以及 Windows 服務。而 MemoryMappedFile 屬於一種 Windows 物件,使用以下指令可列出所有物件並存檔
    accesschk -osv > e:\objList.txt
    在其中尋找 MemoryMappedFile 名稱,若存在可看到類似以下記錄:
    \Sessions\1\BaseNamedObjects\DARKTHREAD
      Type: Section
      Medium Mandatory Level (Default) [No-Write-Up]
      RW NT AUTHORITY\SYSTEM
        SECTION_ALL_ACCESS
      RW DOMAIN\UserName
        SECTION_ALL_ACCESS
      RW DOMAIN\UserName-S-1-5-5-0-954410
        SECTION_ALL_ACCESS
  2. MemoryMappedFile 預設是開在使用者的 Session 中,預設無法跨 Session 使用。例如:兩個分屬不同 AppPool 的 ASP.NET 若執行身分不同,即使 MemoryMappedFile 名稱相同也是各自一份,故運用時需確認溝通雙方使用的執行身分相同。
  3. 若要跨不同執行身分溝通,MemoryMappedFile 可命名為 "Global\Filename"(注意 Global 大小寫有別,我踩到誤寫為GLOBAL 路徑無效的雷),如此可跨執行身分存取。
    但需要注意,Session 0 (Windows Service)以外的 Process 需要具有 SeCreateGlobalPrivilege 權限才能建立 Global\… MemoryMappedFile。(MSDN文件
    關於 Session 0,可參考對岸 MVP 的這篇文章 - 穿透Session 0 隔离(一)裡面有蠻詳細的介紹。

【茶包射手筆記】IIS 設定順序導致 500.19 錯誤

$
0
0

HTTP 500.19 錯誤多因 ASP.NET 父網站與子網站因繼承關係導致設定項目重複,過去曾經歷幾次(IIS 7限制IP存取的設定錯誤怪異的web.config HttpHandler重複錯誤),今天再遇到 IP 限制設定重複導致子網站掛點的狀況,同事發誓一切操作合情合理,想想上回 IIS 7 IP 限制設定打架案例沒有逆天亂搞照樣出錯,推測其中有雷,決定現場模擬還原真相。

我在 IIS 設定一獨立站台 Test,其下加入 Child 子網站。父網站的 index.html 以 IFrame 內嵌 Child/index.html 進行測試。

透過 IIS 管理介面設定 Test 站台拒絕未指定的 IP 用戶端。


接著在 Test 站台設定允許 IP ::1(IPv6 的 Localhost)

檢視 Child 子網站的 IP 位址及網域限制,可發現已自動加上 ::1,這是繼承自 Test 站台的設定。

接著在 Child 另外加入一筆 127.0.0.1,此時瀏覽 localhost:7611 仍正常。

如果我們在 Test 站台也加入 127.0.0.1,就會變成以下德行,重現今天遇到的狀況:

Child/index.html 詳細錯誤如下:

HTTP 錯誤 500.19 - Internal Server Error
無法存取要求的網頁,因為與該網頁相關的設定資料不正確。

詳細錯誤資訊:
模組       IpRestrictionModule
通知       BeginRequest
處理常式       ExtensionlessUrlHandler-Integrated-4.0
錯誤碼       0x800700b7
設定錯誤       在複合金鑰屬性 'ipAddress, subnetMask, domainName' 分別設為 '127.0.0.1, 255.255.255.255, ' 的情況下,無法新增類型 'add' 的重複集合項目
設定檔案       \\?\C:\inetpub\temp\apppools\Test\Test.config
要求的 URL      
http://localhost:7611/Child/
實體路徑       X:\WWW\Child\
登入方法       尚未判定
登入使用者       尚未判定

設定來源:
  835:         <ipSecurity>
  836:           <add ipAddress="127.0.0.1" allowed="true" />
  837:         </ipSecurity>

詳細資訊:
如果讀取網頁伺服器或 Web 應用程式的設定檔案發生問題,此時就會發生這種錯誤。在某些情況下,事件記錄可能會包含何種原因造成這項錯誤的詳細資訊。
檢視詳細資訊 »

由此可知:

先在子網站加入限制 IP,再到父網站加入相同限制 IP,就會因繼承關係造成子網站設定重複,觸發 500.19 錯誤!

發生問題後,子網站的「IP 位址及網域限制」已無法開啟,除了直接修改 IIS config 檔,必須先移除父網站的重複 IP 設定,才能使用 IIS 管理介面重設子網站的 IP 限制。

結論是 IIS 缺少防呆,無法防範或忽略繼承設定與子網站設定重複的狀況,只能操作時多加留意。

【茶包射手筆記】SQL 錯誤-Server 'XXX' is not configured for RPC

$
0
0

在測試台運作正常,程式部署到正式環境後出現 SQL 錯誤:

System.Data.SqlClient.SqlException (0x80131904): Server 'XXX' is not configured for RPC.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at System.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
at System.Data.SqlClient.SqlDataReader.get_MetaData()
at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)

追查程式,錯誤點在以下這段呼叫 Linked Server 端 Stored Procedure 的 SQL 指令: (XXX 為 Linked Server 名稱)

EXECUTE ('CALL PKG_BLAH.SP_BLOO(?,?,?)', @P1, @P2, @Result OUTPUT) AT [XXX];

爬文查到此與 Linked Server 的 RPC Out 選項被設成 False有關。比對正式與測試 SQL Server,的確正式台 RPC Out 設 False,測試台設 True,將測試台設定改成 False 則可重現錯誤。

鐵證如山,調整正式台 RPC Out 設定為 True 後,問題排除。

最後補充,冷門選項 RPC 與 RPC Out 是什麼鬼?

依據 MSDN Blog What is the RPC and RPC Out option on a SQL Server linked-server- – Jason's Technical Topics

RPC 用於古老的 Remote Server 功能(Linked Server 的前身),SQL 2005 之後幾已絕跡,可直接放生。

至於 RPC Out,則與 Linked Server 有關,「呼叫 Linked Server 上的遠端 Stored Proceure」可視同 RPC (Remote Procedure Call),有兩種寫法:

  • EXEC [myserver].master.dbo.sp_helpdb
  • EXEC (‘master.dbo.sp_helpdb’) AT myserver

SQL 預設封鎖這類遠端 Stored Procedure 呼叫,必須將 RPC Out 設為 True 才放行,否則就如本案例,將產生 Msg 7411, Level 16, State 1, Line 1  Server ‘myserver’ is not configured for RPC. 錯誤。

猜想是基於安全考量,SQL 預設封閉未用到的管道以降低風險,故若系統包含呼叫 Linked Server 端 Stored Procedure 的情境,記得要解除封印。

【茶包射手筆記】詭異的 Word 字元疊疊樂

$
0
0

同事遇到一個 Word 鬼問題,某份使用者提供的 Word 文件,有段文字難以修改,輸入的文字會消失或重疊在一起!

做了一個範例重現問題。如下圖所示,文件原本有個字母 T,將游標移到 T 的後方,試著輸入 A、B、C、D、E 字元,看到的不是TABCDE,而是全部字元都疊在一起!

經過一番摸索,發現問題出在這段內容的「字型/字元間距/間距」設定被設成「緊縮」(預設為標準),並指定點數為 7.5 點,將其改回標準即一切正常。

順手測了緊縮不同點數的效果:

基本上 2 點已是極限,超過則文字重疊難以閱讀,還會導致後方即使輸入正常間距文字也出現字元被吃掉的詭異現象,已經不能算是合理使用。而究竟為什麼拿到的 Word 文件上會出現詭異整人陷阱,原因成謎。

總之,下回遇到 Word 輸入字元重疊或被吃掉的狀況,SOP 應納入字元間距檢查。


CODE-使用 C# 批次列印 PDF 檔案

$
0
0

專案遇到批次列印 PDF 檔需求。

Acrobat Reader 或 Foxit Reader 等常用 PDF 軟體本身就具備傳參數直接列印功能,例如 Acrobat Reader 直接列印 PDF 之語法為:AcroRd32.exe /p /h "pdf路徑" "印表機名稱"(印表機名稱省略時由預設印表機輸出)

基於以上資訊,最直覺的做法是找出 Acrobat Reader EXE 檔(AcroRd32.exe)路徑,在 .NET 程式透過 Process.Start() 傳入 PDF 路徑及 /p /h 參數呼叫 Acrobat Reader 列印檔案。但這個做法有個小缺點,它限制使用者必須安裝特定 PDF 閱讀軟體,再不然程式就得夠彈性,支援各種可列印 PDF 的軟體,如此尋找及識別 PDF 軟體邏輯將複雜化。

在 Stackoverflow 看到一個好方法,由於 Windows 多半會預設 PDF 開啟程式,並且還會註冊開啟、列印等動作,方便使用者透過檔案總管右鍵選單直接列印:

探索其背後原理,是 Acrobat Reader 先在 .pdf 副檔名註冊 UserChoice/ProgId = AcroExch.Document.11

而 AcroExch.Document.11 註冊了 Print/Command 對應到先前說過的列印指令: AcroRd32.exe /p /h "%1":

透過以上 Registry,當我們對 PDF 檔下達 Print Verb 時,Windows 便會找到對應程式並執行列印,不管它是 Acrobat Reader 還是 Foxit Reader,遠比指定並尋找特定軟體的做法更具彈性。以下為 Stackoverflow 找到的範例程式:

privatevoid SendToPrinter()
{
   ProcessStartInfo info = new ProcessStartInfo();
   info.Verb = "print";
   info.FileName = @"c:\output.pdf";
   info.CreateNoWindow = true;
   info.WindowStyle = ProcessWindowStyle.Hidden;
 
   Process p = new Process();
   p.StartInfo = info;
   p.Start();
 
   p.WaitForInputIdle();
   System.Threading.Thread.Sleep(3000);
if (false == p.CloseMainWindow())
      p.Kill();
}

仿照上述方法寫好第一版,丟給使用者測試後馬上被打槍-程式在列印多頁報表時會掉頁,例如 6 頁只印完 4 頁就沒了。

推敲其原因,由於 AcroRd32 非標準的命令列程式,無法等待程式執行結束,啟動程式後控制權即回到呼叫端,故範例程式的做法是等待三秒,假設文件已列印完畢即強制關閉 PDF 程式,造成 AcroRd32 6 頁只列了 4 頁就被關掉的狀況。(飄向北方才唱到咀嚼爆肚涮羊就被卡歌來著)

把 3 秒等待時間加長是種鋸箭做法,但魔術數字註定要糾結於「空等 vs 不足」的兩難。最後,我想出一個好方法-監測列印佇列(PrintQueue)。呼叫 AcroRd32 後先等待列印文件出現在 PrintQueue,再等待其列印完畢從佇列消失,最長等待時間則拉長到 180 秒,確保每個 PDF 都印好印滿,如此既沒有無謂等待,也沒有過早中止程式掉頁風險,新做法美妙到我想為自己起立鼓掌 XD(捻鬚而笑)

完整程式範例如下供大家參考:

 
 
//REF:https://stackoverflow.com/a/6106155/288936
publicstaticvoid Print(string filePath)
{
    Status = PrintJobStatus.Printing;
    Message = string.Empty;
try
    {
        logger.Debug($"Printing... {filePath}");
        ProcessStartInfo info = new ProcessStartInfo();
        info.Verb = "print";
        info.FileName = filePath;
        info.CreateNoWindow = true;
        info.WindowStyle = ProcessWindowStyle.Hidden;
 
        Process p = new Process();
        p.StartInfo = info;
        p.Start();
 
        p.WaitForInputIdle();
//以下邏輯克服無法得知Acrobat Reader或Foxit Reader是否列印完成的問題
//最多等待180秒(假設所有檔案可在3分鐘內印完)
        var timeOut = DateTime.Now.AddSeconds(180);
bool printing = false; //是否開始列印
bool done = false; //是否列印完成
//取純檔名部分,跟PrintQueue進行比對
string pureFileName = Path.GetFileName(filePath);
//限定最大等待時間
while (DateTime.Now.CompareTo(timeOut) < 0)
        {
if (!printing)
            {
//未開始列印前發現檔名相同的列印工作
if (CheckPrintQueue(pureFileName))
                {
                    printing = true;
                    Console.WriteLine($"[{pureFileName}]列印中...");
                }
            }
else
            {
//已開始列印後,同檔名列印工作消失表示列印完成
if (!CheckPrintQueue(pureFileName))
                {
                    done = true;
                    Console.WriteLine($"[{pureFileName}]列印完成");
break;
                }
            }
            System.Threading.Thread.Sleep(100);
        }
try
        {
//若程序尚未關閉,強制關閉之
if (false == p.CloseMainWindow())
                p.Kill();
        }
catch
        {
        }
if (!done)
        {
            Console.WriteLine($"無法確認報表[{pureFileName}]列印狀態!");
        }
    }
catch (Exception ex)
    {
        Console.WriteLine($"Error: {DateTime.Now:HH:mm:ss} {ex.Message}");
    }
}
 
//需查詢 WMI 記得加入參照及 using System.Management; 
privatestaticbool CheckPrintQueue(string file)
{
//尋找PrintQueue有沒有檔案相同的列印工作
string searchQuery =
"SELECT * FROM Win32_PrintJob";
    var printJobs =
new ManagementObjectSearcher(searchQuery).Get();
return printJobs.Any(o => (string)o.Properties["Document"].Value == file);
}

SQLite 批次 INSERT 的蝸牛陷阱

$
0
0

假日轉檯寫 Coding4Fun 專案,本週的 Scrum Sprint Planning Meeting 我認領的工作是將 13 萬英文單字轉入 SQLite 資料庫(謎:認領?快醒醒,這專案從頭到尾只有你一個人吧?)。

心想這有什麼難,涮涮涮寫好以下程式,沒想到其執行速度之慢,嚇得我屁滾尿流失了魂…

using (var cnSqlite = new SQLiteConnection(csSqlite))
{
    cnSqlite.Open();
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var totalCount = list.Count;
    var index = 0;
foreach (var voc in list)
    {
        Console.WriteLine(
    $"{index++}/{totalCount}({index * 100.0 / totalCount:n1}%) {voc.Word}");
        cnSqlite.Execute(
"INSERT INTO Dictionary VALUES(@Word, @KKSymbol, @Explanation)", (object)voc);
    }
    sw.Stop();
    Console.Write($"Duration={sw.ElapsedMilliseconds:n0}ms");
}

事實上我沒耐心等到 sw.Stop() 觀察總耗時,花了 30 分鐘只 INSERT 完 10% 我就放棄了。換句話說,全部跑完要 5 個小時啊啊啊啊啊~ 這個年代這種速度?我有正在操作古董火砲對抗航母戰鬥群的無力感…

爬文找到文章(Make your SQLite bulk inserts very fast in C# .NET),才知這是 SQLite 的 FAQ

SQLite 一秒最快能完成 50,000 筆以上的 INSERT,但一秒只能完成幾十筆 Transation,依原本寫法,由於每筆 INSERT 動作預設自成一個 Transaction,速度要快也難。

解決方法很簡單,只需加個兩行,將整個迴圈包成一個 Transaction 就搞定。猜看看速度改進多少?

using (var cnSqlite = new SQLiteConnection(csSqlite))
{
    cnSqlite.Open();
    Stopwatch sw = new Stopwatch();
    sw.Start();
using (SQLiteTransaction tran = cnSqlite.BeginTransaction())
    {
        var totalCount = list.Count;
        var index = 0;
foreach (var voc in list)
        {
            Console.WriteLine(
                $"{index++}/{totalCount}({index * 100.0 / totalCount:n1}%) {voc.Word}");
            cnSqlite.Execute(
"INSERT INTO Dictionary VALUES(@Word, @KKSymbol, @Explanation)", (object)voc);
        }
        tran.Commit();
    }
    sw.Stop();
    Console.Write($"Duration={sw.ElapsedMilliseconds:n0}ms");
}

實測結果,132,319 筆 14.887 秒塞完,平均 8,888 筆/秒(這數字巧合也太神奇惹)!跟原本 7 筆/秒相比,速度提升 1200 倍!已筆記。

COALESCE 發生字元設定不符合錯誤

$
0
0

同事報案,Dapper 查詢 ORACLE 時使用 COALESCE()遇到 ORA-12704: character set mismatch(字元設定不符) 錯誤。

我用以下程式成功重現問題:(jefftest2.t 欄位為 NVARCHAR2)

staticvoid Main(string[] args)
        {
using (var cn = new OracleConnection(csStr))
            {
                var list = cn.Query(
"select 1 from jefftest2 where coalesce(t, :text) like '%'",
new { text = "ABC" });
                Console.Write(list.Count());
                Console.Read();
            }
        }

執行結果如下:

有趣的是,將 COALESCE(t, :text) 改成 NVL(t, :text) 就不會出錯。爬文查到應是 COALESCE(NVARCHAR2, VARCHAR2) 前後字串型別不一致,有人想出 N'' || :text 鋸箭小密技。試了一下,還真的有效!

問題來了,為什麼 :text 會變成 VARCHAR2?想起以前遇過類似問題-Dapper+ODP.NET無法寫入Unicode問題,莫非是相同原因?改用 ODP.NET OracleCommand 測試,指定 OracleDbType.NVarchar2,執行正常:

由此推測,八九不離十又是 Dapper 踩中 ODP.NET Bug 的老問題,搬出上回的 Hacking 修補大法

staticvoid FixOdpNetDbTypeStringMapping()
{
    Assembly asm = typeof(OracleConnection).Assembly;
    Type tOraDb_DbTypeTable = asm.GetType("Oracle.ManagedDataAccess.Client.OraDb_DbTypeTable");
    var fldDbTypeMapping = tOraDb_DbTypeTable.GetField("dbTypeToOracleDbTypeMapping",
        BindingFlags.Static | BindingFlags.NonPublic);
int[] mappings = (int[])fldDbTypeMapping.GetValue(null);
    mappings[(int)System.Data.DbType.String] = (int)OracleDbType.NVarchar2;
    fldDbTypeMapping.SetValue(null, mappings);
}
 
staticvoid Main(string[] args)
{
    FixOdpNetDbTypeStringMapping();
using (var cn = new OracleConnection(csStr))
    {
        var list = cn.Query(
"select 1 from jefftest2 where coalesce(t, :text) like '%'",
new { text = "ABC" });
        Console.Write(list.Count());
        Console.Read();
    }
}

問題排除~

依上次研究中,原以為要集滿「Oracle資料庫未採AL32UTF8編碼 + OracleParameter參數型別指定DbType + 內容剛好有ANSI/BIG5難字」三項條件才會踩坑,但依這回經驗,參數用於 COALESCE() 不一定要有難字也會出錯,看起來,未來應該將這個 Hacking 修補納入 Dapper + ODP.NET 開發的 SOP 比較保險。

KB-當 WHERE AND/OR 條件遇上 NULL

$
0
0

在 SQL 世界裡 NULL 性質特殊,行為獨特,過去就曾討論過:

自以為至此對 NULL 認識已足,不料前兩天在 WHERE LIKE AND/OR 情境中遇上 NULL,一時意志動搖陷入迷惘,想必是認知還不夠深刻,再補篇 KB 吧!

先別急著看答案,大家猜猜以下 SQL 指令會得到什麼結果?

select'T1','MATCH'from dual wherenulllike'78'
union
select'T2','MATCH'from dual wherenot(nulllike'78')
union
select'T3','MATCH'from dual wherenot(nulllike'78') and 1 = 1
union
select'T4','MATCH'from dual wherenulllike'78'or 1 = 1

答案只有 T4 符合!

理由是 NULL 不管 LIKE 任何字串,結果不是 True 也不是 False,因此 T1 不成立;加上 NOT,一樣不是 True 也不是 False,故 T2 也不成立。這個無法論斷 True 或 False 的狀態與 1=1(True) 做 AND 運算結果不會成立,與 1=1 (True) 進行 OR 比較則會成立。

爬文找到專業解說,如下表所示,NULL LIKE 所產生既非 True 也不是 False 的狀態術語為 Unknown,Unknown AND True 為 Unknown(T3 的例子),Unknown AND False 為 False;Unknown OR True 為 True(T4 的例子),Unkown OR False 則為 Unknown。

未來遇 AND/OR 配 NULL 情境如信心動搖,速查此表堅定信念。

【茶包射手日記】ORACLE JOIN GROUP BY 子查詢爆慢疑案

$
0
0

接獲同事報案,某段 Oracle 查詢偶發嚴重效能問題。查詢時資料表經由 JOIN 自身的 GROUP BY 子查詢挑出某日期前客戶最新的一筆資料:

SELECT
    ccb.Key1,
    ccb.Key2,
    ccb.Key3,
    ccb.Key4,
    ccb.Key5,
    ccb.Col1,
    ccb.Col2,
    ccb.Col3,
    ccb.Col4,
    --...略...
    ccb.Col20
FROM MyTable ccb
INNERJOIN ( 
SELECT Key0,Key1,Key2, Key3, MAX(Key4) as Key4,Key5
FROM MyTable
WHERE Key4 <= :TDay
GROUPBY Key0,Key2,Key1, Key3, Key5
)  maxb 
ON   ccb.Key0 = maxb.Key0
AND  ccb.Key1 = maxb.Key1
AND  ccb.Key2 = maxb.Key2
AND  ccb.Key3 = maxb.Key3
AND  ccb.Key4 = maxb.Key4
AND  ccb.Key5 = maxb.Key5

MyTable 約 90 萬筆,GROUP BY 子查詢結果約 8000 筆,JOIN 後筆數與子查詢筆數相同。

Key0 到 Key5 有設 Index,先查出 8000 筆,再透過 Index 從 90 萬筆中找出 8000 筆感覺效能不致於離譜,測試正常的執行時間不超過 12 秒,但卻不時會發生數分鐘跑不完導致程式逾時的狀況。

一早接獲報案,優先想到的偵辦方向是檢視執行計劃試著找出瓶頸,立即實測,此時執行時間約十來秒正常,而執行計劃如下:

我對 Oracle 執行計劃沒啥研究,但先 GROUP BY 再 JOIN 的順序很符合我對「JOIN 一個 GROUP BY 子查詢」的理解。

不到一多小時後再測一次,問題出現了!執行時間超過數分鐘,檢視執行計劃,Oracle 給了一個匪夷所思的結果:

先 JOIN 再 GROUP BY 再 JOIN 是什麼鬼啦?而執行成本爆增三倍,應可解釋查詢爆慢的原因。

進一步調查,這一小時間資料有些變化,但筆數差異不大,應不致產生巨大差異。但同事提到一點,在兩次測試間曾執行過 Analyze Estimate Statistics 動作試著更新統計改善 Index 效能。在模擬環境測試,竟意外重現 Analyze 之後執行計劃崩壞查詢爆慢的現象,真是出乎意料的劇情發展!

官方文件則提到:Do not use the COMPUTE and ESTIMATE clauses of ANALYZE to collect optimizer statistics. These clauses are supported for backward compatibility. Instead, use the DBMS_STATS package, which lets you collect statistics in parallel, collect global statistics for partitioned objects, and fine tune your statistics collection in other ways. The cost-based optimizer, which depends upon statistics, will eventually use only statistics that have been collected by DBMS_STATS. 意思是 Analyze 是舊指令,不宜再用,而且它所蒐集資料無助於 Cost-Based Optimizer (只對 VALIDATE or LISTCHAINEDROWS clauses 或 Freelist Blocks 資訊蒐集有效),要更新統計資料改善查詢效能,應一律改用 DBMS_STATS。不過,無效是一回事,我沒有找到任何執行 Analyze 會傷害效能的說法可以佐證,Oracle 為什麼會出現難以理解的執行計劃及執行速度,對我來說是謎。

無法解釋執行計劃崩壞的原因,但我的想法是只要阻止 Oracle 將 JOIN 拉進子查詢瞎攪和,我就不會踩到雷。用 Temp Table 是一招,而我懷念起 SQL 的 CTE (Common Table Expression),爬文後有意外發現,原來 Oracle  9i 起就已支援 WITH … AS 這種寫法(學名叫 Subquery Factoring),早期沒有遞迴功能,但到 11g 時已加上向 SQL 看齊了。

試著將查詢修改如下:

WITH maxb AS (
SELECT Key0,Key1,Key2, Key3, MAX(Key4) as Key4,Key5
FROM MyTable
WHERE Key4 <= :TDay
GROUPBY Key0,Key1,Key2,Key3,Key5
)
SELECT
    ccb.Key1,
    ccb.Key2,
    ccb.Key3,
    ccb.Key4,
    ccb.Key5,
    ccb.Col1,
    ccb.Col2,
    ccb.Col3,
    ccb.Col4,
    --...略...
    ccb.Col20
FROM MyTable ccb INNERJOIN maxb 
ON   ccb.Key0 = maxb.Key0
AND  ccb.Key1 = maxb.Key1
AND  ccb.Key2 = maxb.Key2
AND  ccb.Key3 = maxb.Key3
AND  ccb.Key4 = maxb.Key4
AND  ccb.Key5 = maxb.Key5

經實測,改用這種 CTE 寫法,就能避免 Oracle 惡搞執行計劃~

本次辦案心得:

  • Oracle 從 9i 起就可以寫 CTE(11g 起支援遞迴)是一大發現,有不少複雜查詢可以因此簡化
  • Analyze 指令已是歷史,勿再使用 ,請改用 DBMS_STATS
  • 在某些情境下,Oracle 可能讓 JOIN ( GROUP BY 子查詢)先 JOIN 再 GROUP BY,導致可怕的龜速… (我的老天鵝)
Viewing all 2311 articles
Browse latest View live