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

客製靜態檔案 HTTP 404 訊息

$
0
0

同事報案,某組 Windows 2012R2 Web Farm 均已設定 web.config <customErrors mode="On" /> HTTP 404 網頁理應如下:

但 Web Farm 其中一台卻會顯示詳細錯誤,導致實體路徑資訊外洩:

最後同事找出原因,IIS Error Pages 設定有個 Edit Feature Settings,問題主機被設成「Detail Errors」:

心中對這組設定與 customErrors 的關係滿心狐疑,爬文後才驚覺自己寫過文章:ASP.NET 相關程式錯誤由 <system.web><customErrors> 控制,靜態檔案(html、gif、png、jpg、js、css)則由 <system.webServer> <httpErrors> 決定,不過一年多前的事竟忘得一乾二淨,特再撰文一篇加強印象,確保此生不忘,阿彌陀佛~


Dapper +Oracle 之 DateTime 注意事項

$
0
0

同事報案,我先前寫的 Dapper 共用程式庫有 Bug,當 WHERE 條件包含日期型別時,將 DateTime 寫入 Oracle Date 欄位,接著用同 DateTime 值做 WHERE 比對,竟找不到剛才寫入的資料。

用以下範例重現問題:

using (var cn = new OracleConnection(csOra))
{
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)", new { idx, dttm });
//重新該時間當WHERE條件比對,筆數為0(登楞!)
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm", new { dttm }).Count();
    Console.WriteLine($"Count={cnt}");
}

第一時間懷疑是 Oracle 的 Date 欄位精準度只到秒造成,元件配合 SQL 已使用好一陣子沒遇過此狀況,應是 SQL DateTime 欄位精準度可到秒以下,故同樣做法在 SQL 不會出錯:

using (var cn = new SqlConnection(csSql))
{
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (@idx, @dttm)", new { idx, dttm });
//重新用DateTime.Now當WHERE條件比對
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = @dttm", new { dttm }).Count();
//筆數為1,測試無誤
    Console.WriteLine($"Count={cnt}");
    Console.Read();
}

有趣的是,若 Oracle Date 搭配 .NET DateTime 有此陷阱,過去使用 ODP.NET 何以相安無事多年?於是我改用 OracleCommand、OracleParameter 做測試,也不會有日期比對不符的狀況:

using (var cn = new OracleConnection(csOra))
{
    cn.Open();
    var cmd = cn.CreateCommand();
    cmd.CommandText = "TRUNCATE TABLE JEFFTEST";
    cmd.ExecuteNonQuery();
    var dttm = DateTime.Now;
    var timeStr = dttm.ToString("yyyy/MM/dd HH:mm:ss");
    cmd.CommandText = "INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)";
    cmd.Parameters.Add("idx", OracleDbType.Decimal).Value = 1;
    cmd.Parameters.Add("dttm", OracleDbType.Date).Value = dttm;
    cmd.ExecuteNonQuery();
 
    cmd.CommandText = "SELECT * FROM JEFFTEST WHERE DTTM = :dttm";
    cmd.Parameters.RemoveAt(0);
    var dr = cmd.ExecuteReader();
if (dr.Read())
        Console.WriteLine(dr["DTTM"]);
else
        Console.WriteLine("No Data");
 
    Console.Read();
}

嗯,很好,所以這是 Oracle + Dapper 衍生出的新問題。弄一段程式碼驗證問題及解決方法,說明寫在註解裡,直接看 Code:

using (var cn = new OracleConnection(csOra))
{
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
    var timeStr = dttm.ToString("yyyy/MM/dd HH:mm:ss");
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)", new { idx, dttm });
//從資料庫取出剛才存入的日期備用
    var dttmInDb = cn.Query<DateTime>("SELECT DTTM FROM JEFFTEST WHERE IDX=1").Single();
//重新用DateTime.Now當WHERE條件比對,筆數為0
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm", new { dttm }).Count();
    Console.WriteLine($"使用原值WHERE比對 Count={cnt}");
//原因是Oracle DATE型別不包含毫秒
    Console.WriteLine($"DB {dttmInDb:HH:mm:ss.fff} vs C# {dttm:HH:mm:ss.fff}");
