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

【茶包射手日記】IIS Log 檔換日問題

$
0
0

線上主機每天產生數百 MB 的 IIS Log,為避免 Log 檔吃光磁碟空間,我們多會安排排程執行壓縮及清理作業,每天將前一天的 Log 壓成 ZIP 檔,再依「原始 Log 檔保留 N 天,壓縮 Log 保存 M 天,超過 M 天移至後線儲存空間」的原則刪除或搬移檔案。

今天發現某台機器的 Log 清理排程每天準時兩點執行,但 IIS Log 壓縮檔全是空包彈,大小只有 22 Bytes,裡面空空如也。

起初以為是 Log 檔過大壓縮失敗,但經驗中用 7Zip 壓過數 GB 的大檔,加上現場實測用相同的 Batch Script 壓縮前一日的 Log 檔並沒有問題。DIR 檢查 IIS Log 所在目錄,看到所有檔案的更新時間是早上八點,頓時開悟。

改測試壓縮今天的 Log (原本的設計是 5/4 02:00 壓縮 5/3 的 Log),成功重現凌晨排程遇到的狀況,今天的 Log 檔 IIS 仍在使用中,會傳回 The process cannot access the file because it is being used by another process 存取錯誤。而遇此錯誤 7Zip 仍會產生不包含任何內容的空白 ZIP 檔,這就是一堆 22 Bytes ZIP 檔的由來。

原因是 IIS Log 的日期算法預設以 UTC 時間為準,對映到台北時區 u_ex180503.log 的記錄範圍是台北時間 5/3 08:00 - 5/4 08:00,故 5/4 02:00 u_ex180503.log 仍在使用中,要等到 5/4 08:00 IIS 改寫入 u_ex180504.log 後才能拿來壓縮。

要解決此一問題,做法有二:

  • 將排程延到 08:00 後執行,但這違背「維護作業應儘可能排在離峰」原則
  • IIS 有個選項「使用本地時間為檔案命名 / Use local time for file naming and rollover」

    勾選後 IIS 會改在台北時間午夜 12 點切換檔案,例如: 台北時間 5/4 00:00:00 起改寫入 u_ex180504.log。但須注意 Log 裡記錄的時間仍會採 UTC 時間,故 5/4 00:00:00 寫入記錄的時間會記為 5/3 16:00:00。

評估後,決定改用本地時間切檔解決,收工。


英文單字朗讀 MP3 DIY

$
0
0

這是「搶救英文大作戰」的副本任務,小閃光的英文字彙慘不忍睹,我想到把單字表轉成朗讀 MP3 的方法,想讓她照三餐服用看看能否起死回生。

查了一下,文字轉 MP3 大家幾乎都是用 Balabolka,它可以朗讀輸入的文字,有眾多調整選項,還能轉成 MP3,十分方便。

