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

【笨問題】Windows 8升級專業版及企業版

$
0
0

不得不承認我又耍笨了...

新買的VAIO T13內附已裝好所有驅動程式及VAIO 附屬軟體的Windows 8,由於少了Hyper-V及遠端桌面功能,我決定將它升級到MSDN訂閱提供的Windows 8 Enterprise版本。依過去經驗,要升級OS只需執行Windows安裝程式,就能從現有作業系統升級的選項,像這樣:

正期待安裝程式施展魔法把我的Windows 8變成企業版,忽然一盆冷水澆頭...

登楞!! Windows 8不能升級成Windows 8企業版,必須重灌??

TechNet上有份Windows 8升級途徑表,提到了XP、Vista、Windows 7可升級到Windows 8或Windows 8 Pro,而Win 7 Pro, Win 7 Enterprise及Win 8的大量授權版(Volume License)才能升級成Win 8 Enterprise,筆電所附的Windows 8應屬隨機版,此路不通。

既然升級不成,就下載好驅動程式,摸摸鼻子準備重灌Win 8 Enterprise。萬萬想不到,硬體、BIOS規格早已日新月異,老骨頭憑著五年前的知識想硬幹,猶如提著十字弓跟長劍參加諾曼地登陸,瞠目結舌之餘,下場淒慘。最後歷經十多個小時跨夜蠻幹不成,終告放棄(有個Firmware Extension Parser Device驅動程式因不明原因安裝失敗,留下兩個Unknown Device,ASSIST等專用鍵無作用,雖不影響使用,但就像全新螢幕上的兩顆亮點,會在奇檬子層次形成嚴重撕裂傷),所幸,還是習得少量知識,有些收獲:

  • 只懂得按F1、DEL進入BIOS畫面調開機設定? 只聽過主要分割跟延伸分割,沒聽過GPT分割? 請不要說你會裝電腦,UEFI時代已經來臨!  (聖光呀! 你有看到那些OEM磁碟分割、EFI系統磁碟分割跟修復磁碟分割嗎?)
  • 使用Windows 7 USB/DVD Download Tool製作Windows 8 USB安裝碟的做法似乎不適用UEFI模式,我實測的結果要切回傳統BIOS模式才能由USB開機。依論壇文章的說法,得使用指令格式化成FAT32及手動複製檔案。
  • USB 2.0真的太慢了!! 如果考慮要用USB碟安裝,建議買支USB 3.0吧! 以免寶貴歲月平白流逝外加不耐久侯可能爆血壓...
  • 不想把生命虛耗在等待USB 2.0傳檔,借到USB DVD光碟機,就沒有USB碟UEFI問題,總算順利安裝
  • VAIO T13開機要進入BIOS/UEFI設定畫面,實測無法靠開機時長按F11、DEL之類的做法,我試出來的方法是透過VAIO Care的還原功能指定重開機後進入修復畫面,在修復選單中再選擇進階模式於下次開機進入類似傳統BIOS ASCII形式的設定畫面,才能指定儲存裝置開機順序、開關Intel Virtualization Technology(預設關閉,記得要打開才能跑Win8模擬器)

年紀大了懂得不要跟電腦拼命,很認命地還原到出廠狀態,乖乖用隨機的Windows 8,以確保自己會體驗到機器的最佳配罝。

週五參加MVP聚會,恰巧向Bill叔提及Windows 8無法直接升級企業版,MSDN訂閱又沒有Windows 8 Pro版的遭遇,如獲當頭棒喝,一語驚醒夢中人... 原來我錯認了兩件事:

  1. MSDN下載區標為”Windows 8”的ISO檔,是所謂的All-In-One版本,隨著輸入產品金鑰不同,就可安裝出各種Windows 8版本,而且MSDN訂閱有提供Windows 8 Pro的產品金鑰
  2. Windows 8要升級成高階版本根本不需要用光碟片安裝,只要輸入其他版本的產品金鑰,Windows 8就會自動下載安裝額外功能,就地"進化"成高階版本。控制台有個"新增功能到Windows 8”就能幫你實現願望:

就這樣,原本鏖戰一畫夜而不得的Hyper-V及RDP,在輸入Windows 8專業版產品金鑰後半小時,就降臨到VAIO小筆電上囉~ (再次感謝Bill叔開示)


為網頁加入多點觸控功能(IE10版)

$
0
0

之前曾以iPad為對象寫過為網頁加入多點觸控功能範例,如今支援觸控的Windows 8筆電在手,不改寫成IE10版怎能止癢?

經過簡單研究,大致整理IE10與Safari/Chrome觸控事件差異如下:

  1. 事件名稱不同,IE使用的不是touchstart、touchmove、touchend等名稱,而是MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut, MSPointerHover... 等。另外,還有高階的手勢事件: MSGestureTap, MSGestureHold, MSGestureStart, MSGestureChange, MSGestureEnd, MSInertiaStart,讓開發者不用自己費心追蹤各指座標變化,就能判定使用者正在縮放、旋轉動作,真是佛心來著,將來肯定要抽空玩一下才不會暴殄天物。
  2. MSPointerDown等事件概念與touchstart也有所差異。在touchstart中會得到touches陣列,包含多個觸控點的資料;MSPointerDown則是個每個觸控點活動都會觸發一次。
  3. 滑鼠也會觸發MSPointer*事件! 如只想對觸控產生反應,可用if (e.pointerType == e.MSPOINTER_TYPE_TOUCH)過濾。
  4. 因為事件觸發基礎不同,原先在touchmove中可以透過changedTouches取得移動中的觸控點,使用IE10則要自行比對座標值該點是否在移動。
  5. 要攔截觸控事件的元素需加上CSS設定: -ms-touch-action: none; 防止跟IE10預設的縮放、移動瀏覽等觸控操作打架。
  6. 檢查navigator.msMaxTouchPoints屬性存在與否可偵測IE是否支援觸控,由navigator.msMaxTouchPoints亦可得到裝置所支援的觸控點數。

把握以上重點,經過簡單修改,支援IE10的多點觸控展示網頁就完成囉~ 線上展示

<!DOCTYPEhtml>
 
<html>
<head>
<title>Mutli-Touch Test</title>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.4.js"
type="text/javascript"></script>
<meta name="viewport"
     content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
        html,body {
/* 停用預設的縮放、移動瀏覽(Panning)等觸控功能, 由自訂程式接管 */
            -ms-touch-action: none;
        }
</style>
<script>
//http://msdn.microsoft.com/en-us/library/ie/hh673557(v=vs.85).aspx
        $.extend($.support, { ieTouch: "msMaxTouchPoints"in window.navigator });
        $(function () {
//檢查是否為支援觸控功能的IE瀏覽器
if (!$.support.ieTouch) {
                $("body").html("<span>IE on touchable device only!</span>");
return;
            }
//使用canvas繪製回應
var canvas = document.getElementById("sketchpad");
var ctx = canvas.getContext("2d");
//宣告變數儲存活動中觸控點的資訊
var touches = {}, changedTouches = {};
//傳回仿touchStart事件的touch物件
function createTouchObject(e) {
return { identifier: e.pointerId, pageX: e.clientX, pageY: e.clientY };
            }
            canvas.addEventListener("MSPointerDown", function (e) {
//由於滑鼠也會觸發,先檢查pointerType,只針對觸控做反應
if (e.pointerType != e.MSPOINTER_TYPE_TOUCH) return;
var t = createTouchObject(e);
                touches[t.identifier] = t;
            });
            canvas.addEventListener("MSPointerMove", function (e) {
if (e.pointerType != e.MSPOINTER_TYPE_TOUCH) return;
var t = createTouchObject(e);
var origT = touches[t.identifier];
                touches[t.identifier] = t;
//與前次保存資料比較,偵測點觸控點是否移動
if (origT &&
                    Math.abs(t.pageX - origT.pageX) + Math.abs(t.pageY - origT.pageY) > 1)
                    changedTouches[t.identifier] = t;
else
                    delete changedTouches[t.identifier];
            });
            canvas.addEventListener("MSPointerUp", function (e) {
if (e.pointerType != e.MSPOINTER_TYPE_TOUCH) return;
var t = createTouchObject(e);
//停止活動,將觸控點自touches及changedTouches移除
                delete touches[t.identifier];
                delete changedTouches[t.identifier];
            });
//定義不同顏色用來追蹤多點
var colors =
                ("red,orange,yellow,green,blue,indigo,purple," +
"aqua,khaki,darkred,lawngreen,salmon,navy," +
"deeppink,brown,olive,violet,tomato,gray").split(',');
//在canvas繪製追蹤點
            ctx.lineWidth = 3;
            ctx.font = "10pt Arial";
var r = 40;
function drawPoint(i, x, y, c, id, chg) {
                ctx.beginPath();
                ctx.fillStyle = c;
//若屬changedTouches則顯示黑框
                ctx.strokeStyle = chg ? "#000" : c;
                ctx.arc(x, y, r, 0, 2 * Math.PI, true);
                ctx.fill();
                ctx.stroke();
//顯示touch的identifier及其在陣列中的序號
//touches在上排藍字,chagedTouches在下排紅字
                ctx.fillStyle = chg ? "red" : "blue";
                ctx.fillText(id,
                    x - r, y - r - 25 + (chg ? 15 : 0));
            }
//清除canvas
function clearCanvas() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
            }
//利用identifier識別,相同時要保持同一個顏色
var touchHash = {}, colorIdx = 0;
function getColor(id) {
if (touchHash[id] == undefined)
                    touchHash[id] = ++colorIdx % colors.length;
return colors[touchHash[id]];
            }
//每秒更新20次,顯示目前的多點觸控資訊
            setInterval(function () {
                clearCanvas();
//console.log(JSON.stringify(touches));
for (var i in touches) {
var t = touches[i];
                    drawPoint(i, t.pageX, t.pageY, getColor(t.identifier),
                              t.identifier);
                }
for (var i in changedTouches) {
var t = changedTouches[i];
                    drawPoint(i, t.pageX, t.pageY, getColor(t.identifier),
                              t.identifier, true);
                }
            }, 50);
        });
