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

2017 石碇初超馬順撿二格三角點

$
0
0

渣打馬意外跑出 SUB 4後跑馬心境大不同,巔峰已達,夫復何求?十足的破百老兵擺爛心態 XD 兩週後緊接而來的石碇馬自然完全視成績如浮雲,用純踏青郊遊的心情享受山野。

抵達會場路上人車稀少,一度狐疑記錯了日子?(事後得知本場全馬只有五百多人,不知是賽事太多被稀釋,還是石碇馬賽道硬斗嚇退了跑者)

不能免俗來張起跑照,出發點在操場中央,晶片感應墊在遠遠的運動場入口(照片中央紅布條處),少了拱門感覺怪怪滴…

石碇山區的風景一樣美,補給一樣豐富有趣,本屆再度與忠孝哥組成閒散玩跑團,邊跑邊吃邊玩。在補給站吃到石碇有名的茶油麵線、翠玉蛋,吃了水果、汽水、豬頭皮,還來上一碗維力炸醬麵配豆干海帶,大滿足~ 經過許家麵線時遇到美食節目出外景,聽說是詹姆士的節目來著。

今年路線做了調整,拿掉陡死人的變態螞蟻路,從華梵大學一路跑到二格路 22K,再原路折返,路線好跑很多,深得我心。而這場44K也成了我生涯的第一場超馬,呵。

 

 

氣候異常,今年各地不復櫻花爆發的盛況,路上看到零星幾株杏花、櫻花,就算有了交代。

花了三個小時跑完21K,路線愈跑來熟悉,原來折返點就設在當年週週報到的二格山綠豆湯舊址(這才想起當年我還有在三角點附近找登山條貼MVP貼紙的陋習 XD)。

看到熟悉的路標,到二格山頂只要205公尺耶!這距離實在太誘人了(雖然海拔要爬升一百公尺),情不自禁做了有點瘋狂的決定,仗著八小時才關門,那就順便上二格山撿個三角點吧!原本打算脫隊獨衝,但閒散玩跑團成員都沒登過二格,又聽我講古講到嘴角全沬,一時興起就變成全團登頂。

氣喘吁吁衝上山頭,這180度的美景值回票價~

經典的漸層峰巒。(感謝PM2.5增添效果 XD)

屈指一算,約有兩三年沒上來了吧?趁著跑馬重遊舊地挺妙的。

三角點,好久不見!別來無恙?

大會計時仍在一分一秒繼續,在山頂不敢逗留太久,趕緊接回賽道。一來一往耗去半個多小時,我們也由中段班落入後段班,落入倒數五十名。一路苦追,石碇馬的補給沒話說,跑後段班仍糧草充足,有吃有喝,就這樣開開心心晃回終點。

看到這一堆模子就知道終點快到了,最後一小段路催了點油門,保住 SUB 7。

回到會場沒領到獎牌,原來是獎牌不夠,跑太慢的同學改為事後補寄(後來問了跑友,得知 6:20 之後就沒獎牌了), 馬場浮沈多年,心裡倒很能接受「跑得慢就是該死」(就像「菜就是該死」一樣天經地義 XD)的潛規則,倒也不以為意,哈。

今年大會安排跑友可坐在餐廳吃排骨便當,很是貼心,只可惜跟獎牌一樣數量出了差錯,我們足足等了近一小時才吃到飯。便當店老闆娘跟大會工作人員滿是歉意,不斷送上飲料(x2)、麵包賠罪。排隊時跟老闆娘閒聊,聽說是大會提報人數有誤,事後追加才搞到手忙腳亂。我們不趕時間,待在餐廳坐著納涼倒也舒服,就樂得悠閒慢慢等慢慢吃,無妨,能坐著吃現做便當,很讚!

賽後沒多久獎牌就補寄到了,就這樣又開心再下一馬。

   

比賽日期恰巧搭上馬拉松世界的線上馬拉松,趁機搞了塊「黑暗執行緒」完賽獎牌假掰一下,這下專屬的號碼布跟獎牌都有囉~ 呵。


ODP.NET 無法顯示 raise_application_error 自訂訊息

$
0
0

接獲報案,某 Oracle Package 使用 raise_application_error抛回自訂錯誤代碼與錯誤訊息(其中包含輸入參數以利偵錯),使用 ODP.NET 呼叫時理應可在 Exception.Message 看到自訂錯誤訊息,但某支程式出錯時卻只傳回錯誤代碼並抱怨找不到該代碼對應訊息:ORA-20001: Message 20001 not found;  product=RDBMS; facility=ORA

經過調查與對照測試,發現與程式被包在 TransactionScope 有關。用以下程式重現與驗證問題:

using Oracle.DataAccess.Client;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Transactions;
 
namespace OraExpLab
{
class Program
    {
staticstring csOra = "Data Source=...;User ID=...;Password=....";
staticstring csSql = "Data Source=(local);Integrated Security=SSPI;";
 
staticvoid querySqlServer()
        {
using (var cn = new SqlConnection(
                       csSql + "Application Name=" + Guid.NewGuid().ToString()))
            {
                var cmd = new SqlCommand("SELECT getdate() as D", cn);
                cn.Open();
                var dr = cmd.ExecuteReader();
                dr.Read();
                Console.WriteLine(dr["D"]);
                cn.Close();
            }
        }
 
staticvoid queryOraServer()
        {
using (OracleConnection cn = new OracleConnection(csOra))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = "SELECT SYSDATE as D FROM DUAL";
                var dr = cmd.ExecuteReader();
                dr.Read();
                Console.WriteLine(dr["D"]);
                cn.Close();
            }
        }
 
staticvoid raiseOraError()
        {
using (OracleConnection cn = new OracleConnection(csOra))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = @"
declare
begin
    raise_application_error(-20001, '我錯了');
end;
";
try
                {
                    cmd.ExecuteNonQuery();
                }
catch (Exception ex)
                {
                    Console.WriteLine("ORACLE CUST ERROR:" + ex.Message);
                }
            }
        }
 
staticvoid Main(string[] args)
        {
            raiseOraError();
 
using (var tx = new TransactionScope())
            {
                querySqlServer();
                queryOraServer();
                raiseOraError();
                Console.WriteLine(
                    Transaction.Current.TransactionInformation.LocalIdentifier);
                Console.WriteLine(
                    Transaction.Current.TransactionInformation.DistributedIdentifier);
            }
            Console.Read();
        }
    }
}

用一小段 PL/SQL Script 故意抛回自訂錯誤,單獨呼叫時可由 ex.Message 看到自訂訊息「我錯了」。隨後用 TransactionScope 將其與 SQL 查詢包起來,刻意觸發分散式交易,並由 Transaction.Current.TransactionInformation.DistributedIdentifier 驗證分散式交易已啟動(參考),第二次呼叫傳回錯誤訊息變成 ORA-20001: Message 20001 not found;  product=RDBMS; facility=ORA。

執行結果如下:

ORACLE CUST ERROR:ORA-20001: 我錯了
ORA-06512: 在 line 4
2017/3/28 上午 05:40:59
2017/3/28 上午 05:41:00
ORACLE CUST ERROR:ORA-20001: Message 20001 not found;  product=RDBMS; facility=ORA
d9f57da1-9ad4-4623-b91c-7ac4044fc7c1:1
e01b29b2-888a-4e8c-b612-452a22e357fc

進一步對照測試,發現這現象只發生在使用 Unmanaged ODP.NET,使用 Managed ODP.NET 不管有無分散式交易都可看到自訂錯誤訊息。猜想這與 Oracle Client 的 Unmanaged 程式庫行為有關,現階段遇此困擾,可考慮改用 Managed ODP.NET逃避問題。

LINE Notify / LINE Login 實作小問題整理

$
0
0

最近在評估網站故障的自動通報機制。LINE 在台灣普及率及依賴度都很高,是很適合的即時通知管道,由於只需單向傳送訊息,LINE Notify 免費且無人數上限,實作又比 LINE Bot 單純。去吧,LINE Notify 就決定是你了。

David 老師有篇詳細的教學文,文章用 Postman示範 API 溝通細節,不難用 WebClient 改寫,即可簡單搞定線上訂閱介面。

流程如下:

  • 針對不同使用者產生專屬URL,例如: httqs://notify-bot.line.me/oauth/authorize?response_type=code&client_id=….&redirect_uri=callback網址&scope=notify&state=使用者身分識別字串
  • 使用者會被導到 LINE App(手機/平板)或是 LINE 登入網頁,同意授權後會被導回指定的 Callback 網址(必須與 LINE 開發設定填寫網址一致)
  • Callback 網址程式收到 code 及 state,以 code 為參數呼叫 httqs://notify-bot.line.me/oauth/token 可取得 access_token,再依 state 判斷使用者身分,將使用者資料及對應 access_token 寫入資料庫,方便日後管理與應用。
  • 要發訊息給指定使用者,由資料庫查詢其 access_token,呼叫 httqs://notify-bot.line.me/api/notify 以發送通知。