簡單試用,得到幾點心得:

  1. 文字轉語音功能事實上來自 Windows 內建的 SAPI (Microsoft Speech API),意思是我也可以自己寫程式做到類似功能。
  2. 我的 Windows 10 繁體中文版內建四種語音(如上圖所示),其中 David 跟 Zira 只看得懂英文,但聲音比較道地自然;Hanhan 小姐有中英雙聲帶,但英語部分機械感偏重,比不上 David 跟 Zira,Tracy 則只會看中文講廣東話。
    【補充】
    關於語音安裝可參考:下載適用於逼真的閱讀程式]、 [閱讀模式中,及 [大聲唸出的聲音 - Office 支援
    Windows 10 新版似乎又增加了更多選項:附錄 C:TTS 語音 - Windows Help
  3. SAPI 也支援第三方語音來源,例如開源的語音合成專案 – Ekho 餘音,會說廣東話、國語、廣東台山話、韶安客家話、藏語、雅言(中國古代通用语)和韓語等語言。
  4. Balabolka 提供命令列工具 balcon,搭配 LAME (MP3編輯工具) 可用批次指令大量產生 MP3
    balcon.exe -n David -s -2 -t "Hi, I am darkthrad" -o --raw | lame -r -s 16 -m m -h – d:\output\darkthread.mp3

經實測,我偏好英文交給 David 唸,國語由 Hanhan 上場的男女混雙組合,Balabolka 有個「外語詞匯」功能,背後是使用 SAPI5 <voice>標籤標註某段文字使用不同語音朗讀,就能實現「英語給 David 唸,國語讓 Hanhan 說」的效果。例如:

<voice required="Name=Microsoft David Desktop">This is a book.</voice> 這是一本書

另外,要在朗讀過程加入較長停頓,我試出來的方法是在英文中插入"...",在中文插入",",用程式讀取字彙表產生加入<voice>標籤的文字檔再交給 Balabolka 輸出,自製的單字朗讀 MP3 就完成了,收工。

展示影片

Coding4Fun–Microsoft Speach API 筆記

$
0
0

前篇文章用 Balabolka 搞定自製英文單字朗讀 MP3,但老讀者們都猜到接下來會發生什麼事... 是的,C# 整合 SAPI 讓電腦講話的練習來了!

原本以為要裝什麼 SDK 或套件,沒想到 .NET 已內建,專案只需參照 System.Speech 就好。

開始前先看一下你的 Windows 裝了哪些語音以及其支援語系:

static void ListInstalledVoices()
{
    var voice = new System.Speech.Synthesis.SpeechSynthesizer();
    voice.GetInstalledVoices()
        .ToList().ForEach((v) =>
        {
            Console.WriteLine(
                v.VoiceInfo.Name + " " +
                v.VoiceInfo.Culture.DisplayName);
        });
    Console.Read();
}

在我的 Windows 10 繁體中文專業版執行結果如下:

Microsoft Hanhan Desktop 中文 (繁體,台灣)
Microsoft Zira Desktop 英文 (美國)
Microsoft Tracy Desktop 中文 (繁體,香港特別行政區)
Microsoft David Desktop 英文 (美國)

依據文件,Windows 10 2018 4 月更新有增加其他語音選項,國語部分加入了 Zhiwei(志偉?) 跟 Yating (雅婷?)。

來個最基本的應用示範,其中包含前篇文章提到的某段文字用不同語音朗讀。要加入 <voice> 標籤有兩種做法,第一種是自己組 SSML再呼叫 SpeakSsml(),但得自己處理 XML namespace 比較繁瑣,另一種做法是使用 PromptBuilder,透過 AppendText() 加入純文字,用 AppendSsmlMarkup() 加入包含 <voice> 等標籤的 SSML 片段,最後將 PromptBuilder 當成參數交給 Speak() 執行,比拼湊 XML 省事也易讀一些。

static void SayHi()
{
    var voice = new System.Speech.Synthesis.SpeechSynthesizer();
    //美語 男聲
    voice.SelectVoice("Microsoft David Desktop");
    voice.Speak("Hi there, I am darkthread.");
    //美語 女聲
    voice.SelectVoice("Microsoft Zira Desktop");
    voice.Speak("Hi there, I am darkthread.");
    //國語
    var pb = new PromptBuilder();
    pb.StartVoice("Microsoft Hanhan Desktop");
    pb.AppendText("大家好,我是黑暗執行緒");
    //https://msdn.microsoft.com/zh-tw/library/hh378418(v=office.14).aspx
    pb.AppendSsmlMarkup("<voice name=\"Microsoft David Desktop\">darkthread</voice>");
    pb.EndVoice();
    voice.Speak(pb);
    //廣東話
    voice.SelectVoice("Microsoft Tracy Desktop");
    voice.Speak("大家好,我是黑暗執行緒");
}

PromptBuilder除了加入 SSML 標籤,還有其他好用的控制選項,例如:

  • AppendAudio()
    插入外部聲音檔(WMA)
  • AppendBreak()
    插入停頓
  • StartParagraph()/EndParagraph()/StartSentence()/EndSentence()
    形成段落跟句子,模擬自然說話的停頓效果,使語音更逼真
  • StartVoice()/EndVoice()
    指定語音名稱,或指定語系、性別、年齡,由 SAPI 挑選適用的語音

除了前面介紹過的 <voice>,SSML 還有一些有用標籤,可做到精細調控:
(參考:Speech Synthesis Markup Language Reference)

  • emphasis
    強調,加重語氣
  • p、s
    標註段落跟句子,讓說話效果更自然逼真
  • phoneme
    可使用特殊音標指定特定字語的發音,例如指定 Zhou 要唸成「趙」
    His name is Mike <phoneme alphabet=""x-microsoft-ups"" ph=""JH AU"">Zhou</phoneme>
    參考:發音標示符號表 Phonetic Alphabet Reference
  • prosody
    指定範圍內文字的音調(pitch)、速度(rate)、音量(volume)
    Your order for <prosody pitch=""+1st"" rate=""-10%"" volume=""50""> eight books </prosody>
  • voice
    指定範圍內文字使用不同語音

想將語音輸出轉成 WAV 檔也很簡單,在 voice.Speak() 之前先加上 voice.SetOutputToWaveFile("x:\\filename.wav"),一行搞定! 如果要轉成 MP3,則需要整合第三方程式庫或直接用 lame.exe 將 .wav 檔轉 .mp3,lame.exe x:\filename.mav x:\filename.mp3,一樣一行搞定最省事。

又到了久違的呼口號時間:

SAPI 真酷! .NET 好威呀!

2018 海山馬

$
0
0

跑馬至今,海山馬是我唯一年年參加不缺席(20134567)的賽事,並留下目前唯一的落馬經驗。為保持全勤記錄,儘管天氣炎熱賽道單調補給平常獎牌無奇,還是報了名。

六點開跑,全馬賽道跟去年一樣,出發向左往城林橋停車場折返回起點 10K,接著繼續往大漢橋折返回起點再 10K,然後依相同路線跑第二趟湊足 42K。

氣象預報晴天少雲,最高溫 31 度,已有心理準備,但七點不到已陽光普照又沒還是讓人挺崩潰。出太陽又無風的天氣把普通全馬推向極限運動,跑來有滿滿的厭世感。沒什麼心情拍照,決定採「不要逗留,能跑就跑,早結束早解脫」戰略。

天空蔚藍,我心賭爛。

此刻唯一的撫慰,僅存賽道上飛揚的馬尾。

全馬人數很少,只有三百多人,稀釋到總長 10 公里的賽道上,前後幾百公尺都沒人的場面不時上演。

懶得拍照,但這幾棵漂亮大樹錯過可惜。籃球場空無一人,這種天氣瘋子才會在外面跑來跑去曬太陽。(在說誰?)

荷花荷花幾月開?五月就開了。

天氣炎熱,每個水站的大水桶成了救命仙丹。後半馬進水站的 SOP 是喝一杯運動飲料、吃一截香蕉或一片西瓜、再舀三杓水把身體淋濕,渾身涼爽打起精神趕往下一個水站淋水。

這次總算振作一點,走路走得不多,守住 SUB5,總排名擠進前 25%。

賽後照例去看了山羊才回家,整理照片時卻發現羊在青我...

公版獎牌是海山馬的傳統。

     

繼前兩場三重馬石碇馬接連發生 30K 之後 GPS 失準,fenix3 這回又在三小時 40 分左右失去 GPS 訊號,之後時好時壞,里程配速大亂,總距離也飄成 47K。

連續三次全馬都出問題,清一色只發生在連續使用 3 個半小時之後,前面的 GPS 則十分精準,平日練跑到 10-20K 也一切正常,這樣很難再用一時秀逗或巧合解釋。(意思是只要跑進 330 就不會有這困擾了,fenix3 對我的期望好高呀 XD) 不像是軟體問題,只能外行地推測,莫非某硬體元件因連續運作過久過熱導致異常?

二話不說,回程就繞去 Garmin 光華服務中心送修。服務中心的先生小姐聽完病情描述也稱奇,看來是罕見案例(那可以用我的名字命名嗎? 例如:達克舒瑞後天耐力缺乏症候群之類的),手錶得住院檢查,這一兩週就順勢休兵保養足底筋膜。(住院前特拍照留念)

IIS HTTP 重新導向功能筆記

$
0
0

將 IIS 網站的特定網址導向其他網址,有幾種做法:

  1. UrlRewrite 模組
    彈性高,支援複雜的轉換規則(可使用 Regular Expression 定義規則),能在使用者未察覺的情況下完成轉換,例如將 /product/book/computer 轉為 /product.aspx?zone=book&catg=computer,提供較友善的網址並增進 SEO。
    但若轉址是因為網站或網頁搬家,需明白告知並建議使用者改用新網址,UrlRewrite 較不適用。
  2. JavaScript 或 HTML 標籤轉址
    在舊網頁加上 location.href = "新網址" 或 <meta http-equiv="refresh" content="0;url=新網址" /> 將使用者導向新網址。缺點是每個舊址要留一個 HTML,瀏覽器需耗消資源載入 DOM 才執行轉址,也未明確告知客戶端應改用新址。
  3. 伺服器端轉址
    即在 ASP.NET 呼叫 Response.Redirect() 或在 ASP.NET MVC return Redirect() / RedirectPermanet()。
    原理是回傳 HTTP 301/302 告知瀏覽器永久或暫時改用新址。301 是宣告舊址作廢以後請改用新址,搜索引擎下回建立索引及計算排名時會以新址取代舊址;302 則是暫時改用新址,未來每次會先回舊址再轉向新址。HTTP 1.1 增加的 307/308 類似 301/302,差別在於只對 GET 導向,POST 時仍回舊址避免回傳資料遺失。
    參考:HTTP Redirect 301, 302 區別及對SEO的影響 @ 符碼記憶 
  4. IIS HTTP 重新導向模組
    效果同伺服器端轉址,好處是只需設定 web.config 不用寫成網頁,且一條設定即可適用整個子目錄。

來看看 HTTP 重新導向怎麼設定。使用前需先確定 IIS 有安裝 HTTP 重新導向(HTTP Redirection):

接著在 IIS 管理員中找到要導向的資料夾進行設定:

套用設定後,IIS 會在該資料夾建立如下的 web.config:

<?xml version="1.0" encoding="UTF-8"?><configuration><system.webServer><httpRedirect enabled="true" destination="/NewPlace" 
          exactDestination="false" childOnly="true" httpResponseStatus="Found" /></system.webServer></configuration>

關於設定介面中的選項,我用以下的資料夾結構示範,假設我們在 Moved 指定重新導向 /NewPlace。


  • 將所有要求重新導向至確切的目的地(而非目的地相對位置)
    exactDestination="true/false"
    勾選時,/Moved/test.html、/Move/SubFolder/test.html 都會被導向 /NewPlace
    如未勾選,/Moved/test.html 導向 /NewPlace/test.html、/Move/SubFolder/test.html 導向 /NewPlace/SubFolder/test.html
  • 只將要求重新導向至此目錄(不是子目錄)中的內容
    childOnly="true/false"
    勾選時,只有 /Moved/test.html 會被導向新址,更下一層子目錄的 /Move/SubFolder/test.html 不會被導向仍可瀏覽

除了透過 IIS 管理介面,我們也可直接修改 web.config 設定重新導向規則,<httpRecdirect> 參數選項可參考官方文件:HTTP Redirects -httpRedirect- - Microsoft Docs另外,<httpRedirect> 還有一些進階應用,例如:ASP 升級 ASPX 時,將所有 *.asp 導向首頁。參考

以上的做法有個小缺點 - 必須為舊網址建立資料夾放置 web.config,雖然視覺化顯示哪些地方存在舊網址避免誤用是好事,但如果你不喜歡新專案冒出一堆無用資料夾,可以試試這招 - 在根目錄的 web.config 中用 <location path="舊址"> 包覆 <httpRedirect> 宣告,就不必實際建立資料夾或檔案也能針對特定路徑進行轉址。

<location path="Moved"><system.webServer><httpRedirect enabled="true" destination="/NewPlace" /></system.webServer></location>

【茶包射手日記】Visual Studio 專案項目 Icon 突變

$
0
0

同事報案,專案有個類別 .cs 的圖示怪怪的,一般 .cs 的圖示應是綠色的 C#,但問題類別卻是個沒見過的文件圖示(下圖黃框處),比對發現是該類別繼承 System.Net.WebClient 造成(註:這麼做是為了修改 WebClient 的 Timeout),隨便新增一個 ClassN.cs,只要繼承 WebClient 圖示馬上變掉,取消繼承就會恢復。

為了搞懂奇怪圖示的意義,我開始搜尋有沒有 Visual Studio Solution Explorer 圖示大全這種東西,爬文很久發現官方文件沒提供這種東西,最後是在 VS Image Library 找到線索,順便整理過程蒐集到的參考資料:

在 Visaul Studio 2015 圖示清單文件(PDF)中比對到這個奇怪圖示,它的名字叫 Component File:

由圖示知道跟 Component 有關,回頭查 Visual Studio 發現我繞了遠路,其實 .csproj 裡就有線索,當 .cs 繼承 WebClient ,該類別 .cs 會多出 <SubType>Component</SubType>:

<ItemGroup><Compile Include="Class1.cs" /><Compile Include="Class2.cs"><SubType>Component</SubType></Compile><Compile Include="Class3.cs" />

即便手動刪掉,開啟或修改該類別後會再自己長出來。更進一步,Visual Studio 還會為它加上特殊設計檢視:

由蒐集到的關鍵字爬文,在 Stackoverflow 找到完整解釋

會加上 <SubType>Component</SubType> 是因為 System.Net.WebClient 繼承了 System.ComponentModel.Component,修改圖示及套用特殊設計 UI 是 Visaul Studio 針對 System.ComponentModel.Component 所加入的邏輯。要避免可在類別加註 [System.ComponentModel.DesignerCategory("Code")]。

經實測,指定 DesignCategoryAttribute 即可恢復正常。

又學到冷知識。

瀏覽器 HTTP 301 導向記錄清除

$
0
0

前篇文章提到 HTTP 重新導向有 301, 302, 307, 308 幾種形式,其中 301/307 為「永久重新導向」,意思是原網址宣告作廢,請客戶端未來一律改連新址。而各家瀏覽器都忠實貫徹這個精神,一旦接獲伺服器回傳 301 將銘記在心,下回使用者再連到該網址,瀏覽器將省略「連接舊址 -> 接收 301/302 轉址 -> 轉往新址」的過程,自行接轉連新網址。

用以下實驗證明。web.config 加入以下設定,指定將 httq://myserver/darkthread 轉址到 http://blog.darkthread.net

<location path="darkthread"><system.webServer><httpRedirect enabled="true" destination="http://blog.darkthread.net"
        exactDestination="true" httpResponseStatus="Permanent" /></system.webServer></location>

啟用設定後,以 Chrome、IE、Edge、Firefox 連上 httq://myserver/darkthread,將會被轉連到本部落格。

接著我們再修改 web.config,將上述設定的 enable="true" 改成 enable="false",或是乾脆將整個 <httpRedirect>設定刪除。這時再用瀏覽器連 httq://myserver/darkthread,將會發現即使設定已停用或刪除,瀏覽器仍記得先前的永久轉址要求。

之前測試專案不小心敲錯新網址,糗了,被瀏覽器記下來,想改都沒得改,於是我被迫研究清除瀏覽器轉址記憶的方法。

改用無痕視窗可避開記憶的轉址設定,一般瀏覽模式如果要抺去記憶,需清理快取或瀏覽歷程,各家做法不一,實測如下:

IE 要清歷程記錄:

Edge 要清快取資料與檔案:

Chrome 記錄在快取圖片和檔案:

Firefox 放在已快取的網頁內容:

自己的瀏覽器還能洗白,但如果給錯新網址的 HTTP 301 是被訪客瀏覽器或搜尋引擎記住,客人都會像變了心的女朋友,再也回不來惹,使用 HTTP 301 務必要謹慎。(有個保險做法是先設 302 運行一陣子,確認沒問題再改成 301。)

SQL Server 使用者定義型別

$
0
0

在同事報案筆錄看到新鮮玩意兒,資料表的某個欄位型別不是 VARCHAR 不是 DATETIME 不是 INT,而是某個沒看過的名稱(如下圖示意),研究了一下,是所謂的使用者定義型別(User Defined Type, UDT),過去只在教材看過,首次觀察到活體。

使用 UDT 有什麼好處?

UDT 可指定型別、資料長度及精確度、是否允許 NULL,還能定義預設值跟規則,定義一次可重複用於多個資料表、Stored Procedure,實現重用性並維持一致性,良好的 UDT 命名能望文生義,具自我說明(Self-Documentation)效果。另外,UDT 整合 SQLCLR 還能用 C# 實現複雜的邏輯,威力強大。(註:整合 SQLCLR 需留意非原生邏輯執行效能問題[案例])

延伸閱讀:

原本以為 UDT 最大的優點應是 -- 當欄位需要放寬,只需修改 UDT,所有使用它的資料表可一次放寬,雖然直覺技術難度甚高,如能做到價值連城。爬文之後發現是我想多了 - UDT 只能 CREATE 跟 DROP,不能 ALTER,如要更改,必須先另建一個 UDT,將逐一修資料表改用型別。參考 12

雖然不如想像中強大,我還是想試試 UDT 的威力,打算建個 UDT 玩玩:

很快地,我又被潑了一盆冷水 - 無法使用 SSMS 管理介面新增 Defaults 跟 Rules(下圖黃色標示處),只能透過 T-SQL 指令建立。
查了文件,SQL Server 未來將不再支援 CREATE DEFAULTCREATE RULE,已不建議使用,應該是 SSMS 不提供操作介面的原因。


由此看來,UDT 僅存型別與長度統一及自我說明的優勢,好處有限,個人對它沒有愛,不推。


Windows 10 連線 USB 數據機發傳真

$
0
0

很久沒有搞電腦搞到一肚子火了,記錄射茶包經過。

時至今日,生活大小事幾乎都能靠 Email、LINE、網站、APP 搞定,但偶爾仍會遇到只收傳真的店家或公司,例如:訂奶茶、傳信用卡授權書... 等。家裡採購雷射印表機時基於體積及成本效益考量,沒選擇有傳真功能的事務機,代價是久久遇到要傳真的場合,就得傷一下腦筋。

小七傳真一張 A4 土匪價 15 元,線上傳真服務便宜又方便,但我的原則是「個資上網能免則免」。評估之後決定花幾百塊買個 USB 數據機,配合 Windows 內建傳真軟體,在家裡插上電話線自己就能發傳真最安全方便。

USB Modem 不貴,白牌產品三百有找,最後我選了一支 Lenovo 的(感覺有廠牌好點,而且如果接 ThinkPad 有問題,客服得負責到底,哈),含運費 360 大洋入手。

接上 USB Modem,Windows 成功自動識別及安裝驅動程式,但顯示裝置為序列埠 COM10,從 Lenovo 官網下載驅動程式更新後變成 USB Modem。

開啟 Windows 傳真和掃描(Windows Fax and Scan),設定好本機傳真(設定說明可參考這篇:HP 和 Compaq 桌上型電腦 - 在 Windows 7 和 Windows Vista 環境下使用電腦傳送傳真 - HP®顧客支持

實測發現問題,發送時文件會被送到預設印表機,操作視窗則卡住數分鐘後彈出操作逾時錯誤,傳真文件也沒出現在送件匣。

還有另一個問題,我注意到多了一台名為 Fax 的印表機,查看印表機內容時卻彈出「這部電腦沒有安裝'Microsoft Shared Fax Driver'印表機驅動程式。除非您安裝了驅動程式,否則無法存取印表機內容,您現在要安裝驅動程式」。同一時間測試接收傳真,USB Modem 發出令人懷念的數據機吟唱,證明驅動程式跟硬體都正常,所以問題只卡在傳送。

 

猜想 Windows 10 是以印表機概念模擬傳真發送行為,Fax 印表機缺少驅動程式應是無法發送傳真的原因。我在網路上有找到第三方提供的 Microsoft Shared Fax Driver 檔案下載,但既然是微軟自家驅動程式,應該 Windows 內建或由官方下載,沒理由安裝來路不明的版本。爬文抱怨找不到 Microsoft Shared Fax Driver 的討論不少,但大多發生在 XP 等舊版 Windows,
畢竟事務機普及又有網路傳真,Windows 10 時代還用 USB Modem 傳真的人已如鳳毛麟角。找到的討論沒什麼標準答案,
不外乎檢查硬體是否故障,確定數據機驅動程式有裝好之類的罐頭建議。還不時看到「你重灌 Windows 試試」之類的回文,
看得我一肚子火,誰都知道重裝治百病,你要來幫我重灌?(搞不定時火氣特大,王藍田上身)

沒什麼頭緒,另外了桌機 Windows 10 Enterprise 試手氣,沒想到順利無比一次 OK,發送傳真會觸發撥號,讓我士氣大振,意味著硬體跟驅動都是沒問題的,純粹是環境因素。而我還發現一個事實 - 在桌機 Windows 並未出現 Fax 印表機。看來 Fax 印表機並非必要,而問題機的 Fax 印表機缺乏驅動程式,則可能是關鍵。手動將 Fax 印表機刪除,問題仍未解決,試著移除 Windows 傳真和掃描軟體再重裝,發現傳送傳真時會跳出以下對話框,回答是會自動新增 Fax 印表機:

新增的 Fax 印表機這回有了驅動程式,可檢視印表機內容:

重新再試一次,問題排除,傳真發送成功。

留下疑點,FAX 印表機是否是必要的?

查到這篇 MS Blog 文章:Installing fax and not seeing the fax printer created (and not getting an error either)。裡面提到若遠端桌面連線殘留 Fax 印表機,則在安裝傳真元件時將不會新增正確的 Fax 印表機也不會有錯,但其狀況跟我不同。文章還提到若不慎將 Fax 印表機刪除,可使用「Install a Local Fax Printer」選單將它加回來,但 Windows 10 早已大幅改版無此選項。 所以,兩台機器不管有無 Fax 印表機都可發傳真是事實,真相成謎,暫且歸入 X 檔案。

非網站 Windows 之 SSL 加密弱點檢測及修補

$
0
0

資安領域深似海,弱點掃描通常是由資安人員或廠商執行,跑工具程式出報告,再依報告進行修補。說起來有點像人體做健檢,但差在拿到的是用火星文寫的健檢報告,隔行如隔山,天曉得怎麼改善? 試想如果你的健檢報告出現一條紅字「TMD 指數低下,免疫力不足,感染 S95 病毒風險偏高」,沒人跟你解釋要怎麼治療,也沒家醫科可以掛號,然後咧?

這回拿到弱點掃描報告有一條:

SSL Medium Strength Cipher Suites Supported
The remote host supports the use of SSL ciphers that offer medium strength encryption. Nessus regards medium strength as any encryption that uses key lengths at least 64 bits and less than 112 bits, or else that uses the 3DES encryption suite.
Note that it is considerably easier to circumvent medium strength encryption if the attacker is on the same physical network.

大意是遠端系統允許使用金鑰長度不夠的 SSL 加密方式(Cipher),防護強度不足有被破解偷窺的風險。參考

乖乖爬文找解藥吧! (補聲暗)

找到一篇文章 讓你的 SSL 更安全 – 移除弱 SSL 加密方式 (Cipher) – I T 練肖喂解釋得挺清楚。不過,我最大疑問是 - 被舉報有問題的幾台機器根本沒開啟 HTTPS,有些甚至連 IIS 都沒裝,為什麼會被挑剔 SSL 加密強度不夠?

再多查一些資料,我才知道除了 IIS,遠端桌面(RDP, Port 3389)也會使用 SSL 加密。

資安人員建議的做法是修改完等固定排程統一重掃,隔天可以看報告有沒有修好。這豈不回到了卡片打孔跑程式的時代,試完不能馬上看結果,身為現代王藍田,我哪受得了?

因此,我想要一個能立即檢測 SSL 弱點的掃描工具,即時回饋問題是否修復,不然多虐心。網路介紹的 SSL 弱點掃描工具多半針對 HTTPS 網站應用,像最多人推的 SSLScan似乎就只能用來檢查網站。(如有錯請指正)

接著,我學到來自 LINUX 世界的強大網路掃描工具 - nmap (簡易教學:Nmap 網路診斷工具基本使用技巧與教學 - G. T. Wang)。而 nmap 最威能之處是它能透過腳本擴充各式檢查,其中 ssl-enum-ciphers可列舉遠端主機支援的 SSL 加密方法,支援協定包含 RDP 3389,就是我在尋覓的利器!

使用方法如下,安裝後執行 nmap -p 3389 --script ssl-enum-ciphers ip_address 程式會列出該主機 RDP 服務所支援的所有 SSL Cipher。

以下是修補前的檢查結果:

C:\Program Files (x86)\Nmap>nmap -p 3389 --script ssl-enum-ciphers 192.168.35.7
Starting Nmap 7.70 ( https://nmap.org ) at 2018-05-16 11:41 ¥x¥_?D·CRE?!
Nmap scan report for 192.168.35.7
Host is up (0.0045s latency).

PORT     STATE SERVICE
3389/tcp open  ms-wbt-server
| ssl-enum-ciphers:
|   TLSv1.1:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|       TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - C
|       TLS_RSA_WITH_RC4_128_MD5 (rsa 2048) - C
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       64-bit block cipher 3DES vulnerable to SWEET32 attack
|       Broken cipher RC4 is deprecated by RFC 7465
|       Ciphersuite uses MD5 for message integrity
|       Weak certificate signature: SHA1
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (dh 1024) - A
|       TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (dh 1024) - A
|       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|       TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - C
|       TLS_RSA_WITH_RC4_128_MD5 (rsa 2048) - C
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       64-bit block cipher 3DES vulnerable to SWEET32 attack
|       Broken cipher RC4 is deprecated by RFC 7465
|       Ciphersuite uses MD5 for message integrity
|       Key exchange (dh 1024) of lower strength than certificate key
|       Weak certificate signature: SHA1
|_  least strength: C

Nmap done: 1 IP address (1 host up) scanned in 3.97 seconds

結果顯示,目前該主機支援的加密方法中共有

TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048)
TLS_RSA_WITH_RC4_128_SHA (rsa 2048)
TLS_RSA_WITH_RC4_128_MD5

三項被列為 C 級,而這就是問題主機被舉發 SSL 加密強度不足的來源。

要改善此一弱點,最有效的做法是透過修改 Registry 停用強度不足的 Cipher。SSL Cipher 相關 Regisry 的官方說明在 Transport Layer Security (TLS) registry settings - Microsoft Docs,若嫌官方文件太長,可以參考這篇 How to disable RC4 and 3DES on Windows Server-

依照文章,我停用了長度為 40,56,128 的 RC4 以及 3DES 四種 Cipher。

停用後,重跑 nmap 可以發現三項等級 C 的弱點消失了,伺服器的 SSL 加密等級升到 A 級,也通過了廠商弱描工具的檢測。(灑花)

C:\Program Files (x86)\Nmap>nmap -p 3389 --script ssl-enum-ciphers 192.168.35.7
Starting Nmap 7.70 ( https://nmap.org ) at 2018-05-16 11:52 ¥x¥_?D·CRE?!
Nmap scan report for 192.168.35.7
Host is up (0.0010s latency).

PORT     STATE SERVICE
3389/tcp open  ms-wbt-server
| ssl-enum-ciphers:
|   TLSv1.1:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       Weak certificate signature: SHA1
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (dh 1024) - A
|       TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (dh 1024) - A
|       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       Key exchange (dh 1024) of lower strength than certificate key
|       Weak certificate signature: SHA1
|_  least strength: A

Nmap done: 1 IP address (1 host up) scanned in 3.55 seconds

長知識了。

土砲版焊接小幫手

$
0
0

看到一篇 8 歲香港小妹妹在 YouTube 分享電子小玩意 DIY 的報導,影片裡的道具很搶眼:


照片來源:專訪 - 香港 8 歲小妹妹 上網教人 DIY 焊接電路 - 香港 UNWIRE.HK 玩生活.樂科技

四支前端裝有夾子像異形觸手的可彎曲塑膠管,夾住電路板喬好角度,雙手專心拿烙鐵跟焊錫就好,這是何等優雅從容呀! 想到我焊接線路常搞到大粒汗小粒汗,雙手要騰出無名指跟小指扶住電路板,物件尺寸較大或角度刁鑽時如同手指做瑜珈,時間久了還會顫抖發麻,超狼狽的。工欲善其事,必先利其器,小妹妹都知道要用專業裝備,我還在用木棍石頭打獵,像話嗎?

爬文查到這道具有個可愛的名字叫「焊接小幫手」 - 【好物開箱】DIY焊接工具 - 進階實用組 (下篇) - Building Maker Economy:自造達人社群-媒體-平台,台灣有售但價格不斐。那,不如自己做一組吧~

要準備的材料有:

  • 萬向曲管 / 萬向噴油管 2分管(1/4吋) 28公分長(比鄉民略短) 45元/支 (購自網拍)
    這玩意原本用在車床鑽床給水給油噴氣, 角度調整幅度大又可固定不位移,除了造型不賞心悅目,可應用的場合蠻多的。
  • 歐洲夾 5元/支 (光華商場電子材料行)
    歐洲夾較細長,比鱷魚夾好施力,長柄為圓柱狀,材料行的歐洲夾有兩種尺寸,較大的那一段與我買的萬向曲管噴嘴口徑差不多相等,直接插入固定連膠水都省了,呵。
  • 熱縮套管 12元/1米 (光華商場電子材料行)
    用來套在歐洲夾口,防止尖銳鋸齒劃傷物體表面。
  • 基座木板
    原本打算去特力屋切一塊便宜拼接板,意外在路邊拾荒撿到一塊合用的,嘿...
  • 曲管固定座
    3D列印,自製無價
  • 攻牙螺絲 0.5元/根 (五金行都有,我在光南百貨買到,呵)

最麻煩的部分是木板加工,2 分管比想像中粗,工具箱裡最大號鑽頭仍不夠,最後我犠牲了一個大小相符的內六角套筒頭,用銼刀在筒口磨出鋸齒權充鑽頭,硬生生將鑽孔擴大到口徑一致。

只靠插洞固定效果不佳,促成我第一次體驗「缺什麼零件自己印」的暢快。Autodesk 的免費 3D 建模工具 - Tinkercad,非常易學好上手,我設計了一個內六角孔固定器,兩側附有螺絲孔,負責將曲管緊緊固定在木板上。

PLA 料質印成零件強度挺好,精密度也夠,與管身六角螺帽密合良好,鎖上螺絲後的固定效果比預期好很多。

組裝成品如下。我還多預留兩個孔,未來可擴充到六隻手~

多了額外四隻手,未來焊接就能三頭六臂如有神助囉! (灑花)

Email 客戶端之 CSS 支援問題

$
0
0

活到老學到老,今天又學會一件事。

雖然顯示 HTML 格式已是當今 Email 軟體或線上信箱的必要條件,但許多被視為基本的 CSS 功能卻不一定在支援範圍內。

用以下範例展示,我設計一段 HTML 當作 Email 內文。先將 .dynamic 設成 display: none,再指定 .mode-1 .dynamic.mode-1 及 .mode-2 .dynamic.mode-2 為 display: inline,如此在容器加 class="mode-1" 或 class="mode-2" 可切換顯示不同區塊內容;下方 <div> 則示範 overflow: hidden 效果。

<style>
	.dynamic { display: none; }
	.mode-1 .dynamic.mode-1 { display: inline; }
	.mode-2 .dynamic.mode-2 { display: inline; }</style><div class="mode-1"><span class="dynamic mode-1">Show on Mode 1</span><span class="dynamic mode-2">Show on Mode 2</span></div><div style="width:200px;overflow:hidden;border:1px solid red;"><div style="width:480px;background-color:yellow;">
		Overflow Hidden Test</div></div>

裡面用到的都是很基本的 CSS 技巧,感覺不應會有問題。不過,當成信件內文寄到 Outlook 卻面目全非。mode-1 / mode-2 切換文字沒出現,設了寬度及 overflow: hidden 的 <div> 被內層 <div> 撐開。

Outlook 介面上有個「如果這個訊息的顯示有任何問題,請按一下這裡,在瀏覽器中檢視。」,點選改用 IE 開啟,這才呈現它應有的樣貌。

由此我學到一件事,Outlook 雖然支援 HTML 格式,但只支援部分的 CSS 功能!

基於安全考量不允許 JavaScript 我能理解,但不支援一些很基本的 CSS 倒在意料之外(猜想是為了簡化程式複雜度及效能考量)。

爬文找到一個很棒的參考資源: Email Client CSS Support - Email Design Reference

其中整理 Gmail、Outlook.com、Yahoo! Mail、Outlook、Apple Mail、iOS、Android 等各家 Email 客戶端對各式 CSS 特性的支援程度,大家在設計信件 Email HTML 內容記得參考,以免 HTML 在客戶端走針而不自知。

Chrome 記憶密碼誤填欄位問題

$
0
0

同事報案,Chrome 會莫名把客戶帳號填入輸入與帳號無關的 <input type="text"> 欄位,初步研判是 Chrome 內建的帳號密碼記憶小工具 Google Smart Lock搞鬼。

用以下網頁重現問題。如操作所示,在登入網頁 Logon.aspx 用 Google Smart Lock 記下密碼,登入後導向 Index.html,網頁上只有一個訂單編號,Chrome 卻自動自發填上使用者名稱。

原因在於 Index.html 中有個被 display: none 隱藏的 <input type="password" />,測試發現當 Chrome 偵測到網頁有 <input type="password" >(即使它被隱藏),就會判定為登入頁面試著找出帳號密碼欄位填入內容,只要隸屬同一 Domain,不受限當初記憶密碼所在 URL 或欄位 id、name,Chrome 都會努力填上資料。

<html><head><title>Query</title></head><body><div>
Symbol: <input type="text" > <input type="submit" value="Query" /></div><div style="display:none">
CheckKey: <input type="password"></div></body></html>

爬文找到 2008 就有人舉報這是個 Bug :1854 - The username and password remember option fill up non login fields. - chromium – Monorail,例如用 <intput type="password"> 輸入信用卡號時會誤擊。開發團隊認為以 Domain 為單位且不限定欄位名稱,有助於廣泛滿足各式情境,故無計劃修正。(但加註了 autocomplete="off" 還被填入內容則是 Bug 無誤)

既然這是 Feature 不是 Bug,來看看該怎麼修改避免問題。

依據 Stackoverflow 的這篇討論

  • Look up to find an upper input type text from the first input type password (Not a hiddentype nor disabled) to pick it as username

Chrome 會將 <input type="password"> 上方非 hidden 或 disabled 的 <input type="text"> 當成使用者名稱欄位(實測 <input style="display: none"> 也會被排除)。我想到的解法是仿效戰機被飛彈鎖定時放出錫箔絲( Chaff )或熱焰彈(Flare)干擾追蹤(想起小時候看飛狼會吐神奇熱導體,什麼飛彈都能閃),在網頁最頂端放上兩個無用 <input type="text">及<input type="password"> 引誘 Google Smart Lock 攻擊填入資料,避免干擾正常欄位,再用<div style="position:absolute;top:-100px">包覆使之隱形。像這個樣子:

<html><head><title>Query</title></head><body><div style="position:absolute;top:-100px"><input type="text" title="Chaff for Chrome Smart Lock" /><input type="password" title="Chaff for Chrome Smart Lock" /></div><div>
Order No: <input type="text" > <input type="submit" value="Query" /></div><div style="display:none">
CheckKey: <input type="password"></div></body></html>

實測可行,僅供大家參考。

OpenCC 中文繁簡體轉換工具

$
0
0

漫長的碼農生涯,難免會遇到中文繁簡轉換需求,過去我都依賴 Word,但在 Web Server 整合 Word 是件麻煩事。Word 程序體積龐大,啟動要耗用不少記憶體跟 CPU,不適合每次 Request 隨用隨建用完即丟。加上 Word 為桌面程式會綁執行身分,不適合用 IIS AppPool 帳號跑。最後我琢磨出來的解決方案是寫成 Windows Service 以 WebAPI 方式提供服務,服務啟動時開啟固定數量的 Word 程序,分攤處理需求。在實務經驗中,Word 偶爾會因不明原因故障,故得加上連續出錯就重啟 Word 的自我修復機制。另外,伺服器得安裝 Office 多少也增加部署複雜度。

總之,利用 Word 做繁簡翻譯稱不上是完美解決方案。

另一個評估過的繁簡轉換選項是 Microsoft Visual Studio International Pack 1.0 SR1,它有 NuGet套件可直接下載安裝,只需一顆 ChineseConvert.dll 就搞定很方便,可惜不支援字彙轉換,例如「預設記憶體大小及硬碟容量」 應翻成「缺省内存大小及硬盘容量」,幾乎都會被客戶打槍。

最近(事實上 Lag 很久了),我發現一個優秀開源專案 OpenCC https://github.com/BYVoid/OpenCC ( 線上展示 ) ,作者 BYVoid是神人 (請參見網路流傳作者的阿里巴巴面試評語 )。OpenCC 以 C++ 開發,支援 Linux、Mac OS X、Windows、iOS、Android 等平台,官方可下載的已編譯 Windows 版只到 1.0.1 版,1.0.2 版之後的新版需自行編譯,而最新版為 1.0.5 (2017/2/6)。

如果你想自己編譯 1.0.5 版,可以參考這篇:实战Windows下编译Opencc 1.0.5 - CSDN博客,該文章作者有提供編譯好的 1.0.5 Windows 版本,但需要CSDN積分才能下載。 最後我決自己試著用  VS2015 編譯,先安裝 CMake再照著官方文件的 Windows Visual Studio 2013 or higher 編譯步驟:

cmake -H. -Bbuild -G"Visual Studio 14 Win64" -DCMAKE_INSTALL_PREFIX="path/to/install" 
cmake --build build --config Release --target install

身為 C++麻瓜,原本只抱著姑且一試的心態,但 Github 上最新版本已排除大部分會遇到的問題,只有一個小地方要調。原本編譯有錯,我參考前述編譯指南將 PhraseExtract.cpp 改為 Unicode (UCS-2 Little Endian) 就成功了。

編譯輸出目錄 path/to/install/share/opencc 下有字典檔及設定檔,執行檔則在原始碼目錄下的 build\src\tools\Release 資料夾,共有 opencc.dll、opencc.exe、opencc_dict.exe、opencc_phrase_extract.exe 四個檔案,將兩個目錄合併即可使用。

如果嫌自己編譯麻煩,直接下載使用 1.0.1 版也是可以的,1.0.1 版應已能滿足需求。版本比較資訊

將繁體文字檔轉成簡體的指令為:

opencc -i 繁體中文檔案路徑 -o 簡體中文檔案路徑 -c tw2sp.json

其中 -c 指定轉換設定檔,OpenCC 支援以下幾種轉換:

  • s2t.json Simplified Chinese to Traditional Chinese 簡體到繁體
  • t2s.json Traditional Chinese to Simplified Chinese 繁體到簡體
  • s2tw.json Simplified Chinese to Traditional Chinese (Taiwan Standard) 簡體到臺灣正體
  • tw2s.json Traditional Chinese (Taiwan Standard) to Simplified Chinese 臺灣正體到簡體
  • s2hk.json Simplified Chinese to Traditional Chinese (Hong Kong Standard) 簡體到香港繁體(香港小學學習字詞表標準)
  • hk2s.json Traditional Chinese (Hong Kong Standard) to Simplified Chinese 香港繁體(香港小學學習字詞表標準)到簡體
  • s2twp.json Simplified Chinese to Traditional Chinese (Taiwan Standard) with Taiwanese idiom 簡體到繁體(臺灣正體標準)並轉換爲臺灣常用詞彙
  • tw2sp.json Traditional Chinese (Taiwan Standard) to Simplified Chinese with Mainland Chinese idiom 繁體(臺灣正體標準)到簡體並轉換爲中國大陸常用詞彙
  • t2tw.json Traditional Chinese (OpenCC Standard) to Taiwan Standard 繁體(OpenCC 標準)到臺灣正體
  • t2hk.json Traditional Chinese (OpenCC Standard) to Hong Kong Standard 繁體(OpenCC 標準)到香港繁體(香港小學學習字詞表標準)

建議使用tw2sp.json,才有常用字彙轉換效果,實測如下,

如此,我們找到速度比呼叫 Word 快 N 倍又支援字彙轉換的理想解決方案,下篇文章,來談談如何透過 .NET 呼叫整合。

使用 C# 整合 OpenCC 執行中文繁簡轉換

$
0
0

前篇文章介紹了輕巧但威力強大的 OpenCC,使用 opencc.exe 可輕鬆完成繁簡轉換。

如果我們要在 .NET 裡寫一個函式招喚 OpenCC 將繁體字串轉成簡體字串該怎麼做?

呼叫外部 .exe 這等小事,自然難不倒 .NET 老鳥,生個 System.Diagnostics.Process,給對 exe 路徑,弄兩個隨機暫存檔放待翻文字與輸出結果,等待 opencc.exe 執行完畢,讀出結果刪掉暫存檔,搞定收工!

    public static class OpenCCConverter
    {

        static string GetPath(string file) => $"X:\Tools\OpenCC\{file}";
        static string GetTempFile() => $"X:\Temp\OpenCCFiles\{Guid.NewGuid()}";

        static void CallOpenCC(string inputFile, string outputFile, string configFile)
        {
            var si = new ProcessStartInfo()
            {
                FileName = GetPath("opencc.exe"),
                Arguments = $"-i {inputFile} -o {outputFile} -c {GetPath(configFile)}",
                CreateNoWindow = true
            };
            var p = new Process()
            {
                StartInfo = si
            };
            p.Start();
            p.WaitForExit();
        }

        /// <summary>
        /// 將繁體轉為簡體
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public static string ToChsString(string text)
        {
            var inFile = GetTempFile();
            File.WriteAllText(inFile, text);
            var outFile = GetTempFile();
            CallOpenCC(inFile, outFile, "tw2s.json");
            var result = File.ReadAllText(outFile);
            File.Delete(inFile);
            File.Delete(outFile);
            return result;
        }
    }

這個寫法醜歸醜但很管用,還十分簡單明瞭。只是啟動外部程序成本較高,加上要不斷建檔刪檔,就算只是翻譯一個字元也要動用兩個暫存檔,執行效能及資源使用效率並不好。

無意發現 OpenCC 將核心邏輯放在獨立程式庫 – opencc.dll,何不透過 Interoperability由 C# 呼叫 C++ 函式直接執行轉換?於是,不知天高地厚的 C++ 麻瓜開啟了 Unmanged DLL 整合大冒險!

先用 Console Application 測試,為求部署方便,我將 OpenCC 納入專案,並設定編譯時輸出到 \bin\opencc 目錄:

開發心得如下:

  1. C# 要呼叫 C++ 寫的 DLL,起手式是用 DllImport 宣告外部函式對應到 C++ 函式,會遇到的挑戰主要是參數的型別傳換。
  2. 在 Github 討論串找到網友 C# DllImport 的程式片段,由於最後有成功,極富參考價值。我學到可先用 opencc_open() 指定轉換設定 json 檔建立 Instance,再呼叫 opencc_convert_utf8() 傳入 Instance Pointer 及待轉換字串,取得結果字串 IntPtr,再轉為 C# 字串。
  3. DllImport 設定不正確時,opencc_open() 時即會出錯,會傳回之類的訊息
    Unable to load DLL 'opencc.dll': The specified module could not be found. (Exception from HRESULT: 0x8007007E)
    我遇過兩種情況:1) DllImport 指定的 opencc.dll 路徑有誤 2) 執行主機缺少 Visual C++ Runtime。
  4. 官方下載的 OpenCC 1.0.1 Windows 版使用 Visual Studio 2012 編譯,需要「Visual Studio 2012 最新支援的 Visual C++ 可轉散發套件」,微軟支援網站有個 最新支援的 Visual C++ 下載網頁已整理好所有 VC++ 版本的可轉散發套件,請自行依所需版本下載安裝。
    若懷疑跟 C++ Runtime 套件沒裝有關,最簡單的驗證方法是手動執行 opencc.exe,若彈出缺少 msvcp***.dll 之類的錯誤訊息就是了。
  5. opencc_convert_utf8() 轉換失敗時不會出錯,會傳回 IntPtr.Zero,詳細錯誤訊息需另外呼叫 opencc_error() 取得。
  6. 我一度卡在一個關鍵點,待轉換字串與結果字串,形式為記憶體指標指向一段 UTF8 編碼格式的 byte[],與 string 之間需要特殊函式轉換,我在 Stackoverflow 找到可用範例

瞎弄一陣,萬萬沒想到還真被 C++ 麻瓜試出來了,可執行程式範例如下:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Debug.WriteLine(
                OpenCCHelper.ConvertToChs(
                    "預設記憶體大小與硬碟容量"));
            Console.ReadLine();
        }
    }

    public static class OpenCCHelper
    {
        [DllImport("opencc\\opencc.dll", EntryPoint = "opencc_open")]
        static extern IntPtr opencc_open(string configFileName);

        [DllImport("opencc\\opencc.dll", EntryPoint = "opencc_convert_utf8")]
        static extern IntPtr opencc_convert_utf8(Int64 opencc, IntPtr input, long length);

        static IntPtr OpenCCInstance = IntPtr.Zero;

        static OpenCCHelper()
        {
            OpenCCInstance = opencc_open(".\\opencc\\tw2sp.json");
        }

        //https://stackoverflow.com/a/10773988/288936
        public static IntPtr NativeUtf8FromString(string managedString)
        {
            int len = Encoding.UTF8.GetByteCount(managedString);
            byte[] buffer = new byte[len + 1];
            Encoding.UTF8.GetBytes(managedString, 0, managedString.Length, buffer, 0);
            IntPtr nativeUtf8 = Marshal.AllocHGlobal(buffer.Length);
            Marshal.Copy(buffer, 0, nativeUtf8, buffer.Length);
            return nativeUtf8;
        }

        public static string StringFromNativeUtf8(IntPtr nativeUtf8)
        {
            int len = 0;
            while (Marshal.ReadByte(nativeUtf8, len) != 0) ++len;
            byte[] buffer = new byte[len];
            Marshal.Copy(nativeUtf8, buffer, 0, buffer.Length);
            return Encoding.UTF8.GetString(buffer);
        }

        public static string ConvertToChs(string text)
        {
            IntPtr inStr = NativeUtf8FromString(text);
            IntPtr outStr = opencc_convert_utf8(OpenCCInstance.ToInt64(), inStr, -1);
            Marshal.FreeHGlobal(inStr);
            return StringFromNativeUtf8(outStr);

        }
    }

}