//把毫秒刪除試試
    var dttm2 = dttm.AddMilliseconds(-dttm.Millisecond);
    cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm2", new { dttm2 }).Count();
//登楞!筆數還是0
    Console.WriteLine($"AddMilliseconds修正後比對 Count={cnt}");
//換個方法,先轉字串再ParseExact轉回日期
    var dttm3 = DateTime.ParseExact(timeStr, "yyyy/MM/dd HH:mm:ss", null);
    cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm3", new { dttm3 }).Count();
    Console.WriteLine($"ToString+ParseExact轉換後比對 Count={cnt}");
//暗!這樣就找到得,為什麼?兩個時間看起來一樣,但CompareTo不同
    Console.WriteLine($"兩種修正結果看似相同 dttm2={dttm2:HH:mm:ss.fff} vs dttm3={dttm3:HH:mm:ss.fff}");
    Console.WriteLine($"dttm2.CompareTo(dttm3)={dttm2.CompareTo(dttm3)}");
//問題出在Ticks,二者Ticks不同
    Console.WriteLine($"關鍵在ms以下部分:dttm2={dttm2:HH:mm:ss.fffffff} dttm3={dttm3:HH:mm:ss.fffffff}");
//REF: http://stackoverflow.com/a/153014/4335757
    dttm2 = dttm.Trim(TimeSpan.TicksPerSecond);
    cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm2", new { dttm2 }).Count();
    Console.WriteLine($"Trim()取到秒再比對 Count={cnt}");
    Console.Read();
}

執行結果如下:

使用原值WHERE比對 Count=0
DB 21:22:22.000 vs C# 21:22:22.356
AddMilliseconds修正後比對 Count=0
ToString+ParseExact轉換後比對 Count=1
兩種修正結果看似相同 dttm2=21:22:22.000 vs dttm3=21:22:22.000
dttm2.CompareTo(dttm3)=1
關鍵在ms以下部分:dttm2=21:22:22.0004700 dttm3=21:22:22.0000000
Trim()取到秒再比對 Count=1

簡單來說,關鍵在 DateTime.Now 秒之下還有 Milliseconds 跟 Ticks,存入 Oracle Date 時只存到秒,而 Dapper 預設使用 Oracle Timestamp 型別(精準到 0.000001 秒)對應 C# DateTime 型別,實際的 WHERE 條是 Date 與 Timestamp 相比。一開始修正問題,以為 AddMilliseconds(-dateVar.Millisecond) 就夠,忘記更底下還有 Tick,比對還是失敗,改用 ToString 再 ParseExact 反而成功。最後我在 stackoverflow 找到漂亮的 Trim(TimeSpan.TicksPerSecond)函式完整截去秒以下部分,才算搞定。

關於 Dapper 將 C# DateTime 對應成 Oracle Timestamp 的行為,延伸到另一個更嚴重的議題:DateTime 被轉成 Timestamp 與 Oracle Date 欄位做比較將無法藉由索引加速,導致速度慢上五倍!參考)換句話說,即使截去小數秒結果正確,也將因 Index Scan 效能低落。網路建議解法是改寫 SQL 語法,寫成「WHERE Col = CAST (:dttm AS DATE)」,但我想到一招:其實大可不必花功夫捨去小數秒,因為在 OracleCommand 範例中,DateTime 搭配 OracleDbType.Date 就沒有小數秒差異問題。在一篇點部落文章看到 SqlMapper.AddTypeMap(typeof(DateTime), DbType.Date) 做法,要求 Dapper 將 DateTime 對應成 OracleDbType.Date,就完全不用煩惱小數秒差異囉!這應該是最簡潔有效的做法,經測試驗證,成功!

using (var cn = new OracleConnection(csOra))
{
//修改Mapping設定
    SqlMapper.AddTypeMap(typeof(DateTime), System.Data.DbType.Date);
 
    cn.Open();
    cn.Execute("TRUNCATE TABLE JEFFTEST");
    var idx = 1;
    var dttm = DateTime.Now;
//將DateTime.Now寫入資料庫
    cn.Execute("INSERT INTO JEFFTEST (IDX,DTTM) VALUES (:idx, :dttm)", new { idx, dttm });
//重新用DateTime.Now當WHERE條件比對
    var cnt = cn.Query("SELECT * FROM JEFFTEST WHERE DTTM = :dttm", new { dttm }).Count();
//筆數為1,測試無誤
    Console.WriteLine($"Count={cnt}");
    Console.Read();
}