順道也測試了 LINE Login,以下整理過程踩過的小坑及眉角:

  • Callback URL 若非 localhost,需使用 https。
  • Callback URL 設定介面說設定會立即生效,實務上要等一兩分鐘較保險,勿心急狂試,還沒生效測不通一直亂改。(對,就是我,性急直逼王藍田)
  • LINE 登入網頁出現 An error has occurred. Please wait a moment and try again. 可能是 client_id 錯誤或失效。
  • 瀏覽器導向 LINE 登入網頁時,在手機平板可以選擇導向「LINE 自動登入」程式或導向登入網頁(如下圖所示)。

    若是在 LINE App 開啟網頁被導到 LINE 登入程序,將自動進入同意畫面。
    感謝網友 Kuan 補充:手機平板瀏覽器導向 LINE自動登入 功能視瀏覽器而定,Chrome 可以,但像 Asus 內建瀏覽器或 iOS Safari 就出現過不支援的狀況。
  • 設定不正確時,登入網頁會出現「無法登入,請稍候再試。」這種模糊訊息,此時可從 URL 找到類似 errorMessage=AUTH_INVALID_REDIRECT_URL&errorCode=400 之類的詳細訊息。
  • 設定不正確時,若導向 LINE 程式會出現「錯誤 無法正確執行」訊息。
  • LINE Login 取得 Access Token 後可取得使用者姓名、照片;LINE Notify 取得 Access Token 後無從識別使用者身份,實務上要由 state 帶入使用者身分以便建立對應。
  • 當 LINE Channl 處於 DEVELOPING 狀態時(未轉為 PUBLISHED),LINE Login 只接受 CHANNEL_EDITOR 及 DEVELOPER 角色成員登入[參考],其餘人員使用登入網頁沒有任何錯誤訊息顯示,只會一直重覆登入網頁(由 URL 偷看訊息為 AUTHENTICATION_FAIL);LINE App 則出現「錯誤 無法正確執行」。
  • Channel 管理介面可將其他測試人員設定成 DEVELOPER。記得 Email 跟 LINE ID 要正確填寫,對方需收 Mail 點連結確認成員關係,完成後,該使用者即可在 DEVELOPING 階段通過 LINE Login 認證。

2017 八卦山台地馬拉松

$
0
0

往年鳳梨馬(八卦山台地馬拉松)都是清明返鄉順便跑,今年因故沒依原訂計劃,只有一人獨行,難得地體驗「一個人小旅行」的滋味。土包子第一次在台北轉運站搭客運、投宿旅館、被台北車站內的指標搞到眼花潦亂,當偽背包客感覺也挺讚的,跟全家出遊感受完全不同…
PS:話說路協的衣保大紅包真好用,所有家當塞好塞滿(還包含跑完換裝及鞋子),一個背包搞定。

精挑細選離接駁點步行兩分鐘的旅館,旁邊又家樂福方便採買補給(後來發現打錯算盤,家樂福的蠻牛跟八寶粥都是一手六罐,我可不想一路扛去會場再扛回台北啊啊啊啊~),清晨從容出門搭車真好。

週六從濕冷細雨的台北出發,過了台中地面已乾,偶爾還能從雲隙看到藍天。心想,中部氣候真不錯,比北部宜人多了,但凌晨屋外淅淅瀝瀝,雨勢不小,感覺大事不妙。到早上五點搭接駁車,雨勢雖然小一點,但濕冷度跟週六出發沒什麼兩樣,我直接穿短褲有點抖,敢情寒流也搭了夜車跟來了?(暗!)在車站遇到一位前輩跟一位穿小飛俠雨衣的女跑友在等車,隔一站又上來兩位跑友,遊覽車就由我們五位包車了。

 

清晨的會場籠罩在雲霧裡,有種矇矓美。

這張是要拍遠處的西嶺國小同學的舞獅表演啦,但相機自動對焦一直被導引到前方… 嗯。

六點半準時起跑。參賽人數不多,全馬跟半馬各約七百多人,又是我愛的小而美。

起跑沒多久,看到不可思議的一幕… 在海拔四百公尺的台地看到雲海!Wow~

 

前 10K 幾乎都跑在雲霧中,之後雨勢漸歇,除了鞋子微濕,但免受日曬又涼爽的天氣我已經很滿足了。

跑了1小時19分,前導車帶著第一名折返了,完全看不到車尾燈呀。靈機一動,轉頭望向駛離的前導車背影,我終於看到第一名的車尾燈惹,YA~(謎:嘿!成熟點,好嗎?)

 

鳳梨馬的補給沒話說,今年最大的亮點-我吃到骰子牛(還墊了洋葱擺盤,呵)配啤酒,好讚!

銀行山折返點,17K 完成。

    
 
 

第四年跑,139 線還是一樣的美。

前年開到無法無天的九重葛,今年仍未重返榮光。跟人生一樣,風光只是一時,上台總有下台日…(嗯,嘴砲完得未雨綢繆一下,要是明年它又大暴發,我該怎麼詮譯才不被打臉?)

鳳梨馬忘了拍鳳梨成何體統?

順便來點櫻花。

32K 到了,暖身完畢,比賽正式開始。每 1K 都有里程牌,位置挺準,跟 GPS 錶測量距離誤差在 200 米以內。

催了點油門,43x 完賽,以總升降一千公尺的山路馬來說成績還可以。重點在完賽時間,不知怎麼的,自動對焦又對到前面的掛牌美眉身上…

大會貼心,更衣帳還有蓮篷頭可以沖水,不過天氣太冷,最後幾公里又開始下雨,我跑到雞皮疙瘩都起來了,無福消受。倒是換衣服時聽到隔壁女生帳有人聊天,女生甲看女生乙這種氣溫也敢沖冷水好敬佩,女生乙連忙謙虛說這沒什麼啦。沒多久,聽到水聲跟一聲「靠北,好冷哦」,噗~

完賽時間比預期早,我搭到十二點準時發車的搭駁車,早上一起等車的三人又碰面了,小聊才知前輩是山路跑 34X 的強者(PB 31X),還分享全馬練習每次至少抓 14K 的個人小訣竅。(筆記)

不到十二點半就抵達南投車站,此時雨勢更大,濕冷得很,只想早點回家。原本買3點10分的車票,我發揮敏捷精神,先換票到2點,再補位坐上一點的車,4點半就回到台北,杜絕浪費,完成一次 Scrum 搭車法的演示。

完賽禮包含一罐近兩公升的土鳳梨汁,搭啤酒超級好喝,不枉我一路扛回台北呀~

補上獎牌照:

        

鳳梨馬是我參加過諸多賽事中品質最穩定的,歷經多年修校,賽道、流程、動線、佈置、接駁、補給等大小細節都已 Tune 到幾無瑕疵,讓人十分安心,所以,明年再見囉~

超過一百萬個檔案的 NTFS 資料夾…

$
0
0

在 NTFS 資料夾放入超過一百萬個檔案,會發生什麼事?讀寫檔案會因此變慢嗎?Windows 會不會因此崩潰?

相信很少人有類似經驗,也不會大費周章搞個 Lab 試玩,既然幸運親身體驗過,分享一下經驗。

先說結論:在 NTFS 資料夾放超過一百萬個檔案基本上是可行的(這次遇到的案例超過 150 萬個檔案),若已知完整檔案名稱,讀、寫檔案速度不受檔案數目影響,但會影響檔案總管及部分檔案操作。

我們有個批次轉檔程式會由資料庫讀取資料、存檔後上傳 FTP,每天產生的檔案數約一千筆。因追查問題有時需要檔案內容佐證(跨系統吵架,手握呈堂證供氣勢立刻翻倍呀,你懂的),故需保留檔案。每次調查問題,多半會由資料庫查到檔案名再開 DOS 視窗「notepad 檔名」調閱檔案,用起來很順手方便,感受不到速度延遲,因此大家就忽略了資料夾檔案數每天持續成長,沒人想到要安排定期歸檔搬移排程,就這麼過了七年…

依據 TechNet 文件:NTFS 每個 Volume 的檔案數上限是 4G-1,40 億個檔案放在同一個資料夾理論上是可行的。資料夾使用 B-Tree 結構管理資料,故在已知檔名的前題下,存取檔案的速度不太受同資料夾檔案數多寡影響。資料庫索引也常用 B-Tree 結構儲存索引資料,若已知完整 Key 值,讀取速度不會因為資料筆數倍增明顯下降,也是同樣道理。