如下圖,我成功呼叫 opencc.dll 完成繁簡轉換。

核子試爆成功是第一步,要寫成共用元件還會再遇到一些問題,例如:x86/x64 必須使用不同 opencc.dll、部署到 ASP.NET 網站時 DllImport 路徑需動態指向網站資料夾、Thread-Safe 考量、Memory Leak 疑慮... C++ 麻瓜大冒險尚未結束,下集待續。

(聲明:程式為門外漢參考爬文及測試所得,如有 C/C++ 高人路過,請鞭小力一點並不吝指正)


2018 台北星光馬

$
0
0

在 2015 年跑過台北星光夜跑,三年後,雖然主辦單位不同,但賽道與時間幾乎一樣,來回味久違的夜跑滋味。

全馬下午四點半起跑,提早出門搭捷運慢慢晃過去,搭接駁車前買了 600 cc 保特瓶喝下肚建立安全庫存,避免天熱缺水狂飲來不及吸收搞到肚子難受。兩點四十左右搭上接駁車有兩段小插曲,車上悶了近十分鐘,有跑友提醒司機才想起忘了開冷氣(登楞),然後疑似行車路線有誤繞了一圈,開了快 20 分鐘才到大佳國小(登楞)。即便如此,三點出頭就到了會場,先在公園大帳篷下躲太陽,快四點才去寄物,順便四處晃晃,噴泉附近水霧瀰漫挺消暑的。