</script>
</head>
<bodystyle="padding: 0px; margin: 0px;">
<canvasid="sketchpad"width="1366"height="760"style="border: 1px solid gray">
</canvas>
</body>
</html>

實測結果如下: (幸好最多只有10點,不然連腳都要舉起來"觸控"才能完成測試,肯定抽筋)

VS2012將不再支援Web Deployment Project

$
0
0

原本使用VS2010維護的專案,改用VS2012開啟時出現Web Deployment Project不相容的警示,爬文後發現,該跟這位VS2005起一路相隨的老朋友說再見了。

有篇MSDN Blog詳細解釋了此一決策的"心路歷程"(參考: Plans regarding Website projects and Web Deployment Projects),簡單摘要如下:

在VS2012開發階段時,開發團隊考慮了對WAP(Web Application Project,ASP.NET MVC也算)及WSP(Web Site Project)部署作業上的改良,限於資源有限,無法在上市時讓WAP及WSP都具備同等部署能力。最後的決策是先從WAP下手,理由是WAP已支援MSDeploy及MSBuild整合,而WSP在這方面則付之闕如,故策略是柿子挑軟的先吃先強化WAP部署功能,待VS2012後續Release再為WSP補齊與WAP同等的部署能力,例如:

  • 支援MSDeploy / MSDeploy package / FTP / File System / FPSE
  • 可設定多組部署Profile並保存在版控系統
  • 命令列執行
  • 部署時自動調整web.config
  • 部署時啟動Entity Framework CF migrations
  • 遞增式資料庫Schema部署
  • 檔案預覽
  • 其他

未來WAP與WSP將會使用同樣的部署模組,但現階段,WAP部署介面已改為.NET寫的版本,WSP則還是Unmanaged版。未來WSP專案可能會增加_Publish之類的資料夾及一些特殊檔案(.pubxml)以配合MSBuild的批次作業模式。換句話說,WSP的部署功能現在還沒完全Ready。

至於Web Deployment Project(WDP),由於其功能未來都可會被WSP內建的部署機制,而WDP要修改到能支援VS2012工程浩大,RD最後的決定是柿子挑軟的吃將時間投資在WSP部署功能的開發,使WAP與WSP的部署經驗一致。這算是好事一椿,但背後的意義是: 再會了,Web Deployment Project~

VS2012 WSP部署功能補強何時釋出,尚無明確時間表,在此之前如有MSBuild等自動部署需求,恐怕還是得招喚WDP再撐一陣子囉。(WDP含淚表示: ...)

再談鋰電池保養: 吃半飽活得老

$
0
0

四年多前寫過一篇關於電池保養的迷思,當時老同事Ryan提問:

SONY VAIO的NB在哪裡設定電量低於某設定值才開始充電啊?  如果沒的更改設定, 到底是管他三七二十一只要用電腦就插著外接電源, 還是應該充飽電就因該關閉電源改用電池電力直到耗盡呢?

沒想到五年後緣起一場"手滑意外",忽然間我會回答這個問題了! XD 雖然遲了五年,還是要認真:

在"VAIO控制中心/電源和電池"有個"電池保養"選項:

開啟後可設定筆電充電到80%甚至50%後就不再充電,以減緩電池衰退速度,延長使用年限。但代價是萬一臨時需要"Unplug"不插電演出,就只有八成或半顆電池可用,續航力大減。故如要長時間依賴電池前,得預先關閉電池保養功能將電充飽,徒增些許不便。

由以上設計不難推測,長期只充八分飽或半飽是讓電池延年益壽的養生之道。而我找到一篇Sony工程師對Battery Care原理的詮釋:

The Battery Care Function helps conserve battery life to assure extended use. Normally, a battery is recharged 100%. However, this function extends battery life by resetting full recharge capacity between 50% (super-caring recharge mode) and 80% (caring recharge mode). It is well known that repeated “additional recharge” of nickel-cadmium or nickel hydride batteries affects the battery (lazy battery effect) to gradually reduce the battery recharge capacity and ultimately shorten battery life. However, the lithium-iron batteries of the VAIO G have different properties. Additional battery recharging does not have the same effect on the memory of this type of battery but it is still beneficial to keep battery recharge capacity at less than 100%. While overall benefits depend on how the VAIO G is used, research shows that there is virtually no decrease in capacity when recharge capacity is controlled at 50%.

簡單來說,雖然鋰電池不像鎳鎘或鎳氫電池會因過度充電產生記憶效能(Lazy Battery Effect,這也是它們需要定期充分放電再充飽的原因),但不要充到100%仍對電池有利,研究指出,若鋰電池充電量能控制在50%,理論上(Virtually)電池容量將不會有任何衰減,故設定充電量為80%稱為養生模式(Caring Recharge Mode),50%則為超級養生模式(Super-Caring Recharge Mode)。

到此的研究心得是 -- 依據Sony RD的看法,長期維持50%的充飽度,電池將可成人瑞,報告完畢!

【延伸閱讀】

其實發問的Ryan同學長年與數位相機為伍,用過的電池比我吃過的米還多(謎: 最好有這麼誇張!),對於電池保養頗有心得,儼然已是達人等級,班門弄斧之餘,一併推薦其兩篇相關文章供大家參考:

筆記-YouTube影片轉MP3(批次作業版)

$
0
0

不久前才知道有個馬拉松迷專屬的網路廣播節目 -- 跑步吧! 人生,主持人飛小魚每週邀請馬拉松界的聞人漫談跑馬點滴,分享訓練成長心得,以及踏上慢跑不歸路的心路歷程... 等。節目表上的來賓,幾乎網羅了我在賽道見過的各大社團名稱及名人: 百信建材、土慢、警愛跑、愛跑部、康軒、拖鞋俠、紅衣女郎... 簡直是一本台灣馬場百科全書。

可惜節目只能透過YouTube收聽,未提供MP3檔下載,沒辦法實現丟進運動MP3邊跑邊聽的夢想。之前我曾找過YouTube轉MP3的方法,但其針對情境為單一YouTube影片轉檔,但這次我面對的節目內容前後共72集,每集約4段影片,故共有288段影片要轉檔,轉完還得四個MP3檔合併成一個...

身為程式魔人,若是認命地手工輸入URL、等待轉檔完成再選檔合併... 傳到江湖肯定被人耻笑,一定要寫程式搞定才是王道呀!! 花了點時間,找到自動化生產的方法: (全都要感謝Open Source社群的無私分享)

  1. 由網頁抓回各集各段影片的YouTube連結,這部分可靠WebClient + RegularExpression解決
  2. 找到一份很棒的說明(Grabbing Your Music from YouTube: Do It Your Way),教你使用三個Open Source命令列工具完成YouTube轉MP3的程序,分別是:
    * youtube-dl取回指定YouTube連結影片的FLV檔案 
       (Windows版下載: youtube-dl under Windows)
    * ffmpeg 可由FLV擷取WAV檔案 (Windows版下載: Zeranoe FFmpeg Builds)
    * lame 將WAV轉成MP3 (Windows版下載: RAREWARES LAME Bundle)
  3. 最後一塊拼圖: 由於一集節目分成3-4個MP3,合併成一集一檔使用起來比較方便。理論上應可找到合用的併檔命令列工具,但我發現了更有趣的東西,一個可以編輯MP3檔案的Open Source .NET元件 – NAudio,有現成的併檔範例可參考,我還假掰順手加上ID3v2 Tag標示標題跟作者(當然是飛小魚啦! 不是我)。學會這顆元件,以後要使用.NET產生MP3檔時就可派上用場。

嘗試了邊聽跑步廣播邊跑步,果然十分對味! 未來兩三個月跑LSD應該都不會無聊囉~

2012艋舺馬拉松~

$
0
0

第四馬,終於,達成心目中的階段性目標,拿下SUB 5,還意外地摸到430的邊~

還記得上回光橋夜跑遇水則炸的慘況,這幾天氣溫在17-22打轉,但下不停的雨讓人擔心,尤其每天清晨雨勢普遍偏大,十分害怕又重演上回的勵志奮發負面教材,鞋一溼就丟盔卸甲潰不成軍的戲碼。

賽前一晚牙齒作怪(不得不承認,自己已到古人云髮蒼蒼、視茫茫、齒牙動搖的年紀了),一夜輾轉怪夢不斷,早上四點起床雨勢不小,心頭涼了半截,今天的路跑有個坎坷的開端。

清晨五點冒雨騎著機車前往會場,一路冷清,直到台大一帶等紅燈時遇上另一位騎車大哥,大紅衣保袋漏了餡,他問了一句: "42公里厚?",相視而笑,接著我們小聊到令人抓狂的光橋溯溪下雨馬.. 將轉綠燈之際,他問了一句"跑過雙溪嗎?",我點點頭,他自豪地說: "我們辦的",哈! 原來是來自永慢的大哥~

所幸上週安排了場30K LSD,從政大跑來華中橋下對這一帶環境稍作了解,不然跟萬華實在不熟。停好車走了一段路寄完物,時間所剩已不多,趕緊找了棵大樹下拉筋暖身,雨勢不小,決定穿上輕便雨衣開跑。(全程視雨勢大小穿脫過兩次,今天氣溫不高,雨衣的防淋溼效果勝過帶來的悶熱感,個人覺得是正確決策)

起跑前發生一件趣事。在樹下拉筋時,拿出後掛式運動MP3環在脖子上預備,移動前往起跑線等待時,摸了一下脖子發現空無一物,頓時一陣驚慌,心想該不會暖身時掉了,要在千人聚集的場子找回來希望渺茫,但還是下意識想沿路走回剛才暖身地點尋找,走沒兩步,才發現MP3早已載在耳朵上,難怪脖子上摸不到,鬆了一口氣,也為自己耍呆一事大笑三聲!

由於一路下雨,路上就沒拿出手機拍照,但深深感受艋舺馬真不負它的好口碑! 只辦全馬組,參賽人數及賽道安排恰到好處,少有全部人狹路對衝的擁擠,雖然在河濱折返四趟有點無聊,但補給站出現許多有趣的東西 -- 鹹鴨蛋(朕決定封其為鹽分補給聖品)、蠻牛、豆漿、豆皮壽司,另外一路上水、運動飲料、香蕉、葡萄、西瓜都沒間斷過,源源不絕,算是補得很周到。