關於資料夾可容許的最大檔案數,我沒有找到明確數字。但上述文件提到一個數字(在 Maximum Sizes on an NTFS Volume 段落),如果要在一個資料夾擺放超過 30 萬個檔案,建議停用 8.3 短檔名(尤其是檔名前六碼重複機率很高時),主要是 Windows 會耗費可觀成本避免短檔名重複,推測這只發生在新増或更名時,但是「30 萬」這個數字倒也意味著單一資料夾放幾十萬個檔案仍在 NTFS 設計的容許範圍。

存取檔案速度 OK,問題會出在需要列舉或掃瞄資料夾所有檔案的情境。例如:當資料夾檔案數愈來愈多,就很難再用檔案總管開啟資料夾,一開啟就卡住,甚至導致桌面凍結,只能改用 DOS 視窗,使用 TYPE、COPY 指令存取指定檔名。而且用到 DIR xxxx_*.txt 等篩選條件也會等到地老天荒。這是簡單的數學問題,假設檔案名稱有 32 個字元,100 萬筆檔案清單,光檔名就有 32MB,除了檔名外還有日期時間檔案大小等資料,搜尋時得一一檢查權限,都會消耗記憶體、CPU 並涉及可觀的磁碟 IO 動作。而檔案數一多,系統需串接更多磁區才擺得下目錄資料,列舉檔案清單需由多個零散磁區彙整資料,也會耗費額外的讀取時間。

回到實務案例,雖然已知檔名時讀寫沒什麼感覺,但系統人員發現一些使用上的問題:

  • 檔案總管一打開內有百萬筆檔案的資料夾便卡死沒反應,還無法取消或關閉。到最後,「千萬不要點開 XXX 資料夾(很可怕,不要問)」列為系統管理員口耳相傳的交接事項。
  • DIR 可以執行列出檔案,但只見檔案清單無窮無盡捲個不停,捲到此恨綿綿無絕期,超越常人的耐心極限,檔案總數與大小始終是謎。
  • 單純 DIR 還可以看到檔名狂跑,但 DIR /OD(依日期排序)、DIR *_Blah.txt (篩選檔名特徵)則是一執行就沒反應,直到天荒地老…
  • 想用 .NET Directory.GetFiles()逐一抓取檔案歸檔,GetFiles() 會一次讀入完整清單,結果…

最後,我寫了支 .NET 歸檔程式,將檔案依日期放在 X:\Archive\yyyy\MM\dd\ 目錄下,而歸檔程式的一項挑戰是不能用 Diretory.GetFiles(),需改用 Directory.EnumerateFiles(),傳回 IEnumerable<string>,每次只取一筆,愚公移山奮戰數小時,將舊檔依日期分類,總算馴服這匹脫韁之馬。

後記,查資料在 Stackoverflow 看到一個資料夾放了 1,400 萬個檔案的案例,結論是 NTFS 資料夾能容納的檔案數比想像多很多,但在這種極端情境下要留意其副作用。

【茶包射手日記】Oracle DBLink 遇分散式交易出錯

$
0
0

Oracle 問題又來惹… Orz

某 Package 原本執行正常,當被包入 TransactionScope 範圍啟動分散式交易會出現 ORA-24777: use of non-migratable database link not allowed 錯誤,爬文找到 Rico 的文章,提到 Procedure 使用 Non-Shared Database Link 會導致類似錯誤。

我在測試環境寫了一個使用 Non-Shared DBLink 的 Procedure:

createor replace procedure SP_AccessDBLink(
       p_Count out number
) is
begin
selectcount(1) into p_Count from BlahTable@BlahDBLink;
end SP_AccessDBLink;

使用以下程式碼試著重現錯誤:(借用上次的範例修改)

staticvoid testDbLink()
{
using (OracleConnection cn = new OracleConnection(csOra))
    {
        cn.Open();
        var cmd = cn.CreateCommand();
        cmd.CommandText = @"sp_accessdblink";
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        var p = cmd.Parameters.Add("p_Count", OracleDbType.Decimal);
        cmd.ExecuteNonQuery();
        Console.WriteLine(p.Value);
    }
}
 
staticvoid Main(string[] args)
{
    testDbLink();
 
using (var tx = new TransactionScope())
    {
        querySqlServer();
        testDbLink();
if (Transaction.Current != null)
        {
            Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
            Console.WriteLine(Transaction.Current.TransactionInformation.DistributedIdentifier);
        }
        tx.Complete();
    }
    Console.Read();
}

如程式碼所示,第一次執行成功,包入 TransactionScope 再執行即出錯,但錯誤訊息不太相同,為 ORA0-24778 cannot open connections(無法啟連線):

依據 Oracle 文件:

  • ORA-24777: use of non-migratable database link not allowed
    Cause: The transaction, which needs to be migratable between sessions, tried to access a remote database from a non-multi threaded server process.
    Action: Perform the work in the local database or open a connection to the remote database from the client. If multi threaded server option is installed, connect to the Oracle instance through the dispatcher.
  • ORA-24778: cannot open connections
    Cause: The migratable transaction tried to access a remote database when the session itself had opened connections to remote database(s).
    Action: Close the connection(s) in the session and then try to access the remote database from the migratable transaction. If the error still occurs, contact Oracle customer support.

二者都與 Transaction 有關,可以解釋先前加入 TransactionScope 才出錯的現象。進一步做了不同 ODP.NET 跟 Oracle Server 版本的比對,我崩潰了…

  • Unmanged ODP.NET 12.1 或 11.2.3 + Oracle 11.2
    ORA-24778 cannot open connections
  • Managed ODP.NET 4.121.2 + Oracle 11.2
    ORA-24778 cannot open connections
  • Unmanaged ODP.NET 11.2 + Oracle 10.2 伺服器A 上另一隻存取 Non-Shared DBLink 的 Package
    ORA-24777: use of non-migratable database link not allowed
  • Unmanaged ODP.NET 12.1 + Oracle 10.2 伺服器B 與 伺服器A (Non-Shared DBLink 未必出錯,登楞!)
    無錯誤
  • Managed ODP.NET 4.121.2 + Oracle 10.2 伺服器B
    ORA-02048 attempt to begin distributed transaction without logging on

我承認以上測試結果並未理出頭緒,但測到這裡我的座位旁已擠了滿滿的羚羊,加上缺少足夠權限在 Oracle 模擬某些條件,拎杯身心俱疲欲哭無淚,僅留下記錄等待有志之士們繼續努力…

ODP.NET 12.1/11.2 並存環境發生找不到 OraOps12.dll 錯誤

$
0
0

是的,Oracle 問題又來了!(沒錯,我桌子旁邊的羚羊又更多惹…)

Windows 2012R2 跑多個網站,從 ASP.NET 2.0、3.5、4.0 到 4.5.2 都有,還涉及多台 SQL、Oracle,Oracle 版本有舊有新,部分程式還用到了分散式交易。考慮 ODP.NET 12.1 無法與 Oracle 10.2 進行分散式交易,而新版共用元件多已改用 ODP.NET 12.1,只好 11.2、12.1 兩種版本 Oracle Client 都裝,並移除發行者原則檔,允許不同 ASP.NET 專案使用不同版本。

此種做法經驗證可行,但今天發現有台機器使用 ODP.NET 12.1 的網站卻冒出以下錯誤:

Unable to load DLL 'OraOps12.dll': The specified procedure could not be found. (Exception from HRESULT: 0x8007007E)

相同程式與 Oracle Client 配置在其他機器沒問題,為何在這台機器會出錯。

面對這種情境,最有效排除問題的做法是逐一比對正常環境與問題環境的大小細節-差異之所在,茶包之所在!

比對程式碼、bin\*.DLL、Oracle Client 版本都一致,依過去的經驗,我想到 PATH 環境變數也影響 ODP.NET 找尋 Oracle Client 的結果,比對正常環境與問題環境設定,發現順序有別:

【問題環境】
Path=D:\oracle\product\11.2R5\client32;D:\oracle\product\11.2R5\client32\bin;D:\oracle\product\12.1.0\client32;D:\oracle\product\12.1.0\client32\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\

【正常環境】 
Path=D:\oracle\product\12.1.0\client32;D:\oracle\product\12.1.0\client32\bin;D:\oracle\product\11.2R5\client32;D:\oracle\product\11.2R5\client32\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\

研判 Oracle 11.2 路徑在前導致 ODP.NET 12.1 找不到 OraOps12.dll,調換 PATH 順序並 IISRESET 仍然無效, 頓時心涼了半截。後來想到 Oracle 許多安裝設定都需要重開機,重開機再試,問題終於消失!