會場有個不斷冒出白煙的醫務帳篷,原來是運動噴劑區,好點題呀,有創意。

四點半起跑,此時天氣轉陰偶有徐風,但氣溫仍破 30 度,不是該拼成績的場子,開心就好。

大會規劃得挺好,每公里都有里程標示,一路上里程標示清晰,交管周到,補給無虞(哆啦A夢小蛋糕、薯片跟西瓜好好吃哦)。而我的 fenix 3 送修回來了,工程師研判為軟體問題,只升級了作業系統連系統都沒重置(不過順便換新錶帶是意外驚喜,Garmin 真是佛心來著),資料都在。而經過實戰測試,30K 之後 GPS 大飄移的問題已消失,算是修好了。

端午快到了,河道上有人在練習划龍舟,吼嘿吼,吼嘿吼,嘿~ 吼!嘿!吼~~~

這場與忠孝哥同跑,剛過 10K,忠孝哥忽然心律莫名飆高畏寒,25K 後還頻頻大腿瀕臨抽筋,一整個失常。 由於是在水站吃完西瓜不久發生,我研判應是吃到倒吊子西瓜所致(大誤),另一種可能是被賽道上的異次元力量干擾,建請未來大會勱察賽道時應請道士隨行(再大誤)。總之,今天不排目標計劃,隨遇而安囉~