原本擔心因下雨重演光橋信心潰堤的慘劇,意外地,今天狀況卻比想像好佷多。在30K之前,大致還能守住六分速,雖然起跑沒多久我的罩門,左腳跟+右膝IT Band,就開始作怪,但在水站補了幾次沙隆巴斯噴劑,一路上倒相安無事,最後也沒有變傷成殘。30K真的是一個門檻,跑滿30K後速度明顯變慢,也慢慢需要加入步兵團尋求身心慰藉,但步行時間我盡量限制不要超過3分鐘,以控制速度損失幅度。

【Pace統計】 (回家匯出資料才發現,最慢的8:57剛好出現在35K,跟傳說中的撞牆線不謀而合)
06:57 / 06:30 / 05:53 / 05:29 / 05:42 / 05:55 / 05:47 / 05:43 / 05:41 / 05:44 /
06:31 / 05:52 / 06:06 / 05:49 / 06:13 / 05:58 / 05:58 / 06:16 / 05:44 / 06:11 /
06:22 / 06:01 / 07:02 / 05:50 / 06:07 / 06:24 / 05:53 / 05:49 / 06:00 / 06:33 /
06:43 / 06:04 / 08:07 / 07:01 / 07:48 / 08:57 / 06:23 / 07:05 / 07:57 / 05:45 /
05:24 / 05:55 / 06:07

而今天一路在雨中陪伴我的是飛小魚的"跑步吧! 人生"網路廣播,一口氣聽完4集,排解了4小時的無聊時光。而最後2K則祭出祕密武器,用電音搖滾來催速度,甚至冒出Pace 530,最終以4:30:41完成了我的第四馬,終於達成SUB 5,還在下雨天跑出430的成績,好得連自己都意外~

通過終點來到會場,雖然已無緣目睹當年跑完不發便當直接吃辦桌的奇景,但仍然熱鬧滾滾,很有里民活動的fu。而我也看到傳說中的台灣生啤車(右下圖),不知道如果天氣好,會不會直接開到賽道旁充當水站,哈! 另外,有看到左下圖那個高聳入雲的獎盃嗎? 那是專屬200馬王者的尊榮!

還晶片時,工作人員直接用晶片過應感機,會拿到一張熱感印含成績的號碼紙,20分鐘後可憑號碼紙領取成績證明,領取過程很順暢。大會還附了透明資料夾裝妥成績證明防止弄溼或摺損,頗為貼心。完賽獎版則為金屬浮雕版,配上藍色掛帶,蠻精緻且帶點設計感。

 

艋舺馬也設有初馬獎,綜合來論,這場賽事交通方便,賽道平坦不致擁擠,補給無虞,整體規劃得頗為周到細心,與櫻花馬相比,除了初馬獎是獎盃,不若琉璃馬獨特,路途風景不若櫻花山馬優美(但少了魔鬼山路,對初馬較為人道 XD),也是一場值得列入初馬賽考量的優質賽事。

【跑馬心得】

  1. 這場與距離上場爆很大的光橋夜跑相比,都是河濱道,都是下雨天,結果卻迥然不同,我想最主要關鍵還是在於9月到11月間又跑了660K,有練有差,沒第二句話。
  2. 有了先前腳趾瘀血的經驗,在舊跑鞋滿1000K退役時,買新鞋時刻意挑選大半號的尺碼,果然大幅改善了長跑後腳腫或鞋溼後腳趾受傷的機率。
  3. 近來開始針對核心肌群做一些簡單訓練,練腹股有助跑步聽起來很神奇,但實際練下去還真的能感受到差異,簡單來說,就是跑起來比較不會累,可以撐比較久。我猜想主來來自核心肌群強化後,跑步姿勢變得較穩定精準,無形中提高了效率,而維持姿勢不走樣,也能降低受傷機率。核心肌群鍛練可算是進階的必修課程,有點後悔太晚才開始練習,不然前幾場或許能跑得更輕鬆一些。

CODE-呼叫命令列程式並即時接收輸出

$
0
0

之前學過透過RedirectStandardOutput設定,可在.NET呼叫其他命令列程式並接收其顯示內容的技巧。這回則有額外需求,由於某個命令列轉檔工具執行耗時(可能長達數分鐘),故進行期間會持續輸出進度資訊讓使用者安心,但依以前StandardOutput.ReadToEnd()的做法,.NET呼叫端只能在數分鐘後一次取得全部顯示結果,無法即時掌握處理進度,使用者體驗大大扣分。

研究之後,發現貼心的.NET BCL早有因應對策: OutputDataReceived!

做法是先在Process.OutputDataReceived事件上加入處理邏輯,接著呼叫Process.BeginOutputReadLine(),之後外部程式每輸出一行內容,OutputDataReceived事件便被觸發一次,並可由DataReceivedEventArgs.Data取得輸出內容進行自訂處理。

以下的程式範例,一人分飾兩角,展示了OutputDataReceived、ErrorDataReceived、從Output及Error輸出內容、傳回ExitCode等小技巧:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace TestProcOutput
{
class Program
    {
staticint Main(string[] args)
        {
//模擬Command Line Utility程式行為
if (args.Length == 1)
            {
switch (args[0])
                {
case"simu":
//間隔一秒由正常Output輸出0-4
for (int i = 0; i < 5; i++)
                        {
                            Console.WriteLine(i);
                            Thread.Sleep(1000);
                        }
//正常結束ExitCode = 0
return 0;
default:
//透過Error TextWriter輸出訊息
                        Console.Error.WriteLine("Syntax Error!");
//傳回ExtiCode = 1
return 1;
                }
            }
//宣告兩個Callback分別顯示Output及Error接收到的內容
            Action<string> outcb = (s) =>
            {
                Console.WriteLine("OUT:" + s);
            };
            Action<string> errcb = (s) =>
            {
                Console.WriteLine("ERR:" + s);
            };
//第一次測試由Output輸出
int exitCode = Execute("TestProcOutput", "simu", outcb, errcb);
            Console.WriteLine("ExitCode={0}", exitCode);
//第二次則測試由Error輸出
            exitCode = Execute("TestProcOutput", "boo", outcb, errcb);
            Console.WriteLine("ExitCode={0}", exitCode);
            Console.Read();
return 0;
        }
 
publicstaticint Execute(string exeFile, string argument,
            Action<string> outputCallback, Action<string> errorCallback)
        {
            ProcessStartInfo si = new ProcessStartInfo()
            {
                FileName = exeFile,
                Arguments = argument,
//必須要設定以下兩個屬性才可將輸出結果導向
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
//不顯示任何視窗
                CreateNoWindow = true
            };
            Process p = new Process()
            {
                StartInfo = si,
                EnableRaisingEvents = true,
            };
//開始執行
            p.Start();
//透過OutputDataReceived及ErrorDataReceived即時接收輸出內容
            p.OutputDataReceived +=
                (o, e) =>
                {
if (!string.IsNullOrEmpty(e.Data) && outputCallback != null)
                    {
                        outputCallback(e.Data);
                    }
                };
            p.ErrorDataReceived +=
                (o, e) =>
                {
if (!string.IsNullOrEmpty(e.Data) && errorCallback != null)
                    {
                        errorCallback(e.Data);
                    }
                };
//呼叫Begin*ReadLine()開始接收輸出結果
            p.BeginOutputReadLine();
            p.BeginErrorReadLine();
 
            p.WaitForExit();
return p.ExitCode;
        }
    }
}

【茶包射手日記】EPPlus 3.1.2 Bug Fixing

$
0
0

發現EPPlus 3.1.2版Bug一枚。

開啟現有xlsx後,不做任何修改就儲存,再使用Excel開啟會出錯。例如以下範例:

using (ExcelPackage p = new ExcelPackage(new FileInfo("通訊錄.xlsx")))

    p.Save();
}

程式執行後使用Excel開啟重新儲存的通訊錄.xlsx檔案,會出現以下錯誤:

Excel 在 '通訊錄.xlsx' 中找到無法讀取的內容。您要回復此活頁簿的內容嗎? 若您信任此活頁簿的來源,請按一下[是]。
Excel found unreadable content in filename.xls. Do you want to recover the contents of this workbook? If you trust the source of this workbook, click Yes.

試圖修復,Excel會回報進一步訊息。

檢視錯誤訊息xml檔,會發現關於style.xml及sheet*.xml的修復報告:

error067160_07.xml檔案 'X:\TestEPPlus\bin\Debug\通訊錄.xlsx' 中偵測出錯誤
已移除的記錄: /xl/styles.xml 部分的 樣式 (樣式)
已修復的記錄: /xl/worksheets/sheet1.xml 部分的 儲存格資訊
已修復的記錄: /xl/worksheets/sheet1.xml 部分的 欄資訊

修復後的xlsx檔案,資料完好,但格式設定大亂。

幸好,xlsx檔的本質是個ZIP壓縮檔,解開後就可以進一步探查真相。在xl\styles.xml檔案中,我發現幾處不合理的XmlNode:

<cellStyleXfs count="2">
  <xf numFmtId="0" fontId="0" fillId="0" borderId="0">
    <alignment vertical="center" />
  </xf>
</cellStyleXfs>
<cellXfs count="3">
  <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="1">
    <alignment vertical="center" />
  </xf>
  <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="1">
    <alignment vertical="center" />
  </xf>
</cellXfs>
<cellStyles count="2">
  <cellStyle name="一般" xfId="1" builtinId="0" />
</cellStyles>

cellStyleXfs count=2,但資料只有一筆、cellXfs count=3,但資料只有兩筆,而且xfId都是"1",cellStyles count=2但資料只有一筆。推測就是這些錯亂導致styles.xml出錯,而sheet1.xml與sheet2.xml的對應也因此損壞,造成格式顯示異常。

感謝老天爺,EPPlus是個Open Source Project,下載原始碼用Line-By-Line追進元件內部,沒多久就找到Bug來源:

EPPlus\ExcelStyles.cs Line 562 UpdateXml() method:

Line 561:            //NamedStyles
Line 562:            count = 1; <== 應改成count = 0;才合理

經過修改,原本存檔會弄壞xlsx檔的問題消失了! 順便把我的發現回報到CodePlex,終於有機會為Open Source社群略盡薄棉之力,Open Source萬歲!


【茶包射手日記】EPPlus無法開啟ReportViewer匯出xlsx檔

$
0
0

使用EPPlus 3.1.2開啟RDLC匯出的xlsx檔,讀取ExcelPackage.Workbook.Worksheets時發生錯誤:

System.ArgumentNullException: Value cannot be null.
Parameter name: String

既然EPPlus是Open Source,照慣例 -- "Use the Source, Luke!"

追進原始碼,找到案發現場 -- ExcelWorksheet.cs LoadColumns(),爆炸點在xr.GetAttribute(“min”)傳回null,導致int.Parse()出錯。

privatevoid LoadColumns (XmlTextReader xr)//(string xml)
        {
            var colList = new List<IRangeID>();
if (ReadUntil(xr, "cols", "sheetData"))
            {
//if (xml != "")
//{
//var xr=new XmlTextReader(new StringReader(xml));
while(xr.Read())
                {
if(xr.LocalName!="col") break;
int min = int.Parse(xr.GetAttribute("min"));
 
int style;
if (xr.GetAttribute("style") == null || 
                        !int.TryParse(xr.GetAttribute("style"), out style))
                    {
                        style = 0;
                    }
                    ExcelColumn col = new ExcelColumn(this, min);
//...略...

追查RDLC匯出xlsx檔中的sheet1.xml,其中<cols><col>結構如下:

<sheetViews>
<sheetViewworkbookViewId="0"showGridLines="0">
</sheetView>
</sheetViews>
<cols>
<colmin="1"max="1"customWidth="1"width="10.3515625">
</col>
<colmin="2"max="2"customWidth="1"width="78.79296875">
</col>
</cols>
<sheetData>
<rowr="1"ht="17"customHeight="0">

經過一番推敲,錯誤源自<col>與</col>會觸發兩次xr.Read(),第二次讀入</col>時,因屬於EndElement,xr.GetAttribute()讀不到東西,故傳回null。猜想這段寫法平日之所以運作正常,是因為絕大部分xlsx都採用<col min="1" … /> Self-Closing Tag形式,而RDLC匯出xlsx時採取較罕見的<col></col>寫法,踩中地雷而爆炸。(Excel開檔OK,應算EPPlus的Bug)

找到原因,要解決就不是難事,在xr.Read()後,加入一行避開</col>,藥到病除! (當然,也回報到CodePlex囉~)

while(xr.Read())
                {
//2012-12-06 by Jeffrey Lee, to ignore </col> End Element
if (xr.NodeType == XmlNodeType.EndElement) continue;
if(xr.LocalName!="col") break;

Coding4Fun - 點陣中文字型顯示

$
0
0

因緣巧合,最近剛好需要處理中文點陣字型。

在DOS+倚天中文的古早年代,曾經用BASICA寫過解析倚天中文字型檔的程式,沒想到二十多年後居然還有機會重新回味,只是這回手上的兵器已由當年的BASICA小開山刀,換成C#加農砲,語言特性已不可同日而言、自己的程式技巧也遠比當年成熟,對照起來格外有趣。

時代演進大大地改變了寫程式的方法,當年要自己瞎摸亂湊好一陣子才能拼湊出檔案規格,現在稍稍爬文就能找到網友的熱心分享:

有了字型檔規格,加上C#的物件導向,我試寫了以不同字型檔為基礎的點陣中文字型元件:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
 
namespace TestChineseFont
{
//允許以不同廠商字型檔作為來源的點陣字型物件
publicabstractclass DotArrayFontProvider
    {
//宣告不同的尺寸規格
publicenum FontSize
        {
            Size15 = 15, Size24 = 24, Size32 = 32, Size48 = 48
        }
//不同字型檔轉為byte[]的實作方式不同
publicabstractbyte[] GetFontData(FontSize sz, bool halfWidth);
//由寬度計算所需要的位元數(半形時用量減半)
protectedstaticint GetWidthBytes(int w, bool halfWidth)
        {
if (halfWidth) w = (w / 2);
return (w / 8) + (w % 8 > 0 ? 1 : 0);
        }
//取得特定字元的點陣資料(byte[])
publicbyte[] GetCharData(char ch, FontSize sz = FontSize.Size24)
        {
byte[] b = Encoding.GetEncoding(950).GetBytes(newchar[] { ch });
int iSz = (int)sz;
bool halfWidth = false;
int offset = -1;
//ASCII 0-255採半形
if (b.Length == 1) halfWidth = true;
int arraySize = GetWidthBytes(iSz, halfWidth) * iSz;
byte[] result = newbyte[arraySize];
//半形時依ASCII碼決定資料起始位址
if (halfWidth)
            {
                offset = arraySize * b[0];
            }
else
            {
//全形文字依倚天字型檔的存放規則
//http://www.cnblogs.com/armstrong-cn/archive/2011/09/01/2161567.html
byte hi = b[0], lo = b[1];
int serCode = (hi - 161) * 157 + (lo >= 161 ? lo - 161 + 1 + 63 : lo - 64 + 1);
if (serCode >= 472 && serCode < 5872)
                    offset = (serCode - 472) * arraySize;
elseif (serCode >= 6281 && serCode <= 13973)
                    offset = (serCode - 6281) * arraySize + 5401 * arraySize;
            }
if (offset < 0) returnnull;
            Buffer.BlockCopy(GetFontData(sz, halfWidth), offset, result, 0, arraySize);
return result;
 
        }
//將點陣內容由byte[]轉為長*寬的二維byte[,],1表示該點要顯示, 0表示該點留白
publicstaticbyte[,] GetDotArray(byte[] data, int w, int h)
        {
//偵測是否為半形字
bool halfWidth = data.Length == GetWidthBytes(w, true) * h;
//如為半形字,寬度減半
if (halfWidth) w = w / 2; 
if (w < 8) w = 8;
//宣告二維陣列以存放點陣資料
byte[,] dotArray = newbyte[h, w];
int widthBytes = data.Length / h;
for (int y = 0; y < h; y++)
            {
int offset = widthBytes * y;
byte b = data[offset];
for (int x = 0; x < w; x++)
                {
if (x % 8 == 0)
                    {
                        b = data[offset];
                        offset++;
                    }
                    dotArray[y, x] = (byte)(((b << (x % 8)) & 0x80) != 0 ? 1 : 0);
                }
            }
return dotArray;            
        }
    }
}

DotArrayFontProvider抽象類別提供了GetCharData(char ch, FontSize sz)以取出指定字型尺寸(例如: 15x15, 24x24)的特定字元點陣資料(一點一個Bit,一個Byte代表8點,以byte[]方式傳回),而另外有靜態方法GetDotArray()可將前述byte[]轉成二維陣列,一點一個byte,1代表顯示,0代表留白,以配合X、Y軸座標運算轉成圖檔。DotArrayFontProvider是個抽象類別,子類別必須實作一個byte[] GetFontData(FontSize sz, bool halfWidth)傳回特定尺寸、全形/半形的全部點陣資料。(ASCII 0-255字元使用半形資料) 這部分與各廠商字型檔規格高度相依,就留給各子類別自行實現。

針對國喬字型檔所寫的KCFontProvider,邏輯很簡單,就只是讀入KCCHIN16.F00、KCTEXT16.F00、KCCHIN24.F00、KCTEXT24.F00等字型檔,從中取出15x15及24x24的全形及半形文字點陣資料,存入靜態Dictionary後,供GetFontData()時取出回傳。其中有些位址計算,純粹是要由國喬字型檔取出資料,只保留點資料的部分,看似複雜,但沒什麼營養。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
 
namespace TestChinFont
{
publicclass KCFontProvider : DotArrayFontProvider
    {
static Dictionary<string, byte[]> dataPool = 
new Dictionary<string, byte[]>();
//http://bbs.unix-like.org:8080/boards/FB_chinese/M.1023317709.A
static KCFontProvider()
        {
byte[] buff = File.ReadAllBytes("KCCHIN16.F00");
 
int offset = 256 + 765 * 2 * 16;
constint charCount = 13195;
byte[] data = newbyte[charCount * 2 * 15];
for (int i = 0; i < charCount; i++)
            {
                Buffer.BlockCopy(buff, offset + i * 2 * 14, data, i * 2 * 15, 2 * 14); 
            }
            dataPool.Add("C" + FontSize.Size15, data);
            buff = File.ReadAllBytes("KCTEXT16.F00");
            data = newbyte[256 * 15];
            offset = 256;
for (int i = 0; i < 256; i++)
                Buffer.BlockCopy(buff, offset + i * 16, data, i * 15, 15);
            dataPool.Add("A" + FontSize.Size15, data);
 
            buff = File.ReadAllBytes("KCCHIN24.F00");
            data = newbyte[charCount * 72];
            offset = 256 + 765 * 72;
for (int i = 0; i < charCount; i++)
            {
                Buffer.BlockCopy(buff, offset + i * 72, data, i * 72, 72);
            }
 
            dataPool.Add("C" + FontSize.Size24, data);
            buff = File.ReadAllBytes("KCTEXT24.F00");
            data = newbyte[256 * 48];
            offset = 256;
for (int i = 0; i < 256; i++)
                Buffer.BlockCopy(buff, offset + i * 48, data, i * 48, 48);
            dataPool.Add("A" + FontSize.Size24, data);
        }
 
publicoverridebyte[] GetFontData(FontSize sz, bool halfWidth)
        {
string key = (halfWidth ? "A" : "C") + sz;
if (!dataPool.ContainsKey(key))
thrownew NotImplementedException();
return dataPool[key];
        }
    }
}

這樣就差不多就能精準取出不同字元的點陣資料。雖然當年用BASICA寫的版本已不復記憶,但我很肯定,這段C#讀檔程式碼的長度絕對不到當年的十分之一!

最後,配合一小段程式,將點陣資料轉成Bitmap,來驗證一下執行結果:

private Bitmap DrawDotArrayText(string text, DotArrayFontProvider.FontSize fs)
        {
int sz = (int)fs;
            Bitmap bmp = new Bitmap(text.Length * sz * 4, sz * 4);
            SolidBrush bb = new SolidBrush(Color.Black);
            SolidBrush yb = new SolidBrush(Color.Orange);
            Graphics g = Graphics.FromImage(bmp);
            g.FillRectangle(bb, 0, 0, bmp.Width, bmp.Height);
int offset = 0;
            var kcfp = new KCFontProvider();
foreach (char ch in text.ToCharArray())
            {
byte[,] d = DotArrayFontProvider.GetDotArray(
                                                 kcfp.GetCharData(ch, fs), sz, sz);
for (int y = 0; y < sz; y++)
                {
for (int x = 0; x < sz; x++)
                    {
if (d[y, x] == 1)
                        {
                            g.FillRectangle(yb, offset + x * 4, y * 4, 3, 3);
                        }
                    }
                }
                offset += sz * 4;
            }
return bmp;
        }

LED字幕機式的中文顯示效果就完成囉~

【冷知識】處理句點或空白結尾的檔案及目錄

$
0
0

依據MSDN文件,檔名或目錄名稱不應以句點"."或空白結尾:

Do not end a file or directory name with a space or a period. Although the underlying file system may support such names, the Windows shell and user interface does not. However, it is acceptable to specify a period as the first character of a name. For example, ".temp".

只是,其中也提到這個限制是來自Windows Shell層,部分檔案系統可以接受,於是,我們可能會看到如下情況。在InvFileName目錄下有一個機車檔案"TrailingPeriod.”、兩個機車目錄"TrailingSpace "跟"DirTrailingPeriod.",DIR可以看到,卻無法檢視目錄或檔案內容,不能更名也沒辦法刪除,不管用命令列指令或是開檔案總管都束手無策,讓人充滿無力感!

D:\InvFileName>dir
 Volume in drive D has no label.
 Volume Serial Number is 0F24-3000

 Directory of D:\InvFileName

2012/12/14  下午 08:26    <DIR>          .
2012/12/14  下午 08:26    <DIR>          ..
2012/12/14  下午 08:27                 7 TrailingPeriod.
2012/12/14  下午 08:29    <DIR>          TrailingSpace
2012/12/14  下午 08:30    <DIR>          DirTrailingPeriod.
               1 File(s)              7 bytes
               4 Dir(s)   3,907,305,472 bytes free

D:\InvFileName>type TrailingPeriod.
The system cannot find the file specified.

D:\InvFileName>dir "TrailingSpace "
 Volume in drive D has no label.
 Volume Serial Number is 0F24-3000

 Directory of D:\InvFileName

File Not Found

D:\InvFileName>dir DirTrailingPeriod.
 Volume in drive D has no label.
 Volume Serial Number is 0F24-3000

 Directory of D:\InvFileName

File Not Found

爬文許久,好不容易摸索出正確關鍵字"Trailing Period",才露出曙光...

微軟有篇KB 您無法刪除 NTFS 檔案系統磁碟區上的檔案或資料夾提到了"\\? \x:\folder_name\invalid_filename”URI表示法,經測試果然可以克服問題,但卻發現,這招對句點結尾的目錄似乎無效(如下方黃字區所示),最後繞了個彎,使用"8.3短名稱"密技,推測出"DirTrailingPeriod."的8.3短名為"DirTra~1",總算成功存取目錄。

D:\InvFileName>type "\\?\d:\InvFileName\TrailingPeriod."
Cool!!!
D:\InvFileName>dir "\\?\d:\InvFileName\TrailingSpace " /w
 Volume in drive \\?\d: has no label.
 Volume Serial Number is 0F24-3000

 Directory of \\?\d:\InvFileName\TrailingSpace

[.]  [..]
               0 File(s)              0 bytes
               2 Dir(s)   3,907,309,568 bytes free

D:\InvFileName>dir "\\?\d:\InvFileName\DirTrailingPeriod." /w
 Volume in drive \\?\d: has no label.
 Volume Serial Number is 0F24-3000

 Directory of \\?\d:\InvFileName\DirTrailingPeriod

File Not Found

D:\InvFileName>dir "\\?\d:\InvFileName\DirTra~1" /w
 Volume in drive \\?\d: has no label.
 Volume Serial Number is 0F24-3000

 Directory of \\?\d:\InvFileName\DirTra~1

[.]  [..]
               0 File(s)              0 bytes
               2 Dir(s)   3,907,309,568 bytes free

2012富邦台北馬拉松~

$
0
0

吃館子上餐廳,有時我們是為了傳說中會在舌頭上開舞會的美味、有時是衝著花小錢吃通海的超值爽快、還有些時候,我們在乎的只是用餐環境燈光美氣氛佳,至於料理是否味如嚼蠟? 一點也沒差。在我心目中,富邦台北馬拉松就是一場純跑氣氛的慢跑嘉年華,沒有超值的紀念排汗T、沒有令人驚豔的補給、沒有壯闊的山景、沒有清新的空氣、沒有濃郁的人情,但要比熱鬧比盛大到感染你每一根神經,這場肯定是每位跑馬人都該親身體驗一次的盛事! 衝著這點就報了名,也在12月初嚐一月雙馬滋味~

 

早上近六點抵到市政府附近,立刻感受到與之前截然不同的馬場氣氛。有遊覽車拉來一整車香港朋友,跟著領隊舉的紫荊旗前進會場;路上還聽到大陸口音、也看到許多外國朋友。才六點現場早已人聲鼎沸,數萬人擠到摩肩擦踵,七點整開跑時,不但有實況轉播、甚至出動了直升機空拍,不愧是國際級的跑馬盛會~

  

  

開跑時發現馬場名人--獅子頭大哥排在前方不遠處,不過開跑後不到一公里,330等級的車尾燈很快消失在視線外。一路沿著仁愛路、中山北轉進河濱道,接著沿河濱向西跑到麥帥二橋,過河再向東跑至圓山大飯店。途經兒童育樂中心,發現旋轉木馬旁有制服正妹外拍團,隔著鐵絲網圍籬我也來一張!

氣象預告16日要變天下雨,原本擔心重演前回艋舺馬淋雨濕冷的悲情,但顯然是多慮了。接近中午時,陽光數度露臉,氣溫直上25度,讓我又開始懷念起上回陰雨微冷的天氣,人真是矛盾的動物。

路協的水站補給果然一如傳說中規中矩,前10K只供應水,後面開始有運動飲料、巧克力、香蕉跟餅乾,但未提供鹽讓我有點擔心抽筋,加上氣溫較預期高出不少,原本就很會流汗的我從起跑就噴汗如瀑,拿出事先備好的梅粉(買芭樂附贈的那種)泡礦泉水飲下,實際效果未知,但穩定軍心的效果是一定有的,這招不錯,日後繼續沿用好了。

 

這一路上遇到兩回釘著目標時間背布、綁著氣球的配速員。前20K跑完約2小時出頭,在河濱道遇到4小時配速員,但SUB 4顯然不是我的實力摸得到的東西,拍完背影照後立刻被海放~ 而在最後3K,430配速員也出現了,讓我享受了一段被免子追的刺激感。基隆路地下道時更跟著另一位綁氣球的430配速帥哥跑了近1K,但他的目標是大會430,無力追趕,終究只能提前下車,力求保住晶片時間430就好。

  

最後成績為大會時間4:30:12、晶片時間4:29:13,比艋舺馬只快了1'18",但還是很厚顏地宣告: "我又破PB了!!" 我決定了,以後每次比賽都比前一次快一分鐘就好,PB省點用,這樣子每一場跑完都可以跟別人說,我"又"破PB了,聽起來比較厲害,哈! 姑且命名為"切香腸式PB微破法"~

完賽奬牌入手,第五馬完成!

【Pace統計】
06:16 / 05:29 / 05:31 / 05:44 / 05:46 / 05:28 / 05:31 / 05:35 / 05:25 / 06:04 /
05:55 / 05:27 / 06:05 / 05:30 / 05:42 / 07:14 / 06:00 / 05:37 / 05:35 / 05:49 /
07:16 / 05:31 / 05:32 / 06:30 / 05:48 / 07:38 / 06:21 / 06:48 / 07:05 / 06:19 /
07:20 / 07:06 / 06:53 / 07:31 / 06:38 / 07:52 / 07:00 / 07:33 / 06:54 / 05:52 /
06:15 / 06:56 / 06:03

控制EF的Transaction範圍

$
0
0

被問到在EF環境要如何控制將某些DB操作包含在Transaction範圍內、將某些排除在外? 整理成簡單範例方便說明。

範例程式碼共有三段DB操作,第一段是寫入追蹤資訊到ActLog資料表、第二、三段則是各寫入一筆Player資料,為了模擬交易Rollback情境,故意讓兩筆Player的Primary Key相同。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Transactions;
 
namespace TestNoTran
{
class Program
    {
staticvoid Main(string[] args)
        {
using (TransactionScope tx = new TransactionScope())
            {
try
                {   //...其他程式邏輯(省略)...
                    SomeLogHelper.Log("Debug: Insert To DB");
                    SomeDALHelper.InsertPlayer("A1", "U1", DateTime.Today, 100);
//故意寫入PK相同的第二筆資料,將引發錯誤
                       SomeDALHelper.InsertPlayer("A1", "U2", DateTime.Today, 120);
                    tx.Complete();
                }
catch (Exception ex)
                {
                    Console.WriteLine("Error: {0}", ex.Message);
                }
                Console.Read();
            }
 
        }
    }
 
class SomeDALHelper
    {
publicstaticvoid InsertPlayer(
string id, string name, DateTime regDate, int score)
        {
using (var ctx = new LabEntities())
            {
                var p1 = new Player()
                {
                    UserId = id,
                    UserName = name,
                    RegDate = regDate,
                    Score = score
                };
                ctx.Players.Add(p1);
                ctx.SaveChanges();
            }
        }
    }
 
class SomeLogHelper 
    {
publicstaticvoid Log(string msg)
        {
using (var ctx = new LabEntities())
            {
                ctx.ActLogs.Add(new ActLog()
                {
                    Info = msg
                });
                ctx.SaveChanges();
            }
        }
    }
}

執行程式,一如預期,會得到因PK重複引發的錯誤:

Error: System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.UpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.SqlClient.SqlException: Violation of PRIMARY KEY constraint 'PK_Player'. Cannot insert duplicate key in object 'dbo.Player'. The duplicate key value is (A1).

檢查資料庫,ActLog及Player資料表均空空如也,代表三個DB動作一起Rollback了。如果我們希望寫ActLog的部分不要參與交易,無論如何都將資訊寫入資料庫,該怎麼做?

最簡單的做法是用另一個TransactionScope將寫ActLog部分包起來,並指定初始化參數為TransactionScopeOption.Suppress,宣告在此範圍內的DB動作不需要參與交易。如下:

staticvoid Main(string[] args)
        {
using (TransactionScope tx = new TransactionScope())
            {
try
                {   //...其他程式邏輯(省略)...
//宣告出一段不參與交易的範圍
using (TransactionScope tx2 = 
new TransactionScope(TransactionScopeOption.Suppress))
                    {
                        SomeLogHelper.Log("Debug: Insert To DB");
                    }
                    SomeDALHelper.InsertPlayer("A1", "U1", DateTime.Today, 100);
//故意寫入PK相同的第二筆資料,將引發錯誤
    SomeDALHelper.InsertPlayer("A1", "U2", DateTime.Today, 120);
                    tx.Complete();
                }
catch (Exception ex)
                {
                    Console.WriteLine("Error: {0}", ex.ToString());
                }
                Console.Read();
            }
 
        }

重新執行程式,錯誤依舊,Player資料表仍無資料,但ActLog資料表會留下一筆記錄,實現了排除在交易範圍外的目標。

最後補充,除了使用TransactionScope外,還有其他處理EF交易的方式,如: 讓DbContext.SaveChanges()自動將一連串動作包成交易、讓多個DbContext共用連線並控制該連線交易狀態等,細節可參考舊文,該文談的雖是LINQ to SQL,但與EF運作原理大同小異。

【茶包射手日記】只涉及單一資料表的Deadlock

$
0
0

在我原本狹隘的SQL知識裡,Deadlock發生情境需要兩個Process A與B跟兩個Table X與Y搭配演出: A鎖定住X想更新Y,B鎖定Y等著要更新X,產生無解的僵持,再由SQL二者擇一選為犠牲者,令其失敗來成立另一個Process。

直到最近處理一起Deadlock案例,才又長了見識。一個處理流水序號的Stored Procedure,讀取與更新對象只限同一Table,並不構成井底之蛙心中的Deadlock成立要件: 兩個鎖定對象、相反的讀取/更新順序,但Deadlock卻硬生生地發生了!

試著用以下方式模擬重現問題。以下的SQL指令,會在一個Transaction中先讀取LockLab的特定計數欄位,再將其更新加1,為確保不會有Phantom Read及Non-Repeatable Read,隔離層級拉高到Serializable(關於隔離層級: 小朱有篇鎖定使用的藝術 (Part 2) - 隔離層次 (Isolation Level)可參考)。為故意製造Deadlock,指令中再加入WAITFOR拖長Transaction的時間到10秒,同時開兩個SSMS執行,就能輕易讓二者強碰相咬。(真實案例因執行時間很短,數千到上萬次才會發生一次Deadlock)

SETTRANSACTIONISOLATIONLEVEL SERIALIZABLE;
BEGINTRAN
DECLARE @i INT;
SELECT @i = Seq FROM LockLab WHERE Code='JEFF'AND YearMon='201212'
SET @i = @i + 1;
WAITFOR DELAY '00:00:10'
UPDATE LockLab SET Seq = @i WHERE Code = 'JEFF'AND YearMon='201212'
--其他程式邏輯(略)
COMMIT TRAN

執行結果如下圖所示,同時執行兩份SSMS,其中一個成功,另一個因Deadlock被選為犠牲者:

用SQL Profiler調出事故現場軌跡圖。兩個Process都放了Shared Lock(S)在PK_LockLab上,當要更新再對PK_LockLab放上Exclusive Lock(X)時形成對峙,造成Deadlock!!

原本腦中死板板地只有兩個Process加兩個Table的典型Deadlock案例,百思不得其解,兩組完全相同的SQL指令對同一個資料表先讀後寫,順序完全一致,怎麼會冒出Deadlock? 思索好久才恍然大悟,是鎖定升級造成的!! 初期的SELECT動作,因宣告了SERIALIZABLE隔離層級,SELECT時對PK_LockLab放上了Shared Lock;之後要UPDATE時,再升級成Exlusive Lock,但此時另一個Process已放了Shared Lock,故要等待對方釋放Lock才能繼續。然而不久之後,對方也想放Exclusive Lock,卻卡在前Process的Shared Lock。碰! Deadlock!!!

想通了這點,問題其實不難解。在此情境下,我們可在SELECT時透過UPDLOCK提示要求SQL直接使用Update Lock(U),避開先S後X的兩階段鎖定過程,便能排除形成Deadlock的條件。將T-SQL改成以下寫法,就能避免Deadlock囉! (但第二個執行的Process需等待第一個Process執行完畢才能SELECT成功,故總共要20秒才能執行完畢,合理。)

SETTRANSACTIONISOLATIONLEVEL SERIALIZABLE;
BEGINTRAN
DECLARE @i INT;
SELECT @i = Seq FROM LockLab (UPDLOCK)WHERE Code='JEFF'AND YearMon='201212'
SET @i = @i + 1;
WAITFOR DELAY '00:00:10'
UPDATE LockLab SET Seq = @i WHERE Code = 'JEFF'AND YearMon='201212'
--其他程式邏輯(略)
COMMIT TRAN

在MSDN Lock Modes說明中,也提到了這點:

更新 (U)鎖定模式
用於可更新的資源上。防止當多個工作階段正在讀取、鎖定及後來可能更新資源時發生常見的死結

回頭想想,過去咬定Deadlock"一個巴掌拍不響"的迷思,恐怕曾導致自己在處理Deadlock議題時多次誤入歧途而不自知,難免心頭一驚。但至少今天起對Deadlock的形成情境又有了新的認識,猶未晚矣~

【茶包射手日記】jQuery自動完成在IE7無法點選提示項目

$
0
0

網友Barry提問,網站套用jQuery AutoComplete Plugin,在IE7下無法用滑鼠點選結果項目,只能透過上下鍵移動選取。

看來得用IE7重現及分析錯誤,此時最痛苦的莫過於沒有IE Dev Tools可用,少了+9雙手劍,只能丟石頭打怪好悲情呀~ 幸好,IE7還有個IE Dev Toolbar可用,拿支小匕首聊勝於無。

在IE7上重現無法點選提示項目的情境,再開啟IE Dev Toolbar,觀察提示區塊的結構,發現DIV class=ac_reulsts下有一個IFRAME,接著才是列出結果的UL,而我注意到該IFRAME的z-index是10,意思是IFRAME會出現在UL上方,造成UL區被遮蔽,猜想就是導致無法使用滑鼠操作的原因。

追查原始碼,發現插入IFRAME是jquery.autocomplete.js為了處理IE6 SELECT蓋不住問題加入的Hacking:

if ($.browser.msie)
{
    // we put a styled iframe behind the calendar so HTML SELECT elements don't show through
    $results.append(document.createElement('iframe'));
}

(此時,我才發現同樣的問題在IE8/9也會發生... 登楞! 那我拿小匕首殺怪是為了什麼? 只好用"偶爾懷舊發思古之幽情有益身心"自我安慰。)

比對了我的範例網頁,同樣有插入IFRAME,滑鼠運作正常,進行交叉比對:

發現差異在於範例的版本z-index為-1,使其被排在<UL>的下方,才不會遮蓋<UL>阻礙滑鼠操作。

利用Trace Style功能,找到問題網站是因為 /js/201112/jquery.autocomplete.css .ac_results IFRAME將Z-INDEX設為10而導致問題。用IE Dev Toolbar強制將IFRAME z-index Attribute設為-1,再移除div.ac_results的display:none讓隱藏的結果提示區顯現,此時測試滑鼠移動會引發Hover反白效果,也能點選結果,問題排除,全案宣告偵破~


ReportViewer Excel檔的考驗: EPPlus、NPOI與Open XML SDK

$
0
0

前陣子曾排除過一枚EPPlus處理ReportViewer匯出xlsx的Bug,繼續深入才發現事情遠比想像複雜: 表格式報表經ReportViewer匯出成Excel檔,透過EPPlus處理存檔後,用Excel開啟又再次爆出xl/styles.xml及xl/worksheets/sheet1.xml損壞訊息,經修復可讀取,但已原本的格式、顏色設定盡失。


圖1 ReportViewer匯出的原始Excel檔,實驗目標是將"A1"儲存格改成"已修改"


圖2 EPPlus處理後發生錯誤,樣式遺失

試著追進Source Code,發現問題埋得頗深。由於EPPlus在存檔時會重新以自己的角度詮釋及重整Open XML文件,即使我們只改一個欄位,XML也會經過一番重構;當原始內容偏複雜,就會產出不符合Excel或Open XML標準的結果。評估要修正這些問題將涉及大規模核心邏輯翻寫,工程不小,有違想利用元件簡化開發的初衷。還在Alpha階段的NPOI 2.0已能支援xlsx,決定也讓它上場試試,考驗一下能否正確處理ReportViewer的匯出檔。


圖3 NPOI存檔後也發生錯誤,遺失樣式設定

可惜,NPOI也闖關失敗,修復訊息中出現很嚇人的"災難性的失敗",且修改結果也未出現。

最後,只能考慮相形之下較難用的Open XML SDK(目前已經出到2.5,但還是只像為硬綁綁的XML套了層絨布套,坐了屁股照樣會疼),由於它基本上是用維護XML文件的角度進行操作,不會重構翻寫XML,破壞結構風險低很多。而它也是本次測試唯一修改成功且Excel開啟正常的範例。


圖4 Open XML SDK 2.5修改結果

附上程式碼供參考:

staticvoid Main(string[] args)
{
string src = @"d:\temp\source.xlsx";
 
//NOPI
    IWorkbook workbook = new XSSFWorkbook(src);
    ISheet sheet = workbook.GetSheetAt(0);
    sheet.GetRow(0).GetCell(0).SetCellValue("已修改");
    FileStream sw = File.Create(@"d:\temp\npoi.xlsx");
    workbook.Write(sw);
    sw.Close();
 
//EPPlus
using (ExcelPackage p = new ExcelPackage(new FileInfo(src)))
    {
        var sht = p.Workbook.Worksheets.First();
        sht.Cells[1, 1].Value = "已修改";
        p.SaveAs(new FileInfo(@"d:\temp\epplus.xlsx"));
    }
 
//OpenXML SDK 2.5
//REF: http://msdn.microsoft.com/en-us/library/office/cc850837.aspx
string dst = src.Replace(Path.GetFileName(src), "sdk.xlsx");
    File.Copy(src, dst, true);
using (var shtDoc = SpreadsheetDocument.Open(dst, true))
    {
        var sht = shtDoc.WorkbookPart.Workbook.Descendants<Sheet>().First();
        var shtPart = shtDoc.WorkbookPart.GetPartById(sht.Id) as WorksheetPart;
        var cell = shtPart.Worksheet.Descendants<Row>().First()
                    .Descendants<Cell>().First();
        cell.RemoveAllChildren();
//REF: InlineString http://bit.ly/ZpUf18
        var ins = new InlineString();
        ins.AppendChild(new Text("已修改"));
        cell.AppendChild(ins);
        cell.DataType = 
new DocumentFormat.OpenXml.EnumValue<CellValues>(
                CellValues.InlineString);
//shtPart.Worksheet.Save();
        shtDoc.WorkbookPart.Workbook.Save();
        shtDoc.Close();
    }
}

使用Open XML SDK保護工作表不被修改

$
0
0

在先前測試中,Open XML SDK是唯一挑戰ReportViewer匯出Excel檔修改成功的程式庫,手邊的下一步需求是要將工作表(Worksheet)設為不可修改。

在Open XML SDK中,有個SheetProtection類別,將其加入xlsx的XML結構,就可向應用程式宣告該工作表允許或禁止的操作,例如: 刪除欄(deleteColumns)、重設儲存格格式(formatCells)、插入列(insertRow)... 等等。在SheetProctection設定可以指定解除鎖定的密碼雜湊值(Hash),甚至可採SHA-512再配合Salt反覆計算多次的高強度雜湊演算法,但必須強調,由於設定保護後的xlsx仍是一個可被解讀的ZIP檔,其中XML還是可能被編輯修改,即便不知密碼,有心人只需移除XML上的宣告即可移除保護,故工作表保護機制只防君子不防小人,不宜視作資安管控機制。

延伸閱讀: Overview of Protected Office Open XML Documents
Note, after implementing the document protection mechanism programmatically the document is not considered secure since the password is stored in plain text in the OOXML document structure and can fairly easily be obtained and/or removed by editing the “workbook.xml” file, under the “xl” folder (or the “document.xml” file for Word, under the “word” folder) in the ZIP package.  By comparison, a Compound File Binary file protected document is considered more secure since the password is stored in an encrypted stream in the CFB file format.

雖說不到資安防護的等級,工作表保護在一般情境下已可初步防止End-User任意更動報表數字混淆視聽,仍有一定實用性,以下是簡單程式範例:

//OpenXML SDK
string dst = src.Replace(Path.GetFileName(src), "sdk.xlsx");
    File.Copy(src, dst, true);
using (var shtDoc = SpreadsheetDocument.Open(dst, true))
    {
        var sht = shtDoc.WorkbookPart.Workbook.Descendants<Sheet>().First();
        var shtPart = shtDoc.WorkbookPart.GetPartById(sht.Id) as WorksheetPart;
//建立一個SheetProtection物件
        var proc = new SheetProtection()
        {
            Password = new HexBinaryValue("ABCD"),
            Sheet = true,
            Objects = true,
            Scenarios = true
        };
//需安插於sheetData後方
        shtPart.Worksheet.InsertAfter<SheetProtection>(proc, 
            shtPart.Worksheet.Descendants<SheetData>().First());
        shtDoc.WorkbookPart.Workbook.Save();
        shtDoc.Close();
    }

補充說明: SheetProtection.Password的值會被當成密碼的雜湊值,故使用Excel開啟時,直接輸入ABCD是無法解密的,然而輸入什麼密碼才會產生ABCD這種雜湊值是個謎,等於沒人能在Excel中用密碼解鎖。誰都猜不出解鎖密碼符合我的應用情境,但如果想實現在Excel用密碼解鎖,MSDN論壇有篇高手寫的SheetProtection.Password雜湊演算法可由指定的密碼字串推算Password值,很值得參考。

【2012-12-28補充】新選擇: 令人驚豔的Excel程式庫 - ClosedXML

令人驚豔的Excel程式庫 - ClosedXML

$
0
0

處理ReportViewer匯出檔的比武大會上,NPOI與EPPlus都敗下陣來,Open XML SDK雖然勝出,但在應用呼叫上繁瑣難搞,用起來總覺礙手礙腳。在研究Open XML SDK設定工作表保護的過程,發現新大陸 - 另一套Open Source的Excel程式庫,ClosedXML

簡單整理ClosedXML特色如下:

  1. 程式庫很俏皮地命名為ClosedXML,事實它高度依賴Open XML SDK,在引用時,程式必須一併參考DocumentFormat.OpenXml.dll。ClosedXML切入的角度是為Open XML SDK提供容易操作的程式介面,而事實證明它做得很成功,程式介面的確非常簡潔易用。
  2. 以Open XML SDK為基礎,所以只支援xlsx,不支援xls格式。
  3. CodePlex上就文件與範例,內容十分完整,很容易上手。
  4. 支援NuGet安裝,加入時會一併帶入Open XML SDK參照,安裝簡單。
  5. ClosedXML的很多Method在設計上仿照Excel VBA慣例,例如: sheet.Cell("A1").Value = "Boo"、sheet.Range("A1:C5")選取範圍,用起來相當簡單直覺。
  6. 很重要的一點,先前讓EPPLUS及NPOI灰頭土臉的ReportViewer匯出檔測試,ClosedXML輕易過關,產出結果與Excel相容,程式碼又比Open XML SDK簡短易理解,大勝!  (以下範例順便展示了保護工作表功能,一行搞定。)
    //ClosedXML
        var wb = new XLWorkbook(src);
        var ws = wb.Worksheets.First();
        ws.Cells("A1").Value = "已修改";
        ws.Protect("LetMeEdit");
        wb.SaveAs(@"d:\temp\closedXml.xlsx");

初步評估,ClosedXML支援不少Excel VBA風格的簡潔API,在ReportViewer匯出檔案相容性測試又比NPOI及EPPlus好,看起來很值得一試!

最後不能免俗地,比照NPOIEPPlus,要用ClosedXML試做網站檔案結構轉Excel的範例:

/// <summary>
/// 將目錄下的目錄檔案結構匯出成Excel工作表
/// </summary>
/// <param name="dirPath">要匯出的目錄路徑</param>
/// <param name="excelPath">匯出Excel路徑</param>
/// <param name="filter">過濾函數,傳入Path進行判斷,傳回true時表排除</param>
/// <returns></returns>
publicstaticvoid WebTreeToExcel(
string dirPath, string excelPath,
        Func<string, bool> filter = null)
    {
//將目錄結構整理成清單
        List<WebItem> list = new List<WebItem>();
        explore(list, dirPath, 0);
//建立Excel
        XLWorkbook workbook = new XLWorkbook();
        var sheet = workbook.Worksheets.Add("Site Tree");
int colIdx = 1;
foreach (string colName in"Path;File;Description".Split(';'))
        {
            sheet.Cell(1, colIdx++).Value = colName;
        }
//修改標題列Style
        var header = sheet.Range("A1:C1");
        header.Style.Fill.BackgroundColor = XLColor.Green;
        header.Style.Font.FontColor = XLColor.Yellow;
        header.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
 
int rowIdx = 2;
foreach (var item in list)
        {
//若bypass檢測傳回true,則略過該筆
if (filter != null&& filter(item.Path))
continue;
//將Path放在第一欄(稍後隱藏)
            sheet.Cell(rowIdx, 1).Value = item.Path;
//存入檔名或目錄名
            sheet.Cell(rowIdx, 2).Value =
new String(' ', item.Layer * 4) + item.Name;
if (item.IsFolder)
            {
                sheet.Cell(rowIdx, 2).Style.Font.FontColor = XLColor.Blue;
            }
            rowIdx++;
        }
//第一欄隱藏
        sheet.Column(1).Hide();
//自動伸縮欄寬
        sheet.Column(2).AdjustToContents();
        sheet.Column(2).Width += 2;
        sheet.Column(3).Width = 50;
//寫入檔案
        workbook.SaveAs(excelPath);
    }

程式簡潔度與EPPlus相近,結果也正確無誤,ClosedXML勝出之處在於產出檔與Excel相容度較高,是一套值得推薦的Excel程式庫。

Hacking樂無窮-為ReportViewer匯出PDF檔加上浮水印

$
0
0

接到一個頗富挑戰性的需求,Reporting Service或RDLC報表可匯出成Excel、PDF等檔案格式,對一般麻瓜型使用者而言,PDF唯讀,Excel則可修改,業務單位希望在拿到報表紙本時加以區分;換句話說,如果能讓PDF與Excel檔的列印結果有別,即可做為報表結果是否唯讀,有無被修改可能的依據。(姑且排除使用者設法修改PDF檔或將Excel仿製成PDF樣式的情境)

我想到一個做法是為匯出的PDF檔加上浮水印。同一張報表匯出的Word、Excel、PDF檔內容理應一致,當PDF檔被加註浮水印,便足以形成區隔。在PDF檔加上浮水印非難事,用iTextSharp應可搞定,但匯出PDF檔的過程本屬ReportViewer內部運作,不容外人插手,要在匯出PDF檔時動手腳需要點Hacking,好一個讓程式魔人熱血沸騰的挑戰!

分析問題的第一步,先剖析ReportViwer匯出PDF檔的原理:

當執行PDF匯出動作時,實際上是呼叫ReportViewer加掛的HttpHandler,web.config可以看到相關設定:

<add path="Reserved.ReportViewerWebControl.axd" verb="*" type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" validate="false" />

在網頁按匯出鈕時,瀏覽器被導向特定URL,傳入OpMode=Export、Format=PDF參數,由HttpHandler傳回當下檢視報表的PDF檔。如要在此過程動手腳,有個不錯的切入點是透過Global.asax或HttpModule攔截BeginRequest事件,遇到呼叫Reserved.ReportViewerWebControl.axd匯檔案時加入自訂邏輯,修改要傳回的檔案內容。但ReportViewer的HttpHandler在PDF檔產生後便立刻寫入HttpRepsonse傳回客戶端,輪不到我們插手,因此下一個挑戰是如何攔截並修改其內容。

此時,ASP.NET的另一個好用機制派上用場: Response.Filter,它允許我們在HttpResponse將結果byte[]寫入輸出Stream之前,先交由我們自訂的Stream物件處理,可以實現修改後再傳至客戶端的目的。(延伸閱讀: 保哥文章 介紹一個 ASP.NET 裡鮮為人知的 Response.Filter 屬性)

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
 
publicclass ExpFileFilterStream : MemoryStream
{
private Stream output = null;
    Func<byte[], byte[]> modifier = null;
private HttpResponse response = null;
privatebool firstFlush = false;
public ExpFileFilterStream(HttpResponse resp, Func<byte[], byte[]> modifier)
    {
        response = resp;
        output = resp.Filter;
this.modifier = modifier;
    }
publicoverridevoid Write(byte[] buffer, int offset, int count)
    {
//由於ReportViewer會關閉BufferOutput,並分成多段Flush傳回前端,
//在此重新啟用Buffer功能(因必須得到檔案完整內容再處理),
//但會漏掉第一次的Flush(),藉以以下邏輯避免第一次部分Flush()
//註: ReportViewer在分段Flush的大小為81920,當少於此值表示不需略過Flush
if (!response.BufferOutput && count == 81920)
        {
            response.BufferOutput = true;
            firstFlush = true;
        }
base.Write(buffer, offset, count);
    }
publicoverridevoid Flush()
    {
if (firstFlush)
        {
            firstFlush = false;
return;
        }
//Flush時,將要傳回內容byte[]交由外部邏輯處理後再取回
byte[] buff = base.ToArray();
if (modifier != null)
            buff = modifier(buff);
        output.Write(buff, 0, buff.Length);
    }
}

我寫了一個簡單的Filter Stream物件,原理是在Write()時先蒐集ReportViewer HttpHandler要傳回的檔案內容,當Flush()要傳回結果時,將先前接收到的PDF檔案內容(byte[])交由外部邏輯,Func<byte[], byte[]>,進行加蓋浮水印處理,再傳回修改版檔案到真正的OutputStream。

其中有小技巧: ReportViewer HttpHandler為了減少記憶體耗用及提高回應效率,會將Response.BufferOutput設為false,讓匯出檔案內容分成多段Flush()傳回(每段不超過81920 bytes)。由於我們需要接收完整檔案才能進行修改並一次回傳,故不容先傳回部分未修改內容的情形發生。在Write()將Response.BufferOutput改回true即可偷偷取消分段傳回,唯此時第一個分段的Flush()已箭在弦上,故要用一個firstFlush旗標避開第一次Flush()。之後因Response.BufferOutput已被設為true,會等到全部的PDF檔都透過Write()寫入才呼叫Flush(),此時MemoryStream所保存的便是完整PDF檔內容。

在BeginRequest事件加掛Response.Filter的工作,則寫成一個HttpModule。程式很單純,較花工夫的是透過iTextSharp在PDF左上角印上PDF yyyy/MM/dd HH:mm:ss樣式的半透明浮水印,iText歷史悠久、功能強大,網路上不難找到現成範例,很順利就完成了。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Web;
using iTextSharp.text.pdf;
using iText = iTextSharp.text;
using iTextSharp.text;
using System.IO;
 
namespace ReportViewerHacking
{
publicclass WaterMarkModule : IHttpModule
    {
#region IHttpModule Members
 
publicvoid Dispose()
        {
        }
 
publicvoid Init(HttpApplication context)
        {
            context.BeginRequest += context_BeginRequest;
        }
 
void context_BeginRequest(object sender, EventArgs e)
        {
            HttpApplication app = sender as HttpApplication;
string url = app.Context.Request.RawUrl;
            var context = app.Context;
if (url.Contains("Reserved.ReportViewerWebControl.axd"))
            {
                var req = context.Request;
                var resp = context.Response;
string opType = req["OpType"];
string name = req["Name"];
string format = req["Format"];
if (opType == "Export"&& format == "PDF")
                {
                    resp.BufferOutput = true;
                    resp.Filter = new ExpFileFilterStream(resp, (buff) =>
                    {
//輸入PDF內容,加上浮水印
                        PdfReader pr = new PdfReader(buff);
                        iText.Rectangle dimension = pr.GetPageSize(1);
                        MemoryStream ms = new MemoryStream();
                        PdfStamper stmp = new PdfStamper(pr, ms);
//REF: http://bit.ly/10qirzK
                        BaseFont bf =
                            BaseFont.CreateFont(BaseFont.TIMES_ROMAN, BaseFont.CP1252, false);
                        iText.Font fnt = new iText.Font(bf, 6, iText.Font.NORMAL, BaseColor.BLACK);
                        PdfContentByte cb = stmp.GetOverContent(1);
//設定半透明文字
                        PdfGState gstate = new PdfGState();
                        gstate.FillOpacity = 0.2f;
                        gstate.StrokeOpacity = 0.2f;
                        cb.SetGState(gstate);
                        cb.BeginText();
                        cb.SetFontAndSize(bf, 6);
                        cb.SetColorFill(BaseColor.BLACK);
                        cb.ShowTextAligned(PdfContentByte.ALIGN_LEFT,
string.Format("PDF {0:yyyy-MM-dd HH:mm:ss}", DateTime.Now),
                            dimension.GetLeft(1), dimension.GetTop(5), 0);
                        cb.EndText();
 
                        stmp.Close();
                        pr.Close();
return ms.ToArray();
                    });
                }
            }
        }
#endregion
    }
}

將HttpModule掛進ASP.NET網站,之後只要ReportViewer匯出PDF檔,就一律會被偷偷加上浮水印,讓我過了小小當駭客的癮,哈!!

2012年的最後一天,就用這篇歡樂的Hacking文劃上句點吧! 祝大家新年快樂~

在Report Server安裝HttpModule

$
0
0

先前完成ReportViewer匯出PDF檔加蓋浮水印的把戲,想套用到SSRS(SQL Server Reporting Service)上,二者原理相近,差別在於SSRS使用的是"/ReportServer/ReportServer?rs:Command=Render&rs:Format=IMAGE&..." URL進行匯出作業,故只需稍加修改BeginRequest的URL過濾條件,一樣能透過HttpModule掛載HttpResponse.Filter加入修改匯出檔的程序。

修改C:\Program Files\Microsoft SQL Server\MSSQL\Reporting Services\ReportServer\web.config加上HttpModule設定後,卻導致ReportServer網站應用程式完全無法運作,彈出以下錯誤訊息:

[SecurityException: Request for the permission of type System.Web.AspNetHostingPermission, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 failed.]
   SSRS2000Hacking.WaterMarkModule.Init(HttpApplication context) +0
   System.Web.HttpApplication.InitModules() +100
   System.Web.HttpApplication.InitInternal(HttpContext context, HttpApplicationState state, MethodInfo[] handlers) +1330
   System.Web.HttpApplicationFactory.GetNormalApplicationInstance(HttpContext context) +392
   System.Web.HttpApplicationFactory.GetApplicationInstance(HttpContext context) +256
   System.Web.HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr) +414

錯誤訊息指出,問題出在設定BeginRequest事件的動作需要特定CAS權限,研判是ReportServer網站應用程式基於安全考量調降了一般ASP.NET程式執行權限,HttpModule因而也承襲低階權限,執行事件掛載動作時產生權限不足錯誤。查看ReportServer\web.config發現以下設定:

  <securityPolicy>
    <trustLevel name="RosettaSrv" policyFile="rssrvpolicy.config" />
  </securityPolicy>
<trust level="RosettaSrv" originUrl="" />

原來ReportServer設定了一組自訂securityPolicy,其內容儲存於rssrvpolicy.config,除了ReportServer運作必須的組件外,預設不賦與任何CAS權限。因此,解決此一權限問題最簡單方法是修改rssrvpolicy.config,將我們的匯出檔浮水印HttpModule DLL調成FullTrust安全等級(資安提醒: 本案例因程式為自行開發,可擔保其中不含惡意程序或安全漏洞,故授與FullTrust安全等級沒有風險。若調高安全等級的對象為外來元件,請務必確認其安全無虞方可為之。)

我採用的做法是在ReportServer\rssrvpolicy.config中為HttpModule DLL(SSRS2000WatermarkModule.dll)加上設定,以URL為比對依據,調為FullTrust等級:

<CodeGroup
        class="UnionCodeGroup"
        version="1"
        PermissionSetName="FullTrust">
    <IMembershipCondition
            class="UrlMembershipCondition"
            version="1"
            Url="$CodeGen$/*"
    />
</CodeGroup>
<CodeGroup
        class="UnionCodeGroup"
        version="1"
        PermissionSetName="FullTrust">
    <IMembershipCondition
            class="UrlMembershipCondition"
            version="1"
            Url="$AppDirUrl$/bin/SSRS2000WaterMarkModule.dll"
    />
</CodeGroup>   

加入設定後,浮水印模組就運作如常囉~

Viewing all 2458 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>