【結論】

  • Dapper 搭配 Oracle 時,C# DateTime 預設會對應成 OracleDbType.Timestamp,當資料庫端欄位為 Date 型別,會因小數秒差造成 WHERE 比對不符,另外會因無法套用索引造成查詢效率不彰。
  • 在查詢語法中使月 CAST(:dateParam AS DATE) 轉型可以克服上述問題。
  • 若資料庫以 Date 為主未用到 Timestamp,透過 SqlMapper.AddTypeMap(typeof(DateTime), DbType.Date) 將 C# DateTime 改對應至 DbType.Date,可更巧妙避開問題。

【Coding4Fun】注音符號輸入字盤及國字轉注音解決方案

$
0
0

小木頭國文實力有點虛,看在常靠國文騙吃騙喝的老爸眼裡不免焦急。結果,皇帝不急急死太監,我跟著看課文、自己出測驗題,忙得不亦樂乎,但求力挽狂瀾…

但每次出題要輸入一串純注音符號總叫我抓狂,先前沒學到好方法在電腦快速輸入「純注音符號」?我只會用注音輸入法再選字,操作步驟略嫌繁瑣,加上平日用倉頡對注音輸入操作不熟,輸入速度跟繡花有得拼。

靈機一動,想到好久沒寫 WPF ,不如就寫個小工具練練功好了。拉個Gid,動態塞入注音符號按鈕,再用 KeyUp 攔截按鍵事件對應注音輸入法的相對按鍵,將輸入結果顯示到 TextBox.Text,按 Enter 將輸入結果複製到剪貼簿,再貼到 Word,一個滑鼠鍵盤兩用的注音符號輸入字盤就完成了,出題速度從此加快五倍呢!(新武器就位,小木頭發抖中… XD)

程式碼已放上 Github,有興趣的朋友請自行參考。

昨天在 Facebook 貼了影片,收集到好多網友熱心回饋的替代做法,一併整理如下:

  1. 使用「通用輸入法」自訂輸入字根對應注音符號(通用輸入法介紹)… 感謝 Ammon Lin 提供
  2. 使用純注音字型,示範影片… 感謝 Esmond Wang 提供
  3. 教育部中文轉拼音查詢  中文到注音轉換工具… 感謝 Jethro Yu 提供

好方法很多,但自已寫的永遠是最棒的,呵~

本日成語:敝帚自珍

自家的破掃帚,卻視如千金之寶。比喻極為珍惜自己的事物

VS2017 無法載入專案,出現 compiler could not be created 訊息

$
0
0

從 VS2017 RTM 起我就一律改用它開發專案,還算順利沒啥問題。今天則遇到一起小錯誤,某個從未用 VS2017 開啟過的專案,開啟時發生專案載入錯誤,出現以下訊息:

Project 'Blah' could not be opened because the Visual C# 2017 compiler could not be created. An item with the same key has already been added.

重試兩次狀況依舊,爬文求解。

找到不少相關討論,但案例集中在 ASP.NET 專案,推論與 IIS Express 有關,靠重設 IIS Express 設定可排除。但我的專案是 Windows Form 專案,情境明顯不同。在 stackoverflow 一則 ASP.NET 案例討論串我找到改用管理者模式開啟 Visual Studio 排除問題的經驗分享,雖然與其解決原理不符,還是抱著死馬當活馬醫心態一試,結果一改用管理者身分執行 Visual Studio 問題就消失了,之後改回一般身分執行,專案開啟編譯均正常。

無法重現問題無從深入調查,留個記錄供參。

OracleParameter 型別不符導致 ORA-03111 通訊中斷錯誤

$
0
0

記錄在 Oracle 遇到的古怪錯誤。

Oracle Server 版本 10.2.0.4 64bit,Client 端用 Managed ODP.NET 12.1.24160719(取自 NuGet),某段程式碼誤傳 Varchar2 OracleParameter 與 DATE 欄位進行比對,預期應出現型別不符錯誤,但得到錯誤訊息為 ORA-03111 在通訊通道上收到中斷訊號(Break received on communication channel):