跑完第一圈天色開始變暗,對岸 11K 健跑組的綠色 LED 手環的點點星光串成一長串,很是好看。

行經百齡橋時遇到大批消防車從頭上急駛而過,不久後河濱路燈全滅,回家才知剛好遇上士林夜市火警斷電

夜景很美,沒帶腳架拍不下來。好不容易在河岸找到一塊位置適中的水泥護欄,試了幾次,終於拍出一張像樣的。

跑跑走走,但有打起精神趕了一段路,趕在 5:20 之內完賽,成績普普,沒想到還有超過一半以上全馬跑友還沒回來。

完賽時會場人潮已散,速速領完東西坐在會場空地拉筋吹著涼風,有種度假的悠閒感。(但聽說半馬組領完賽禮排隊要排近半小時,跑友大抓狂。)

完賽禮比想像豐富,獎牌蠻好看的,還意外有一枚金幣。

獎牌。

 

金幣。

 

跑得蠻開心的,將本場列入回袋賽事。

【茶包射手日記】SQLAgent 無法執行批次檔

$
0
0

燃燒一小時寶貴青春才查出問題 Orz,PO 文留念。

同事報案,某個用 SQL Agent 定期跑的批次檔 (.bat) 執行無效,原因不明。其寫法類似如下範例,看起來沒什麼問題:

實測開 DOS 視窗直接跑 ImportBOMFromSysA.bat 正常,於是我將偵察方向導向 SQL Agent 執行時工作目錄是 Windows\System32 所致,但檢查該批次檔有依 TIPS-指定主控台應用程式的工作目錄一文所提使用 CD 改路徑技巧,加上其 Log 檔未輸出至 System32,初步排除可能。嘗試手動執行 SQL Agent Job,執行歷程顯示程式執行正常,Exit Code 為 0,代表程式未出錯,而 Log 檔未見相關偵錯訊息軌跡,感覺是程式完全沒執行。

開啟 Process Monitor 觀察,至始至終沒看到讀寫 ImportBOMFromSysA.bat 檔案的記錄,這就不合理了。試將 .bat 檔搬走,SQL Job 居然也沒出錯,一怒之下,將原指令清除改成 D:\Batch\WTF.bat,這才冒出找不到執行檔的錯誤。

回頭再看一次原指令,我似乎懂了什麼。恢復原寫法,將 rem 註解行刪除,ImportBOMFromSysA.bat 便跑了起來。

原來,命令輸入欄雖然可以輸入多行,只有第一行是有效的啊啊啊啊...

理解了這點,下對關鍵字蒐集不少人的踩坑經驗:

面對此一限制的有效解法是用 &&  串接多行指令,但我們的案例第一列是 REM 註解不適用本招,因此我將 REM 移至第二行解決問題。