結論

  • Oracle 11.2 與 12.1 Client 並存的環境,PATH 環境變數順序可能影響 ODP.NET 找尋 Unmanaged DLL 的結果。
  • 安裝 Oracle 或修改設定,最好重開機再試較保險。
  • 改用 Managed ODP.NET 能有效減少被 Oracle Client 版本踩雷機率,建議多多利用。(NET 4.0 以上適用,2.0/3.5 哭哭)

VS2017 Angular TypeScript 定義檔編譯錯誤

$
0
0

改用 Visual Studio 2017 好一陣子,維護修改 TypeScrpt+ Angular 專案都沒什麼問題。這兩天新起一個 ASP.NET 網站專案想寫個簡單的 Coding4Fun SPA,用 NuGet 裝好 jQuery、Angluar,順手也裝上 jQuery 與 Angular 的 TypeScript 定義檔,發現 Angular 定義檔冒出數十個 Cannot find name 'IPromise'、Namesapce 'angular' has no exported member 'IPromise'、Cannot find type definition file for 'jquery' 之類的錯誤:

起初懷疑是 Anuglar 定義檔改版後與 VS2017 不相容,TypeScript + VSCode 開發漸成主流,找資料還得要先搞清楚文件說的是 Visual Studio 還是 Visual Studio Code,胡亂試了加 tsconfig.json 改設定,愈弄愈渾。後來想想不太對,其他 VS2015 時代的專案用 Angular 定義檔就用得好好的,難道是 VS2017 新開專案的 csproj TypeScript 編譯設定有異造成問題?

深入調查才發現-我裝錯 Angular TypeScript 定義檔了!

之前的專案是用 angularjs.TypeScript.DefinitelyTyped

有多個 angular*.d.ts

新專案裝成 angluar.TypeScript.DefinitelyTyped

只有 angular-component-router.d.ts 及 index.d.ts

再研究了一下,angular.TypeScript.DefinitelyTyped 採用 index.d.ts 屬於新做法,主要配合 npm 安裝、Bower、tsconfig.json 運作較順。angularjs.TypeScript.DefinitelyTyped 最新版只到 2016/12/8,並已從 DefinitelyTyped Github 移除,預期未來也不會再更新。如果你只想使用 Visual Studio 內建的 TypeScript 編譯機制寫 TypeScript,不想牽扯 Bower、Webpack 那堆複雜前端框架,認明使用 angularjs.TypeScript.DefinitelyTyped 比較省事。

由最近的趨勢來看,用 Visual Studio Code 開發 TypeScript 漸漸成為主流,像我這様習慣用 Visual Studio 寫 TypeScript,應該會變成邊緣人吧?哈!

補充:如果你遇到 Duplicated identifier 'export=' 錯誤,可以試著刪除 %localappdata%\Microsoft\TypeScript\node_modules\@types 下的定義檔。VS2017 加入自動偵測載入定義檔的功能(由 package.json、bower.json 及檔案比對),有時可能造成衝突。參考:stackoverflow討論


生活瑣記-201704

$
0
0

地板漏水維修

樓下鄰居通報天花板漏水,漏水點靠近四戶交界,很難判斷水從何來。依水電師傅建議做了實驗,關水塔兼放空水管一整天,再觀察漏水是否止住即知結果。(好熟悉的手法,不管修水電、治病、抓 Bug 還是射茶包,原理都大同小異唄!)

樓下回報,關水之後漏水就停了… Orz 很好,乖乖敲牆鑽地抓漏吧!

二十年前裝修時沒留下水電佈線圖(當時要是有數位相機跟手機就好了,我相信 D 槽一定有滿滿的線索,但那可是 20 年前啊…)水電師傅只能一路鑽一路找。最後找出問題點如下圖,後方塑膠管為冷水管線,前方鐵管則熱水管。熱水管線上方的 T 字型接管(專業術語為「三通」)下方出現鏽蝕(紅1),下方 L 型接管(術語為「彎頭」,紅2)則鏽得更厲害,估計是主漏水點。

在前後端都固定不能移動的情況下要怎麼換掉三通跟彎頭?這還真是門學問,總之,水電師傅靠著精巧裁切技術、神奇串接道具及不斷 Trial and Error,總算拆下鑄鐵材質的三通跟彎頭換上不鏽鋼版新品排除漏水問題。只開挖兩片半塊地磚面積就搞定,算是不幸中的大幸。

 

指尖陀螺 DIY

小木頭學校時興指尖陀螺,我也好奇爬文了解一番,看到不少網友用瓦楞紙板加滾珠軸承的創作。家裡有之間蛇板換下的退役軸承廢品,剛好有 4 枚,便興起 DIY 的念頭。網路上看過的紙板或三秒膠版本效果都不好,感覺 3D 列印的精準度應該可靠些,上網還真找到現成的 3D 建模,借到機器輸出,不到一小時組裝套件就出爐了。

模型精準度不錯,608 軸承內外徑有固定規格,模型的嵌入密合度頗高。另外有個小訣竅,這類軸承原本需承重有加油封,用去漬油洗去潤滑油及雜屑可降低摩擦力延長轉動時間。實測結果,廢物利用版轉動效果不及市售初階產品,玩玩有趣就好,別太認真。

長柄摘心專用剪

陽台的香水檸檬老樹再陷低潮(對照),枝長葉疏無花無果,早先做過功課,學會需適度修剪才會發展良好。上個月剪掉長枝,近日回暖開始大量抽芽,接著計劃著手「摘心」好讓枝葉更茂密。(註:摘心是指摘除新發樹梢頂芽,促使長出更多側芽,以利枝葉茂密。參考 1 2)

不過我遇到一點阻礙:部分樹梢尖端伸出鐵窗近一公尺,手伸再長也模不到,更慘的是檸檬多刺,手臂在枝葉間穿梭,常痛得我哇哇叫… Orz

發揮馬蓋先精神,找來廢棄的魔術拖把鋁柄、從報廢鍵盤拆下 USB 線,用束線帶把剪刀固定在拖把頭塑膠座,USB 線穿過管心綁住剪刀柄,一枝「黑暗牌長柄摘心專用剪」就完成囉~

就見我手起刀落手起刀落手起刀落,沒幾下樹梢嫩芽清潔溜溜。

好啦,其實沒那麼帥,找不到合適的彈簧,目前長柄摘心專用剪必須先收回來用「手」把剪刀柄拉「起」來,伸出去拉 USB 讓「刀」口「落」下來,是這種「手起刀落」法,等找到彈簧再改成連發吧~

Coding4Fun

反正已經不是第一回,開發老爸的即刻救援任務又來了~

為了拯救小木頭慘不忍睹的英文字彙,上網抓了國中常用 2000 字詞,搜集語音檔,開了個 Anuglar TypeScript 專案簡單拼裝單字學習小工具,好一陣子沒寫 SPA ,幸好寶刀未老,沒被難倒,呵。

小「使用者」挺賞光的,持續給了一些改良意見,希望這個「系統」能達成預計效益…

使用 Dapper 接收 Oracle Ref Cursor

$
0
0

沒實際遇到,但接連兩次被問到使用 Dapper 如何從 Ref Cursor 讀取結果,看來上天已強烈暗示我沒 PO 文分享,趕緊補上以免逆天遭譴。

爬文找到的做法都是靠自訂 OracleDynamicParameter 處理 Ref Cursor 對應轉換,循著 Stackoverlow 討論找到一個 Gist 分享的現成版本,將 OracleDynamicParameter.cs 加入專案,便可使用 OracleDynamicParameters 物件 .Add("cursor_name", dbType: OracleDbType.RefCursor, direction: ParameterDirection.Output) 宣告 Ref Cursor 接收查詢結果。Ref Cursor 可用於 PL/SQL Script 或 Stored Procedure,記得改用 QueryMultiple().Read() 讀取結果,如果是 Stored Procedure,QueryMultiple() 時需指定 commandType: CommandType.StoredProcedure 參數。

註:上述的 Gist 版 OracleDynamicParameter配合 Dapper 新版使用時,第 163 列需加上第三個參數 false:appender = SqlMapper.CreateParamInfoGenerator(newIdent, false, false);

測試成功!

附上完成範例:

staticvoid Main(string[] args)
        {
using (var cn = new OracleConnection(cs))
            {
                var p = new OracleDynamicParameters();
                p.Add("n", 1234, OracleDbType.Decimal);
                p.Add("rc", 
                    dbType: OracleDbType.RefCursor,
                    direction: ParameterDirection.Output);
//使用 T-SQL Script 測試 Ref Cursor
                var cmd = @"
declare 
begin 
open :rc for select :n as n, sysdate as d from dual;
end;";
                var m = cn.QueryMultiple(cmd, p);
                var data = m.Read();
                Console.WriteLine(JsonConvert.SerializeObject(data));
 
/*
                --假設 Stored Procedure 如下
                create or replace procedure MyProc(
                 n NUMBER,
                 rc OUT SYS_REFCURSOR 
                )
                as
                begin
                  open rc
                   for select n as N, sysdate as D
                         from dual;
                end;
                 */
 
//Stored Procedure記得指定commandType
                m = cn.QueryMultiple("MyProc", p, commandType: CommandType.StoredProcedure);
                data = m.Read();
                Console.WriteLine(JsonConvert.SerializeObject(data));
 
            }
            Console.Read();
        }
 

【茶包射手日記】問題 JS 導致 ASP.NET MVC 所有 View 無法顯示

$
0
0

查出某支 JavaScript 有錯,修改後更新到網站,沒想到整個 ASP.NET MVC 網站壞光光,所有 View 都無法顯示,出現如下錯誤:

'/' 應用程式中發生伺服器錯誤。

並未將物件參考設定為物件的執行個體。

描述: 在執行目前 Web 要求的過程中發生未處理的例外狀況。請檢閱堆疊追蹤以取得錯誤的詳細資訊,以及在程式碼中產生的位置。

例外狀況詳細資訊: System.NullReferenceException: 並未將物件參考設定為物件的執行個體。

原始程式錯誤:


行 14:     <div>
行 15:     </div>
行 16:     @Scripts.Render("~/bundles/test")
行 17: </body>
行 18: </html>

原始程式檔: E:\Lab\WebAp\Views\Home\Index.cshtml    行: 16

堆疊追蹤:


[NullReferenceException: 並未將物件參考設定為物件的執行個體。]
   Microsoft.Ajax.Utilities.OutputVisitor.Visit(CallNode node) +373
   Microsoft.Ajax.Utilities.CallNode.Accept(IVisitor visitor) +18
   Microsoft.Ajax.Utilities.OutputVisitor.Visit(Block node) +405
   Microsoft.Ajax.Utilities.Block.Accept(IVisitor visitor) +18
   Microsoft.Ajax.Utilities.OutputVisitor.OutputFunctionArgsAndBody(FunctionObject node, Boolean removeUnused) +899
   Microsoft.Ajax.Utilities.OutputVisitor.Visit(FunctionObject node) +603
   Microsoft.Ajax.Utilities.FunctionObject.Accept(IVisitor visitor) +18
   Microsoft.Ajax.Utilities.OutputVisitor.Visit(Block node) +405
   Microsoft.Ajax.Utilities.Block.Accept(IVisitor visitor) +18
   Microsoft.Ajax.Utilities.OutputVisitor.Apply(TextWriter writer, AstNode node, CodeSettings settings) +74
   Microsoft.Ajax.Utilities.Minifier.MinifyJavaScript(String source, CodeSettings codeSettings) +545
   System.Web.Optimization.JsMinify.Process(BundleContext context, BundleResponse response) +92
   System.Web.Optimization.Bundle.ApplyTransforms(BundleContext context, String bundleContent, IEnumerable`1 bundleFiles) +273
   System.Web.Optimization.Bundle.GenerateBundleResponse(BundleContext context) +141
   System.Web.Optimization.Bundle.GetBundleResponse(BundleContext context) +45
   System.Web.Optimization.BundleResolver.GetBundleContents(String virtualPath) +166
   System.Web.Optimization.AssetManager.DeterminePathsToRender(IEnumerable`1 assets) +205
   System.Web.Optimization.AssetManager.RenderExplicit(String tagFormat, String[] paths) +35
   System.Web.Optimization.Scripts.RenderFormat(String tagFormat, String[] paths) +107
   System.Web.Optimization.Scripts.Render(String[] paths) +21
   ASP._Page_Views_Home_Index_cshtml.Execute() in E:\Lab\WebAp\Views\Home\Index.cshtml:16

從來沒想過更新 JS 會讓整個網站掛點,起初以為是剛才更新 JS 時無意間動到什麼才搞壞系統,但反覆檢查後排除各種可能,直到將 JS 檔還原,系統立即恢復正常。嗯,我確定了一件事:有問題的 JS 的確有可能讓 ASP.NET MVC 網站的所有 View 壞光光!

回頭檢查剛才更新的 JS,發現我做了一件很豬頭的事,一時眼花誤把 TypeScript 當 JavaScript 複寫 JS 檔才搞飛機。因此,問題範圍進一步縮小到「 JS 出現 TypeScript 語法導致 ScriptBundle Crash」。在開發機重現問題並用消刪去法反覆測試過濾,找出是 "blah[]" 寫法踩到 ScriptBundle Minifier 元件(Microsfot.Ajax.Utilities.Minifier)的Bug,讓 ScrptBundle Render 出錯,當出錯點在全站共用的 Layout cshtml,所有的 View 就會一次壞光光。

//test.js
function test() {
    a[];
}

直接傳入 "a[]" 給 MinifyJavaScript() 引發一模一樣的錯誤,證實了我的推論。

由此經驗,日後再遇更新 JS 導致全站出錯,就不會手忙腳亂了。

MS OracleClient 改用 ODP.NET 之數字型別差異

$
0
0

System.Data.OracleClient 被微軟宣告為過時不建議使用,是你知道我知道連獨眼龍都知道的事,硬要繼續用甚至會有效能懲罰。所以在維護舊專案時,看到還在用 System.Data.OracleClient 的程式,我都會順手換成 Managed ODP.NET。(若為 .NET 3.5 平台則只能用 Unmanaged ODP.NET)

近日踩到小鐵釘一根。

如下圖,程式原本使用 System.Data.OracleClient,執行正常:

改 using Oracle.ManagedDataAccess.Client 換用 Managed ODP.NET,出現型別轉換錯誤:

這才想起之前就研究過,呼叫 GetValue()、GetFieldType() 讀取 NUMBER 型別欄位,ODP.NET 會依 byte, short, long, single, double, decimal 的順序,使用第一個能完整容納精確數字的型別,與 System.Data.OracleClient 一律轉為 decimal 不同。在上述案例,改用 ODP.NET 後 dr["n"] 的型別為 double,硬用 (decmial) 轉型會出錯,修改為 Convert.ToDecimal()後,問題排除。

以上經驗提供需要處理 System.Data.OracleClient 轉 ODP.NET 的同學參考。

2017 三重馬

$
0
0

為達成上半年每月一的目標,報了前年跑過的三重馬,路線雖單調但好跑,補給花樣不多但平實,地點近加上報名費親民,算是鞏固業績的好選擇。

前年遇到下雨,今年氣象預報是多雲到晴的好天氣…(抖)

半馬晚十分鐘出發,全馬只有七百多人參賽,是我愛的「小而美」!

六點起跑,起跑沒多久看到紅色朝陽,是跑馬族(或是公園養生操阿公阿媽)比一般正常人更有機會看到的景色。

路線從三重重陽橋跑到八里媽媽嘴咖啡折返,來回 21K,半馬一趟,全馬兩趟。跑第二次已無驚喜感,破 SUB4 後對成績亦無期待,便輕鬆跑隨便看。補給就是簡單的水、運動飲料、香蕉、小蕃茄、盬、餅乾… 等,中規中矩,該有的都不缺。

但隨著里程增加,時間接近中午氣溫上升,加上不知是霧是霾,遠方一片茫然,路像是沒有盡頭一般,要不斷催眠自己「我不是在跑步,我是在修行」,才能動力跑下去 XD

河濱有一段路邊有人在曬米白色的不知名粉屑狀物,散發微酸但又說不出是什麼的刺鼻味,應是老天為增加修行難度所安排的關卡。

天熱難當,但最後咬牙少走多跑,再添一場 SUB5。

回家想起,找出 Fenix 3 的溫度歷程推算,終點前在太陽底下跑的那段路,溫度高達 35 呀 Orz

補上完賽獎牌照,還不錯看,再下一馬。

【茶包射手日記】VBScript ASC() 中文傳回 63

$
0
0

同事報案,某上古神獸古老 ASP VBScript 移至 Windows 2012R2 x64 主機後執行出誤,深入追查,問題出在執行 ASC() 解析中文字元一律傳回 63 (?)。

首先聲明,ASC() 並不支援 Unicode,理應改用 ASCW() (參考:1 2),但舊程式汰換在即,能運行就不想投資時間修改重測。程式原本在 Windows 2003 x86 執行正常,一開始以為是 Windows 版本較新造成 VBScript ASC() 行為改變,寫了一小段檢測程式在本機 Windows 10 測試,一樣是新版 Windows,執行結果卻與問題主機不同:

WScript.Echo ASC("黑")
WScript.Echo ASCB("黑")
WScript.Echo ASCW("黑")