該資料表有其他 DATE 欄位,將 WHERE 條件換成其他 DATE 欄位,也會觸發 ORA-03111。

另一個資料表也有類似 DATE 欄位,換資料表再測,錯誤訊息變回 ORA-01861 literal does not match format string,這才是字串日期型別不符應出現的訊息。

回到問題資料表,如果不比對 DATE 欄位,改成 to_date('20121221','YYYYMMDD') = :d,則訊息變成 ORA-01861。

將 Managed ODP.NET 換成 Unmanaged ODP.NET(2.112.1.0,取自 NuGet),得到的也是 ORA-01861:

結論:由以上推測這是一個 Managed ODP.NET 的 Bug,只在特定資料表誤用 VARCHAR2 OracleParameter 比對資料表 DATE 欄位時發生,出錯時傳回 ORA-03111 通訊中斷錯誤,而非預期的 ORA-01861 日期字串轉換錯誤。

前後測試了多個資料表,只有一個資料表能重現問題,判斷屬於「人品觸發式茶包」,嚴重等級不高。但由此經驗,未來若遇到 ORA-03111 錯誤,宜留意是否為其他錯誤造成。

由 Dapper 傳回 dynamic 物件取得欄位清單

$
0
0

不用預先宣告強型別,查詢資料表後直接傳回 dynamic 是 Dapper的強項,例如:var list = cn.Query("SELECT Col1,Col2 FROM T).ToList(); 將傳回 List<dynamic>,用 list.Fisrt().Col1 就能讀取欄位內容,簡潔又方便。

最近有個花式應用,想用通用函式接收 Dapper 查詢結果,自動列舉其中包含屬性(資料庫欄位)。一開始依循 System.Refelction 思維,想說用 GetType().GetProperties() 就可以搞定,不料踢到鐵板,Dapper 傳回的 dynamic 物件 .GetType() 結果為 null:

進一步追查,Dapper 傳回的 dynamic 骨子裡其實是個 DapperRow 型別,特別的是它實做了 IDictionary<string, object> 介面:

還記得既然要動態就動個痛快 - ExpandoObject介紹過 IDictionary<string, object> 的妙用,既然 DapperRow 是個 IDictionary<string, object> 一切好辦,轉型後用 .Keys 取回屬性清單,接下來就簡單了。

staticvoid Main(string[] args)
        {
string sql = @"
SELECT 1 AS ColNum, SYSDATE AS ColDate, 'HELLO' AS ColString FROM DUAL UNION
SELECT 2 AS ColNum, SYSDATE AS ColDate, 'WORLD' AS ColString FROM DUAL
";
using (var cn = new OracleConnection(cs))
            {
                var res = cn.Query(sql);
                StringBuilder sb = null;
foreach (dynamic rec in res)
                {
                    var d = rec as IDictionary<string, object>;
if (sb == null)
                    {
                        sb = new StringBuilder(string.Join("\t", d.Keys.ToArray()));
                        sb.AppendLine();
                    }
                    sb.AppendLine(string.Join("\t",
                        d.Keys.Select(n => 
                            d[n] == null ? string.Empty : d[n].ToString()).ToArray()));
                }
                Console.WriteLine(sb.ToString());
 
                Console.Read();
            }
        }
 

經實測可成功列舉欄位名稱及內容:

COLNUM  COLDATE COLSTRING
1       2017/3/22 下午 09:07:30 HELLO
2       2017/3/22 下午 09:07:30 WORLD

最後,回到 DapperRow.GetType() 傳回 null 的謎團上,照理來說,GetType() 源自 Object 物件,查過 DapperRow 的原始碼並沒有覆寫 GetType(),想不出傳回 null 的理由。於是把問題丟上 stackoverflow,很快就有高手出面解惑,原來 DapperRow 實做了 DynamicMetaObject,攔截所有 dynamic 形式的成員存取,只要遇到非欄位名稱就傳回 null,因此不只 GetType(),呼叫 AsEnumerable() 也會傳回 null,所有謎團都解開了,結案!

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,眾人都有印象當初已調整成台灣,有什麼原因造成整批主機設定異動,原因成謎。

Viewing all 2311 articles
Browse latest View live