VS2017 無法載入 MVC4 專案

$
0
0

最近接連遇到兩次的問題。

首先是某個用 Visual Studio 2017 開發的專案,同事 T 從 TFS 取回最新版以 VS2017 開啟,其中卻有兩個 MVC 專案呈現截入失敗,其他還有 Class Library 及 Web Site Project 等多個專案則沒問題;改用 VS2015 開啟則能正常載入。

嘗試重新載入專案會出現以下錯誤訊息,並附上一段說明連結:

SomeMVC\SomeMVC.csproj: 找不到這種專案類型的基礎應用程式。請嘗試這個連結以取得其他資訊: (其英文為 The application which this project type is based on was not found. )
http://go.microsoft.com/fwlink/?LinkID=299083&projecttype=E3E379DF-F4C6-4180-9B81-6769533ABE47

但說明連結已失效最終被導向 http://www.asp.net,斷了線索。

同事 T 有使用 VS2017 在開發其他 MVC 專案,當場實測開啟其他 MVC 專案正常,排除 VS2017 未安裝 MVC 專案型別支援的可能。

無獨有偶,同事 J 也回報他重新安裝 VS2017 後,也出現前幾天還在維護的 MVC 專案卻載入失敗況,請我試試有無問題。一樣是我的 VS2017 可開啟,同事的 VS2017 無法載入。

歸納兩起案例:

  1. 訊息指向 VS2017 不支援 MVC 專案型別,但當下開啟其他 MVC 專案又正常。
  2. 我的 VS2017 開啟問題 MVC 專案並無問題。
  3. 同事改用 VS2015 開啟問題 MVC 專案則正常。
  4. 無法載入的 MVC 專案共通點是多年前建立 (甚至可回溯至 VS2012/VS2013 時代),猛然發現,問題專案都是 ASP.NET MVC4!
  5. 同事 J 的案例更有趣,重新安裝 VS2017 後,前幾天還在維護的 MVC 專案忽然無法載入。

爬文在 VS 討論版找到解法

使用文字編譯器開啟問題 .csproj,其中有個 ProjectTypeGuids,長得類似 <ProjectTypeGuids>{E3E379DF-F4C6-4180-9B81-6769533ABE47};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

將第一個 {E3E379DF-F4C6-4180-9B81-6769533ABE47}; 刪掉,經實測可立即排除問題。

不過,網路查到的狀況發生於 VS2017RC 版,依據 VS Team 在 2017/3/2 的留言,問題已於正式 Release 時修正。我的 Visual Studio 2017 是 RTM 時安裝一路升級,開啟 MVC4 專案沒問題。同事 T 安裝時間較晚,同事 J 則是移除重裝最新版時出狀況。莫非是新版 VS2017 移除了過時的 MVC4 專案支援?

研究問題的過程中,學到兩則冷知識:

  1. ASP.NET MVC 專案是所謂的 Flavored Project (或稱為 Project Subtype,專案子型別)
    Eilon Lipton's Blog - Opening an ASP.NET MVC project without having ASP.NET MVC installed- The p
    意思是 ASP.NET MVC 並未另建新專案型別,而是在 ASP.NET Web Application Project 專案型別上加料讓專案具有 MVC 特性。
  2. .csproj 中有個 ProjectTypeGuids 可包含多個 GUID 值代表專案子型別
    List of Visual Studio Project Type GUIDs - CodeProject整理了專案子型別 GUID 清單,查表得知問題 MVC4 專案共有
    * ASP.NET MVC 4 {E3E379DF-F4C6-4180-9B81-6769533ABE47}
    * ASP.NET MVC 5 {349C5851-65DF-11DA-9384-00065B846F21}
    * C# {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
    三種專案子型別。

結論,新安裝 VS2017 開啟 MVC4 專案如遇無法載入狀況,手動編譯 .csproj,移除  ProjectTypeGuids 中的 {E3E379DF-F4C6-4180-9B81-6769533ABE47} 可排除問題。