陸續找了 Win 8.1、Win 10、Win 2008R2、Win 2012R2,中文英文都有,發現所有主機的執行結果都跟舊主機一致,唯一傳回 63 的只有出問題的 Win 2012R2。

相同作業系統卻有不同結果,問題又可反覆重現,只要找出正常主機跟問題主機的環境差異,就能找出答案。

最後,關鍵在意想不到的地方!

問題主機的國別格式被設成「Match Windows display language」,由於是英文版,啟用中的國別設定是 English(United States),而其他測試正常的主機格式設定都是台灣。而將原本正常的英文版 Windows 2012R2 格式改成 United States,也能重現 ASC(中文字元) 傳回 63 的現象。實驗如下:

這也太奧妙了,我一直以為格式只影響的是日期、時間、數字的格式偏好,語系編碼應該是看 System Locale 才對(如下圖所示,安裝時早已設好 Chinese (Traditional, Taiwan) ),萬萬沒想到 VBScript 的 ASC() 傳回值竟會受日期、數字格式設定影響!

問題在調整國別格式設定後排除,但留下一個疑點。問題主機為 Web Farm,事後檢查多台主機的格式都被改成 Match Windows display language,眾人都有印象當初已調整成台灣,有什麼原因造成整批主機設定異動,原因成謎。

ODP.NET 無法讀取 Oracle 欄位計算結果

$
0
0

同事報案,使用 Dapper + ODP.NET 呼叫某 Procedure,以 Ref Cursor 取資料時出現型別轉換錯誤,一路深入追查,發現問題跟是否用了 Procedure、Ref Cursor、 Dapper 都沒有關係,錯誤發生在 ODP.NET 層。

有問題的查詢涉及幾個高精確度的欄位運動,經過一番簡化,我先找出用下列查詢可重現問題。

使用 PL/SQL Developer 查詢不會出錯,但使用 ODP.NET OracleDataReader dr["N"]、dr.GetDecimal(0)、dr.GetValue() 都會出現「指定的轉換無效」(Invalid Cast Exception)錯誤,dr.GetDouble(0) 則能讀出結果:

由於出錯案例的爆點在幾個高精確度數字(NUMBER(14)、NUMBER(10,7))的乘除結果,一度以為跟運算成員的欄位精確位數大高造成的。

經過一番調查,我才搞懂背後發生什麼事,並有了新結論:

  1. Oracle NUMBER的最高精確位數高達 38 位,C# decimal只有 28-29 位,當 Oracle 欄位值高於 28 位,ODP.NET 祭出 .NET 世界精準度最高的 decimal 也裝不下,引發轉型別轉換失敗錯誤。(延伸閱讀:ODP.NET 如何決定數值欄位的 .NET 對應型別?
  2. 其實不必動用超大位數數字進行運算,只要一個簡單的除法搞出無窮小數就能重現 Oracle 位數超出 decimal 負荷的狀況。
  3. 當未指定型別,Oracle 會以 NUMBER(預設 38 位最高精確度)傳回運算結果,換言之,只要遇到無窮小數就會傳回滿滿 38 位數字讓 decimal 難看。
  4. PL/SQL Developer(或其他 Oracle 查詢軟體)顯示的結果小數位數是經過處理的,並非 Oracle 傳回的原始內容。

讓我用一個例子證明 1、2 兩點:

class Program
{
staticvoid TryGetValue(OracleDataReader dr, int n)
    {
try
        {
            Console.WriteLine($"N{n}={dr.GetValue(n)}");
        }
catch (Exception ex)
        {
            Console.WriteLine($"N{n} GetValue Error = {ex.Message}");
        }
    }
staticvoid DumpBinData(OracleDataReader dr, int n)
    {
string binData = BitConverter.ToString(dr.GetOracleDecimal(n).BinData);
        Console.WriteLine($"N{n} .BinData = {binData}");
    }
 
staticvoid Main(string[] args)
    {
using (var cn = new OracleConnection(Helper.GetCnnStr()))
        {
 
            cn.Open();
            var cmd = cn.CreateCommand();
            cmd.CommandText = @"
SELECT         
100/17 AS N0,
CAST(100/17 AS NUMBER(*,28)) AS N1,
CAST(100/17 AS NUMBER(*,29)) AS N2,
CAST(100/17 AS NUMBER) AS N4
FROM DUAL";
            var dr = cmd.ExecuteReader();
            dr.Read();
for(var i = 0; i < 4; i++)
            {
                Console.WriteLine($"---N{i}---");
                TryGetValue(dr, i);
                DumpBinData(dr, i);
            }
        }
        Console.Read();
    }
}

我用 100/17 製造出包含無窮小數的數值,再分別 CAST 換成 NUMBER(*, 28)、NUMBER(*,  29) 及 NUMBER 三種型別。結果只有轉成 NUMBER(*, 28) 的欄位能被正確讀取,其餘的都發生轉型失敗。另外,由 OracleDataReader.GetOracleDecimal().BinData 可以檢視 Oracle 傳回的原始資料,N0(未指定型別)及 N4(轉型為 NUMBER)的 BinData 是相同的,足以證明當未指定型別時,Oracle 採用最大精確度的 NUMBER 型別。

所以,當查詢結果出現除不盡狀況的狀況,其真實數字位數會高達 38 位,由 Oracle 工具軟體看到的結果是經過四捨五入的結果,如第一張圖例的 PL/SQL Developer 取 16 位,不同的工具結果不同,例如改用 SQLPLUS,位數就只有 9 位。

所以,不要拿工具軟體查詢到的數字當成標準答案!

最後,好奇在 SQL Server 上會不會遇到同樣的狀況?答案是不會:

SQL 整數相除結果預設為整數,非整數相除預設精準度預設為 Single,除非刻意轉型讓位數破錶,否則不會出錯,而錯誤訊息為「轉換溢位」較明確。

結論:

當在 Oracle 查詢使用除法運算,請記得轉型讓精準度夠用就好,否則一旦出現除不盡的狀況,你就有得忙了。


LINE 反斜線變日圓符號(¥)之謎

$
0
0

電腦版 LINE 輸入與顯示時老將磁碟路徑的反斜線符號「\」改成日圓符號「¥」,挺困擾的:

推測可能與 Meiryo 明瞭體字型有關。如果你的 Windows 有安裝日文語言選項,以下網頁可證明在 Meiryo 字型中反斜線符號會變成 ¥。

原因出在並不是每個國都使用 \ 作為路徑分隔字元,日本是用 ¥,而韓國則是用 ₩。參考 

維基百科找到日韓鍵盤配置圖範例,右上角 Backspace 左側我們習慣的反斜線鍵位置,在日本為 ¥,在韓國為 ₩,也可證明這點:

知道是字型問題就有了方向,試著修改聊天字體顯示,由預設字體改為 Arial Unicode MS 或其他我們常用的中文字型:

實驗發現,聊天內容中的反斜線變正常了,但輸入文字區仍然不對。

將介面語系改成英文,文字輸入區的反斜線顯示就正常了。

再查深一點,在 C:\Program Files (x86)\Naver\LINE\res\skin\basic\css\common.cs 可以找到 LINE 借用 CSS 指定不同國別語系的 UI 字型,在 Chinese-Taiwan 部分 font-family 優先指定 Meiryo,而 English 部分則為 Tahoma,這就解釋了為什麼切成英文一切正常。

既然有 CSS,我們可以自己動手修正它嗎?本來可以,但後來 LINE 新版本似乎改了架構,將這些樣式設定打包成單一檔案(Qt Resource?),無法直接更改,既然不開放使用者客製調整,這問題就只能靠 LINE 自已修正了。

使用 WebClient FTP 上傳檔案發生 553 錯誤

$
0
0

在 .NET 要 FTP 上傳檔案,最精簡有效的做法莫過於使用 WebClient,例如:

using System;
using System.IO;
using System.Net;
 
publicclass CSharpLab
{
publicstaticvoid Test()
    {
string userName = "ftpAccount";
string password = "ftpPassword";
string uploadUrl = "ftp://myFtpServerHost/someFolder/test.txt";
byte[] data = newbyte[] { 0x31, 0x32, 0x33 };
        WebClient wc = new WebClient();
        wc.Credentials = new NetworkCredential(userName, password);
        wc.UploadData(uploadUrl, data);        
    }
}

這個寫法在專案中廣泛運用多年都沒遇到什麼問題。近日同事在 FTP 上傳某一主機時,卻發生 553 File name not allowed 錯誤:System.Net.WebException: The remote server return an error: (553) File name not allowed

手動 FTP 登入上傳,確定路徑、權限都沒有問題。爬文發現,原來 Linux FTP Server 與 IIS FTP Server 存在行為差異

Linux FTP Server 不像 IIS 有共用的 FTP 根目錄,登入後會處於該帳號的使用者根目錄(例如:/home/username),因此對Linux FTP Server,ftp://myFtpServerHost/someFolder/test.txt 將指向 /user/home/someFoler/test.txt,如果要指向絕對路徑,需多加一根「/」,寫成 ftp://myFtpServerHost//someFolder/test.txt 。

過去面對的 FTP 主機都是 Windows 故沒發現,第一次遇上 Linux FTP 主機才學到這點。問題在修改 URL 多加 / 後排除,結案。

閒聊:用 LINQ 還是自己寫 SQL?

$
0
0

前陣子在網路看到「該靠 EF(LINQ)還是自幹 SQL 語法(甚至一律轉成 SP)」 的討論,我的「個人偏好」挺明確-CRUD 可靠 EF/ORM 省工,複雜查詢或操作則走 Dapper自己寫 SQL。不過它只算是「偏好」,其效益因客觀條件劇烈變動,若無視開發者背景、人力資源配置、系統需求等各種因素,無限期支持 OOO 一定比 XXX 好,肯定會在特定情境踩坑。既然沒有一體適用的「建議」,我就只從開發老人的角度聊聊決策理由及優劣分析,不陷入追求「最佳解」的迷思。(充其量只會有符合某種情境的「最適解」)

依據看過的專案實踐,我把在 .NET 執行 SQL 邏輯的策略分成三種:EF(LINQ)、自幹 SQL 語法、將邏輯都放入 SP,先分析一下優缺點:

EF(LINQ)