但成功載入後,可能還有缺少參照組件問題,例如 System.Net.Http.Formatting 來自 GAC,若機器沒裝過 ASP.NET MVC4 套件,就不會有 C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 4 資料夾跟相關組件。

要解決此問題,可移除缺少的參照組件改由 NuGet 下載安裝,或是一不做二不休將專案升級成 MVC5,升級方式可參考官方文件:如何升級 ASP.NET MVC 4 和 Web API 專案,以 ASP.NET MVC 5 和 Web API 2 - Microsoft Docs

CSHTML Layout Page、Partial View 執行順序實驗

$
0
0

維護 ASP.NET MVC 專案遇上巢狀 Layout 引用 Partial View 的情境,無法斷定執行先後順序,想必是自己觀念不清,做了以下實驗驗證,順手分享之。

假設有 ASP.NET MVC 巢狀 Layout 並混用 Partial View 結構如下:

_Layout.cshtml

@{
    System.Diagnostics.Debug.WriteLine("_Layout.cshtml");
}<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>@ViewBag.Title</title></head><body>

@Html.Partial("_PartView1")

@RenderBody()

</body></html>

_PartView1.cshtml (_PartView2.cshtml 及 _PartView3.cshtml 做法相同,只有數字不同)

@{
    System.Diagnostics.Debug.WriteLine("PartialView1");
}<div>Partial View 1</div>

_NestedLayout.cshtml

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
    System.Diagnostics.Debug.WriteLine("_NestedLayout.cshtml");
}
@Html.Partial("_PartView2")
@RenderBody()

Index.cshtml

@{
    Layout = "~/Views/Shared/_NestedLayout.cshtml";
    System.Diagnostics.Debug.WriteLine("Index.cshtml");
}

@Html.Partial("_PartView3")<h2>Index</h2>

HomeController.cs

    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            System.Diagnostics.Debug.WriteLine("Index Action");
            return View();
        }
    }

執行結果不難預期:

問題來了,HomeController.cs、Index.cshtml、_PartView1.cshtml、_PartView2.cshtml、_PartView3.cshtml、_NestedLayout.cshtml、_Layout.cshtml 都埋了 System.Diagnostics.Debug.WriteLine(),將以什麼順序執行?

給大家 20 秒自我測驗。

答案揭曉:

HomeController Index Action –> Index.cshtml –> Partial View 3 –> _NestedLayout –> Partial View 2 -> _Layout –> Partial View 1

這順序不難理解,基本上就是從 HomeController.cs 開始,從 Index.cshtml、_NestedLayout.cshtml 到 _Layout.cshtml,由內而外的順序將 Razor View 轉為 HTML,生成 HTML 過程才載入 Partial View。

(原想找到官方文件證實,搜索未獲,十方大德如有知悉懇請不吝補充)

在 Partial View 與 View 間使用 ViewBag 傳送資料

$
0
0

在 ASP.NET MVC View 引用伺服器端傳來的資料,正統做法是定義 View Model 類別,Action return View(viewModelObject),在 CSHTML 宣告 @model 定義強型別並使用 Razor 語法存取 Model 變數。(延伸閱讀:mrkt 的程式學習筆記: ASP.NET MVC 的ViewModel - 基礎篇 )

但如果是要傳遞簡單的數字或字串(像是啟用特定功能的旗標、頁面標題... 等等),為此定義 View Model 類別有點小題大作,此時 ViewData 跟 ViewBag 是不錯的輕巧選擇。(不建議使用 TempData、Session,延伸閱讀:[探索 10 分鐘] 寫點有關 ASP.NET MVC ViewModel, ViewData, ViewBag, TempData 的代碼)

前幾天學到在 Partial View 使用 ViewBag 的一則眉角,寫成筆記備忘。

我在 ViewBagTest.cshtml 裡宣告 ViewBag.Num = 123:

@{
    Layout = null;
    ViewBag.Num = 123;
}<!DOCTYPE html><html><head><title>ViewBag Text</title></head><body><div><div>V1: Num=@ViewBag.Num</div>
        @Html.Partial("_PartialView")<div>V2: Num=@ViewBag.Num</div><div>V2: Text=@ViewBag.Text</div></div></body></html>

ViewBagTest.cshtml 使用 Html.Partial("_PartialView") 引用 _PartialView.cshtml,在其中將 ViewBag.Num 改成 456,並另外指定 ViewBag.Text = "Test":

<div style="background-color: #ddd; padding: 6px;"><div>P1: Num=@ViewBag.Num</div>
    @{
        ViewBag.Num = 456;
        ViewBag.Text = "Test";
    }<div>P2: Num=@ViewBag.Num</div><div>P2: Text=@ViewBag.Text</div></div>

請問,ViewBagTest.cshtml 在執行 Html.Partial("_PartialView") 之後,讀取到的 ViewBag.Num 與 ViewBag.Title 為何?

答案是 ViewBag.Num == 123,ViewBag.Text == null。

依此實驗推論,在 View 設定的 ViewBag 屬性會傳入 Partial View,但 Partial View 對 ViewBag 所做的修改則不會傳回 View 端。如此就有定論了嗎?

且慢,再看一個實驗,我們改設 ViewBag.List = new List<string>():

@{
    Layout = null;
    ViewBag.List = new List<string>()
    {"View"
    };
}<!DOCTYPE html><html><head><title>ViewBag Text</title></head><body><div><div>V1: List=@string.Join(",", ViewBag.List.ToArray())</div>
    @Html.Partial("_PartialView")<div>V2: List=@string.Join(",", ViewBag.List.ToArray())</div></div></body

在 _PartialView.cshtml 新增字串到 ViewBag.List:

<div style="background-color: #ddd; padding: 6px;">
    @{
        ViewBag.List.Add("Partial");
    }</div>

測試結果如下。這次 View 端成功讀到 Partial View 塞入 ViewBag.List 的內容:

以上兩個現象,說穿了是 Value Type 與 Reference Type 特性使然。(延伸閱讀:Self Test - Value Type vs Reference Type ) 而背後的原因是 - ASP.NET MVC 會為 Partial View 複製一顆專屬的 ViewBag,但因為 Reference Type 特性,複製版 ViewBag 與原始 ViewBag 的 Reference Type 屬性是同一個物件個體。

由 Github 上的 ASP.NET MVC 原始碼可驗證這點:(是的,ASP.NET MVC 的原始碼已經完全在 Github 公開了,Open Source 萬歲!)

internal virtual void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData, 
object model, TextWriter writer, ViewEngineCollection viewEngineCollection)
{
	if (String.IsNullOrEmpty(partialViewName))
	{
		throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
	}

	ViewDataDictionary newViewData = null;

	if (model == null)
	{
		if (viewData == null)
		{
			newViewData = new ViewDataDictionary(ViewData);
		}
		else
		{
			newViewData = new ViewDataDictionary(viewData);
		}
	}
	else
	{
		if (viewData == null)
		{
			newViewData = new ViewDataDictionary(model);
		}
		else
		{
			newViewData = new ViewDataDictionary(viewData) { Model = model };
		}
	}

	ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View, 
	newViewData, ViewContext.TempData, writer);
	IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection);
	view.Render(newViewContext, writer);
}

HtmlHelper 在 RenderPartial() 時會以 new ViewDataDictionary(原來ViewData) 方式另建一個 newViewData 供 PartialView 使用,這就是為什麼在 Partial View 修改數字及字串不會反映回 View 端,但 List<string> 會。

好,如果我們希望在 Partial View 裡能修改 ViewBag 的所有屬性並傳回 View 端該怎麼做?

以下是我找到最簡潔的做法,在 View 端來個 ViewBag.ParentViewBag = ViewBag,把自己當成 Reference Type 屬性傳進去:

@{
    Layout = null;
    ViewBag.Num = 123;
    ViewBag.ParentViewBag = ViewBag;
}<!DOCTYPE html><html><head><title>ViewBag Text</title></head><body><div><div>V1: Num=@ViewBag.Num</div>
    @Html.Partial("_PartialView")<div>V2: Num=@ViewBag.Num</div></div></body></html>

在 Partial View 改用 ViewBag.ParentViewBag,所有的修改就會忠實反映回 View 端囉!

<div style="background-color: #ddd; padding: 6px;">
    @{ var vb = ViewBag.ParentViewBag;}<div>P1: Num=@vb.Num</div>
    @{
        vb.Num = 456;
    }<div>P2: Num=@vb.Num</div></div>		

測試 OK,打完收工,我們下次再會。(揮手下降)

Viewing all 428 articles
Browse latest View live