優點:

  1. 開發人員不需要太多太深的 SQL 語法與知識,只需學好 LINQ,由 EF 負責翻譯成具備專業水準的 SQL 語法
    (EF 甚至已考慮許多連中鳥也忽略的細節,例如:讀取資料再更新前先比對欄位是否已被第三方異動,避免更新衝突。)
  2. 支援強型別及 Intellisense,不必擔心敲錯欄位名稱
  3. 相較於傳統 ADO.NET 新増與更新,寫法簡潔許多(例如:一行更新
  4. 完全杜絕 SQL Injection 風險
  5. 支援跨資料庫開發,實現「可抽換不同廠牌資料庫」的夢想

缺點:

  1. 難以應用 SQL 語法專屬特性簡化問題或提升效能(例如:CTE、Cursor、Index Hint…)
  2. 將複雜查詢邏輯轉成單一 LINQ 查詢很耗腦力
    (造成有些開發者會選擇將資料分批拉回再用 .NET 處理,其效率不如在 DB 端直接完成)
  3. 在某些狀況下 LINQ 可能被轉成較無效率的語法,效能低於預期

自幹 SQL 語法

優點:

  1. 得以完全發揮 SQL 語法特性,展現極致效能
  2. 直接提供 SQL 可省去由 LINQ 轉換的過程,效率略好
  3. 開發者充分掌握最終 SQL 指令,較能精準控制執行效能、鎖定範圍等細節
    例如:一次更新多筆資料的某個欄位
  4. 必要時可依 SQL 與 C# 強項拆解運算邏輯,由 SQL 查詢取得半成品再以 C# 加工轉為最終結果,靈活分工可節省可觀開發時間

缺點:

  1. 可能徹底誤用 SQL 語法特性,引發驚人災難(水能載舟,亦能覆舟呀)
  2. 有寫出 SQL Injection 的可能 Orz
  3. 遇到 INSERT/UPDATE 時要逐一列舉欄位名稱、宣告欄位值變數,超級囉嗦
  4. 綁死資料庫廠牌甚至版本,大幅提高抽換資料庫難度

一律寫成 SP

優點:

  1. SP 經過事先編譯,效率優於 .NET 動態傳入 SQL
  2. 採此模式時,一般多由專屬有經驗人員撰寫,與放任開發者自由發揮相比,較易管控品質
  3. 邏輯集中於 SP,有利於跨平台共用(例如:從 Java、PHP 也可直接引用)

缺點:

  1. 所有邏輯限定只能用 SQL 語言(T-SQL、PL/SQL…)實現,難度較高,需要更多相關知識才能勝任
  2. 某些情境將邏輯拆分成兩段,.NET 與 SQL 分別處理自己擅長部分是最省時省力的做法,限定在 SP 實現一切時就喪失此優勢
    (SQLCLR 算是例外,但與純 .NET 相比仍有限制)
  3. SP 的開發偵錯環境遠不如 .NET 程式便利(尤其 Visual Studio 一出,誰與爭峰?) 
  4. 所有運算負擔集中在資料庫端,無法靠增加 .NET 中台、前台主機數量提高系統產量

 

當優缺點已知,依據開發者背景、團隊人力配置、專案需求,要做出抉擇便非難事:

  • 如果你有一批經驗豐富,有能力用 PL/SQL 或 T-SQL 刷 LeetCode 面試題庫的專屬開發人員,就全部寫 SP 吧!不要為難前後端的 .NET 開發人員。
  • 如果系統要求將來資料庫平台可以抽換,EF 是較省力選擇,避免日後換掉 DB 痛到像剝皮。
  • 如果你的開發團隊 SQL 知識背景不深,使用 LINQ 可以確保大家完成水準之上的 SQL 相關系統,還不必擔心豬隊友搞出 SQL Injection 讓系統裸奔。
  • 如果你的系統一秒鐘幾十萬上下、資料量龐大,對資料庫一丁點效能提升也錙銖必較,那麼別猶豫,SQL 絕對要自己寫。

回到我的個人觀點,明知 .NET 端有一堆神兵利器還限定自己只用 T-SQL、PL/SQL 解決問題,是信仰堅定的苦行僧才走的修行之路,專案團隊傾向全端開發沒有專門研究 DB 的人員,全面 SP 化對我來說太苦太難成本太高,優先排除。 那我為什麼偏好「查詢用 Dapper,CRUD 用 EF/LINQ」?

我愛死在 C# 輸入屬性名詞前幾個字,Visual Studio 帶出完整名稱,不用擔心奶油桂花手敲錯字 Bug 半天。而 new EntityObject,Add 後 SubmitChanges 就完成新増,跟自己組 INSERT INTO TXX (C1,C2,C3…) VALUES (@C1,@C2,@C3…),再 AddParameter("@C1")、AddParameter("@C2")、AddParameter("@C3") 相比,根本是跟人火併要帶手槍還是揮球棒的問題呀!

做 CRUD 借助 EF/ORM 省時又省力,是明智之舉。至於查詢,則有 Dapper 自己寫 SQL vs 寫 LINQ 轉 SQL 兩種選擇,要怎麼選?依我看法,SQL vs LINQ 就像手排車 vs 自排車的差異。

自排固然方便,但要能充分發揮車輛性能極致,手排才是王道!在今天,馬路上跑的幾乎全是自排車,在 F1 賽車界依舊是手排的天下:來源

在分秒必爭的賽道中,搭載扭力轉換器傳輸動力、透過排檔電腦決定檔位的傳統自排變速箱,雖然便利、讓車手可以專注在賽道路線的攻略,但是檔位的切換與動力的傳輸卻顯得遲緩且不直接,無法確實將檔位與引擎轉速維持在車手所需的範圍,結果就是影響實際的單圈速度。
即使傳統手排在主流賽事中已不常見,但追根究柢,序列式變速箱、自手排變速箱仍舊是以手排變速箱為基礎所發展而來,若以廣義而論,手排仍是賽車運動中的主流。

手排更能貫徹車手的意志,是好事也是壞事。手排讓舒馬克在賽道上風馳電掣,也讓菜鳥上坡熄火倒滑害你驚呼What The Fxxk。

EF 查詢轉換 SQL 的過程有許多眉角不易掌控,而只有自己寫 SQL 才能善用 CTE、Index Hint、WITH (NOLOCK) 等技巧讓查詢效能最佳化,我面對的系統需求甚少有抽換 DB 的可能,但對系統效能有較高要求,故只能假裝自己是專業賽車手,不妄想開自排一路打 D 檔拿下名次。(資安宣導:自己寫 SQL 請務必使用參數化查詢,提醒愛用參數組 SQL 字串的同學,閻羅王最近針對 SQL Injection 開發者研發了一批專用刑具在等著你們…)

你不會因為換手排車就會開比較快(也有可能是死比較快),走這條路必須投資時間學習 T-SQL、PL/SQL 語法、查詢技巧、效能考量,搶了原本 EF 代勞的重責大任,知識與經驗不足很容易砸鍋,這也是選擇自己寫 SQL 前必要的認知。

EF 與自寫 SQL 併用的組合,像是用高階語言快速開發,但易成瓶頸的重度運算改用 C/C++ 寫成程式庫從外部呼叫,試著各取其長處。這種做法比起純用高階語言複雜,需要多懂 C 並存在沒管好 Unmanaged 記憶體當機的風險,但如果目標是要省時省力又兼顧效能,這是很值得考慮的做法。

最後我想說的是:不管自排或手排,駕駛永遠是關鍵,別當三寶…

CSV 轉換利器-ServiceStack.Text

$
0
0

做專案免不了遇到匯出或讀取 CSV 的需求,將物件轉成逗號分隔字串看似小菜一碟,用 C# 串字串也能搞定,但魔鬼在細節裡:字串值如包含逗號就要用雙引號包夾,遇到雙引號要置換成兩個雙引號,如果字串內容有換行符號更是讀取識別時的一大挑戰… 不管是匯出或解析 CSV 都得費不少力氣。最近發現一個處理 CSV 的強大元件-ServiceStack.Text 的 CsvSerializer!

ServiceStack是一套用於快速打造 SOA 服務的 Framework 工具組(可取代 WCF、WebAPI),強調輕巧、快速。ServiceStack.Text則是其中處理 JSON、CSV、JSV 序列化與反序列化的程式庫(在 ServiceStack 自家評測中 JSON 處理速度比 Json.NET 快三倍),而 CsvSerializer 正好可解決專案中的 CSV 需求。

以下簡單示範如何利用 ServiceStack.Text 匯出及解析 CSV。

首先使用 NuGet 安裝 ServiceStack.Text:

我用一小段程式做示範,測試對象是自訂物件陣列。

  • Test1() 用 CsvSerializer.SerializeToCsv<T>() 將陣列轉為 CSV 字串。
  • Test2() 用擴充方法 .FromCsv<List<T>>() 將 CSV 字串再轉回物件陣列。
  • Test3() 則嘗試不定義強型別物件,將 CSV 還原回字串陣列進行客製化應用。

為了增加趣味挑戰性,當然要刻意在物件字串屬性穿插逗號、雙引號及換行,試試 ServiceStack.Text 的能耐。

using ServiceStack.Text;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ServiceStack;
using Newtonsoft.Json;
using System.Dynamic;
 
namespace CsvTest
{
class Program
    {
publicclass Entity
        {
publicint Num { get; set; }
public DateTime Date { get; set; }
publicstring Text { get; set; }
publicbool Flag { get; set; }
 
public Entity(int num, DateTime date, string text, bool flag)
            {
                Num = num;
                Date = date;
                Text = text;
                Flag = flag;
            }
        }
 
static Entity[] TestData = new Entity[]
        {
new Entity(1, new DateTime(2012,12,21), "Normal", true),
new Entity(16, new DateTime(2012,12,21), "Taipei, Taiwan", true),
new Entity(32, new DateTime(2012,12,21), $@"""雙引號""跟,都來一下
換行當然不可少
", false)
        };
 
 
staticvoid Main(string[] args)
        {
//Test1();
//Test2();
            Test3();
        }
 
//物件陣列轉成CSV
staticvoid Test1()
        {
            File.WriteAllText("E:\\CSVLab\\Test1.csv", 
                CsvSerializer.SerializeToCsv<Entity>(TestData),
//指定new UTF8Encoding(true)產生包含BOM標記的UTF8檔案
//不然Excel直接開啟會有亂碼
new UTF8Encoding(true));
        }
 
staticvoid Test2()
        {
            var csv = File.ReadAllText("E:\\CSVLab\\Test1.csv");
//記得using ServiceStack啟用擴充方法
            var data = csv.FromCsv<List<Entity>>();
            Console.WriteLine(JsonConvert.SerializeObject(data, Formatting.Indented));
            Console.Read();
        }
 
staticvoid Test3()
        {
            var csv = File.ReadAllText("E:\\CSVLab\\Test1.csv");
string[] propNames = null;
            List<string[]> rows = new List<string[]>();
foreach (var line in CsvReader.ParseLines(csv))
            {
string[] strArray = CsvReader.ParseFields(line).ToArray();
if (propNames == null)
                    propNames = strArray;
else
                    rows.Add(strArray);
            }
            Console.WriteLine($"PropNames={string.Join(",", propNames)}");
for (int r = 0; r < rows.Count; r++)
            {
                var cells = rows[r];
for (int c = 0; c < cells.Length; c++)
                {
                    Console.WriteLine($"[{r},{c}]={cells[c]}");
                }
            }
            Console.Read();
        }
 
    }
}

Test1() 順利地輸出 CSV,由結果驗證 CsvSerializer 遇到逗號時會自動加雙引號,遇到內含雙引號也懂得置換。

測試以 Excel 開啟匯出的 CSV,欄位分隔解析完全正確。(注意:匯出中文 CSV 時記得要傳 Encoding.UTF8 或 new UTF8Encoding(true) 參數避免亂碼。參考

Test2() 將 CSV 還原物件陣列也成功!

Test3() 使用 CsvReader.ParseLine()、CsvReader.ParseFileds() 將 CSV 拆解成多筆資料字串,再逐筆依欄位分解成字串陣列,有自己土砲過的人就知道要判斷 逗號 vs 夾在雙引號中的逗號、換行 vs 夾在雙引號中的換行 有多煩人,有了 ServiceStack.Text,一切簡單多了!

工具箱再添順手兵刃一件!

陸續接獲網友回饋:補充其他處理 CSV 的好選擇:

花 8.29 英鎊拯救世界,WannaCrypt 勒索病毒中場休息

$
0
0

好戲劇化的發展,震驚全球的 WannaCrypt 勒索病毒(嚴格來說是蠕蟲),在一位英國資安研究員註冊某個網域名稱後,中止了第一波攻擊。(讓我想起電影世界大戰裡莫名烙賽停擺的外星人)


照片來源:http://thestagblog.com/tuesdayapocalypse-waroftheworlds/

相信大家應該都從各大媒體看到報導了,WannaCrypt 勒索病毒參考先前美國國安局流出的攻擊程式,利用 Windows 的一個資安漏洞(微軟於今年 3 月已釋出安全更新),能主動攻擊感染同一區域網路(或直接曝露於 Internet)未做 Windows Update 的 Windows Vista/7/8.1/2008R2 主機,將該主機的所有文件檔加密上鎖,要求相當美金 300 元的比特幣贖金。先在英國健保署、西班牙電信公司傳出嚴重災情,俄羅斯、烏克蘭與台灣也受害嚴重,依據防毒公司 avast 的統計,全球共有 104 國家受害,超過 12 萬 6 千台機器被感染。而微軟為原本已停止支援多時的 Windows 2003/XP 破例緊急出了安全更新,可見事態之嚴重。

但 WannaCrypt 的這波猛烈攻擊在英國一位資安研究員註冊了某個網域後意外停止了。

經營 MalwareTech 部落格的一位英國 22 歲年輕資安研究員分享他意外拯救世界的經過

在 WannaCrypt 災情傳出後,MalwareTech 取得在英國健保署肆虐的 WannaCrypt 勒索軟體樣本準備進行研究,丟進隔離環境執行時,發現 WannaCrypt 會一直嘗試存取某個未註冊的網域名稱。

MalwareTech 在研究惡意程式時習慣會註冊拿下這類網域名稱,目的在於蒐集感染數據及研究破解之道,於是這回先花 8.29 英鎊註冊再說。

有趣的是,註冊網域名稱生效後,他接到其他研究員詢問,表示樣本似乎出了問題,已無法重現感染行為。

之後經過反組譯勒索軟體以及模擬未註冊網域情況,驗證了「勒索軟體只要檢測到該網域存在就會停止執行」,而網域名稱寫死在程式裡(有些惡意軟體會使用演算法動態改變使用的網域名稱,或同時檢查多個網域名稱才決定),換言之,這個網域名稱是當初設計用來停止活動的開關(概念是在研究室的沙箱環境中,連不該存在的網域也會有回應,此時惡意軟體會停止活動避免行跡敗露),除非勒索軟體改版,這波所散佈的 WannaCrypt 勒索病毒,都將因為 MalwareTech 註冊網域而中止活動,這波的攻擊應該已告平息。

不過,可以預見勒索病毒作者一定會很快改版,試圖捲土重來,大家快利用這個天下掉下來的機會,趕快檢查自己的 Windows Vista/7/8/2008 R2 是否已安裝好 MS17-010安全更新!(Windows 10 有強迫安全更新,風險較低)

【2017-05-14更新】

看來中場休息結束惹,傳出已有變種出現:勒索病毒WannaCry變異 傳播速度恐更快 - 中時電子報

Viewing all 428 articles
Browse latest View live