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

裝飾者模式(Decorator Pattern)實例:壓縮加密一次完成

$
0
0

手邊有個自訂傳輸管道的加密需求,預期資料量可能高達數MB,為提升效能,先壓縮再加密是不錯的做法,既可減少壓縮時間及成本,又能節省頻寬,一舉兩得。

過去用C#寫過DES加密,也寫過GZip壓縮,把兩個結合起來不是難事。如果不要想太多,取得待處理資料(byte[]),用GZipStream壓縮可得壓縮後資料(byte[]),再以壓縮後資料當成輸入參數交給CryptoStream進行DES加密,就得到壓縮並加密的結果(byte[])。

事實上,有更輕巧簡潔的做法-.NET的Stream支援設計模式(Design Pattern)裡的裝飾者模式(Decorator Pattern,也有人翻成修飾模式),裝飾者模式的精神在於「不使用繼承,而以組裝方式動態地為物件加入新功能」。常見的做法是設計一個裝飾類別,以被裝飾物件A做為建構式參數,而裝飾類別提供與被裝飾對象相同的方法,讓呼叫端使用跟原來相同的介面呼叫,得到裝飾類別加工後的結果。以GZip壓縮使用的GZipStream為例,假設原本有個FileStream fs,呼叫fs.Write()可以將byte[]寫入檔案。若要加入壓縮功能,我們可以建立一個GZipStream gzip = new GZipStream(fs, CompressionMode.Compress),寫入時改呼叫gzip.Write(),資料使會先經GZip壓縮再寫入檔案。如果要做到動態決定要不要壓縮,則可將變數stream宣告成抽象型別Stream(FileStream、GZipStream共同繼承的繼承來源),一開始stream = new FileStream(…),若決定壓縮,就讓stream = new GZipStream(stream, CompressionMode.Compress),寫入資料時則一律呼叫stream.Write(),便能實現執行期間動態切換是否壓縮,展現裝飾者模式的優勢。

解釋完概念,以下是完整程式範例:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Demo
{
class Program
    {
//以金鑰字串進行雜湊運算產生DES所需的Key及IV byte[]
publicstatic Tuple<byte[], byte[]> GetKeyIV(string keyString)
        {
            MD5 md5 = MD5.Create();
            var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(keyString));
byte[] key = newbyte[8];
byte[] iv = newbyte[8];
            System.Buffer.BlockCopy(hash, 0, key, 0, 8);
            System.Buffer.BlockCopy(hash, 8, iv, 0, 8);
returnnew Tuple<byte[],byte[]>(key, iv);
        }
publicstaticbyte[] Encrypt(string key, byte[] data, bool compress = false)
        {
using (MemoryStream ms = new MemoryStream())
            {
                DESCryptoServiceProvider des = new DESCryptoServiceProvider();
                var keyIv = GetKeyIV(key);
//先建立加密用的CryptoStream
using (CryptoStream crypto = new CryptoStream(ms,
                    des.CreateEncryptor(keyIv.Item1, keyIv.Item2), CryptoStreamMode.Write))
                {
//依compress參數將Stream gzip設成GzipStream
//若不需壓縮,gzip直接等於CryptoStream crypto
using (Stream gzip =
                        compress ?
                        (Stream)new GZipStream(crypto, CompressionMode.Compress) :
                        (Stream)crypto)
                    {
//不管是CryptoStream或GZipStream
//一視同仁寫入byte[]就對了
                        gzip.Write(data, 0, data.Length);
                    }
                }
return ms.ToArray();
            }
        }
publicstaticbyte[] Decrypt(string key, byte[] crypted, bool decompress = false)
        {
//用MemoryStream將傳入的byte[]包裝成Stream物件
using (MemoryStream ms = new MemoryStream(crypted))
            {
                DESCryptoServiceProvider des = new DESCryptoServiceProvider();
                var keyIv = GetKeyIV(key);
//解密兼解壓時,CryptoStream一樣在外層
using (CryptoStream crypto = new CryptoStream(ms,
                    des.CreateDecryptor(keyIv.Item1, keyIv.Item2), CryptoStreamMode.Read))
                {
using (Stream gzip =
                        decompress ?
                        (Stream)new GZipStream(crypto, CompressionMode.Decompress) :
                        (Stream)crypto)
                    {
//另外宣告一個MemoryStream用來存結果
using (MemoryStream msDec = new MemoryStream())
                        {
byte[] buff = newbyte[8192];
int len = 0;
//由於不知結果資料長度,持續讀取
//由Read()傳回長度偵測是否已到結尾
while ((len = gzip.Read(buff, 0, buff.Length)) > 0)
                            {
                                msDec.Write(buff, 0, len);
                            }
return msDec.ToArray();
                        }
                    }
                }
            }
        }
staticvoid Main(string[] args)
        {
            WebClient wc = new WebClient();
byte[] testData = wc.DownloadData(
"http://cdn.kendostatic.com/2014.1.318/js/kendo.all.min.js");
            var encoding = Encoding.UTF8;
int displayLen = Math.Min(testData.Length, 128);
            Console.WriteLine("來源長度={0:n0}", testData.Length);
            Console.WriteLine("內容預覽: {0}...", 
                encoding.GetString(testData, 0, displayLen));
            Console.WriteLine();
            var encKey = "SECRET";
byte[] encrypted = Encrypt(encKey, testData);
            Console.WriteLine("加密後長度={0:n0}", encrypted.Length);
            Console.WriteLine("內容預覽: {0}...", 
                encoding.GetString(encrypted, 0, displayLen));
            Console.WriteLine();
byte[] decrypted = Decrypt(encKey, encrypted);
string srcB64 = Convert.ToBase64String(testData);
            Console.WriteLine("解密還原驗證: {0}",
                srcB64 == Convert.ToBase64String(decrypted));
byte[] compressEncrypted = Encrypt(encKey, testData, true);
            Console.WriteLine("壓縮加密後長度={0:n0}", compressEncrypted.Length);
            Console.WriteLine("內容預覽: {0}...", 
                encoding.GetString(compressEncrypted, 0, displayLen));
            Console.WriteLine();
byte[] decompressDecrypted = Decrypt(encKey, compressEncrypted, true);
            Console.WriteLine("解密還原驗證: {0}",
                srcB64 == Convert.ToBase64String(decompressDecrypted));
            Console.Read();
        }
    }
}

以Kendo UI Libaray為測試對象,原本長度為1,493,858,加密後大小為1,493,864,啟用壓縮再加密後大小為442,504,解密、解壓縮還原後與原始內容比對一致,測試成功。


介紹jQuery map()與grep()

$
0
0

寫這篇的動機是常在專案看到「古典式」JavaScript陣列處理,例如:跑迴圈將物件陣列的某個字串屬取出轉成字串陣列、篩選物件陣列取得特定類別的集合。用for迴圈處理沒什麼不對,但既然專案已經用了jQuery,能一行搞定卻寫成三五行不免可惜(程式又不按行數計酬,寫愈多手愈酸咩 XD)。感覺上還有些朋友不認識$.map()$.grep()這兩個好東西,故寫篇文章推薦一下。

若用LINQ做比方,$.map()相當於 .Select(o => 傳回數值或其他型別物件).ToArray(),可將物件陣列轉換成某個屬性或其他型別物件組成的陣列;$.grep()則類似 .Where(o => 傳回布林值).ToArray(),可過濾陣列保留符合條件的元素項目。

使用概念如下,輸入陣列,執行後傳回陣列:

$.map(array, function(element, index) { return 新陣列的元素; });
(註:若輸出陣列想略過特定元素,就return null或undefined)

$.grep(array, function(element, index) { return true或false; });
(傳回true的元素才會出現在篩選結果)

來看實例,以下程式有四個範例,分別用傳統方法及$.map(), $.grep()實現「物件陣列轉字串陣列」以及「過濾物件陣列保留特定類別」,程式不複雜,直接看程式碼,開始想想可以用在哪些專案角萿吧!
線上展示

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>jQuery map & grep</title>
</head>
<body>
<scriptsrc="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
function Product(category, name) {
this.category = category;
this.name = name;
    }
var items = [
new Product("Phone", "iPhone4S"),
new Product("Phone", "Lumia 920"),
new Product("Phone", "小米"),
new Product("Pad", "Nexus7"),
new Product("Pad", "iPad")
    ];
//範例一:取得產品名稱字串陣列
var names = [];
for (var i = 0; i < items.length; i++) {
      names.push(items[i].name);
    }
//同場加映:JSON.stringify時傳入額外參數加上縮排
    alert(JSON.stringify(names, null, 4));
//範例二:篩選手機類的產品陣列
var phones = [];
    $.each(items, function(i, item) {
if (item.category == "Phone")
        phones.push(item);
    });
    alert(JSON.stringify(phones, null, 4));
//jQuery改良版
//範例三:取得產品名稱字串陣列$.map()
var name = $.map(items, function(item) { 
return item.name; 
    });
    alert(JSON.stringify(names, null, 4));
//範例四:篩選手機類的產品陣列$.grep()
var phones = $.grep(items, function(item) {
return item.category == "Phone";
    });
    alert(JSON.stringify(phones, null, 4));
</script>
 
</body>
</html>

【茶包射手日記】object內嵌PDF文件在IE7無法顯示

$
0
0

接獲報案,某網頁使用<object data="url_to_pdf" type="application/pdf">技巧內嵌網頁,在IE7無法顯示,只出現全灰背景。經過一番冗長調查及測試,查出與Acrobat Reader及IE版本有關。

我是利用以下網頁重現問題:

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Embedded PDF</title>
<style>
        object {
            display: block;
            width: 600px;
            height: 150px;
            margin-bottom: 12px;
        }
</style>
</head>
<body>
<objectdata="which-prog-lang.pdf"type="application/pdf">
<!-- 提示安裝PDF Reader或提供PDF檔下載連結(在此忽略) -->
</object>
<objectdata="http://192.168.0.17/WebFormLab/LabEmbPdf/which-prog-lang.pdf"
type="application/pdf"></object>
</body>
</html>

在網頁中放了兩個<object>內嵌PDF文件,利用data指定PDF檔案位址,第一個用相對路徑,第二個用絕對路徑,如此可產生我們想要的差異:前者PDF永遠來自同一網站,後者則視網頁的URL決定PDF來自同一網站或跨網站。以上面的例子,若使用httq://192.168.0.17/…存取網頁,第二個<object>的PDF來自同網頁;若使用httq://機器名稱/…或httq://localhost/…連上網頁,則變成跨網站存取PDF。

故意使用localhost連上網站,讓第二個PDF變成跨網站存取,使用Chrome或IE11標準模式,二個PDF都能正常顯示:

將IE11切為相容IE10/IE9也還正常,一旦切成IE7/IE8相容模式,第二個<object>會彈出跨站台讀取警示,按Yes會顯示PDF,選No則為一片灰色:

若將網頁URL改為httq://192.168.0.17/…,IE7/IE8模式均可直接顯示:

再將網頁URL改為httq://127.0.0.1/…,則不會有跨網站存取確認,第二個PDF <object>無法顯示:

localhost有提示,127.0.0.1直接拒絕,讓我聯想到與安全性區域有關。果不其然,若將127.0.0.1歸入近端內部網路,就會像存取localhost時一樣彈出跨網讀取確認,若加入信任的網站則一樣是直接拒絕。

【結論】

在IE11的IE7/IE8模式(或使用IE7/IE8)以<object>跨網站存取PDF,如安全性區域為「近端內部網路(Local Intranet)」需經使用者確認才能載入,其餘安全性區域則一律拒絕存取,無法顯示物件或呈現灰色背景。(灰色背景的樣子可參考下圖,測試環境為在VM使用IE7且安全性區域設成網際網路)

Chrome認定內嵌HTTPS IFrame未使用安全連線

$
0
0

同事回報:在HTTP網頁中使用IFrame內嵌HTTPS網頁,檢視框架資訊時,可看到HTTPS網站的SSL憑證資訊,但卻出現連線未加密。

我做了一個實驗:在172.28.1.1網站放一個測試網頁,其中放入IFrame連向https://www.microsoft.com/language,以Chrome 43.0.2357.124 m 版本檢視,在IFrame按右鍵選取「檢視框架資訊」:

框架資訊表示「您與這個網站建立了私人連線」,但連線狀態卻是「您到 www.microsoft.com的連線未加密」,接著又說連線使用TLS 1.2,矛盾得很。

使用F12開發者工具監看傳輸內容,確實是走HTTPS沒錯。

疑似Chrome的Bug(或預設行為),本以為HTTP內嵌HTTPS才會發生。進一步測試,所有HTTPS IFrame都呈現相同狀況,例如JSFiddle HTTPS內嵌Twitter HTTPS:

那如果IFrame與其所在網頁都是來自同一個HTTPS網站呢?

用了一點小技巧,將JSFiddle的結果視窗IFrame 改為<iframe name="result" src="/">內嵌走HTTPS的母網頁,Chrome依然認定IFrame的連線未加密。

爬文找到類似提問,但無明確結論。歸納以上實測,暫以「Chrome針對IFrame內嵌之HTTPS網頁,顯示的框架資訊一律認定連線未加密,與事實不符,疑為Bug」結案。

IIS 7+禁止URL路徑使用加號代表空白

$
0
0

網友回報,部落格的標籤(Tag)分類查詢,遇到標籤包含空白(例如:Kendo UI、Windows 8…)會出現HTTP 404.11錯誤:

錯誤訊息已提供明確指示,有幾個關鍵字,allowDoubleEscaping、RequestFilteringModule,推測IIS是基於某些資安考慮封鎖了路徑出現+的URL,調整allowDoubleEscaping設定即可排除。所以,把閞關打開就沒事了嗎?

不,這不是我的風格,IIS之所以會加入新限制一定有理由,移除前應審慎考慮其風險(遇到資安就變龜毛是我的傳統,例如:多想兩分鐘,你可以不必教User關掉Vista UAC 多想兩分鐘,你可以不用 validateRequest=-false),這回,或許也該「多想兩分鐘,你可以不要打開allowDoubleEscaping」。

URL參數(Query String)編碼時空白會轉成+是常識,在過去URL路徑也採納相同的規則(CGI時代的標準),但依據新的HTTP規範,URL路徑的空白應轉為%20,只有URL參數的空白才應該轉成+。

+ means a space only in application/x-www-form-urlencoded content, such as the query part of a URL: http://www.example.com/path/foo+bar/path?query+name=query+value
In this URL, the parameter name is query name with a space and the value is query value with a space, but the folder name in the path is literally foo+bar, notfoo bar. 參考

而這也是為什麼JavaScript不建議再用esacpe(),改用encodeURI()或encodeURIComponent()的理由。延伸閱讀:encodeURI / encodeURIComponent的使用時機

問題來了,部落格平台程式萬年沒改過(羞愧),標籤查詢URL何以一夕出錯?我想起,由於Windows 2003平台支援中止在即,Hosting廠商最近將網站搬到Windows 2012R2上,IIS從IIS 6升級到IIS 8.5,二者對URL把關標準不同,開始出錯。

IIS Team Blog的這篇文章用一個實例解釋了什麼為IIS要禁止URL路徑使用加號:

<authorization vdir="my vdir">
    <allowed users="Administrators"/>
</authorization>

若允許URL將+轉成空白,則httq://the-web-server/my+vdir/可以存取"my vdir"路徑,卻不受權限設定規範。簡單來說,這項額外轉碼規則易形成URL樣式比對上的漏洞或增加比對複雜度,當比對與安全控管有關,就會帶來風險。

認同這樣的安全考慮,乖乖地修改程式,在產生URL路徑時,用%20取代+, 問題排除。

PS:感謝網友Alex Tam通報網站錯誤。

StreamReader讀取InputStream注意事項

$
0
0

用了這麼多年,這幾天才發現SteamReader的一項行為。故事從jQuery.post內容給MVC接收說起…

我有一段MVC Action程式,會從Request.InputStream接收來自jQuery.ajax送來的內容,為求簡化起見,就拿舊文範例來示範:

@{
    ViewBag.Title = "Home Page";
}
<br/>
<buttonid="btnPost">Post Content to Action</button>
@section scripts {
<script>
//以application/json ContentType傳送JSON字串到Server端
    jQuery.postJson = function (url, data, callback, type) {
if (jQuery.isFunction(data)) {
            type = type || callback;
            callback = data;
            data = undefined;
        }
return jQuery.ajax({
            url: url,
            type: "POST",
            dataType: type,
            contentType: "application/json",
            data: typeof (data) == "string" ? data : JSON.stringify(data),
            success: callback
        });
    };
    $("#btnPost").click(function () {
var players = [{
            Id: 1000, Name: "Darkthread",
            RegDate: new Date(Date.UTC(2000, 0, 1)),
            Score: 32767
        }, {
            Id: 1024, Name: "Jeffrey",
            RegDate: new Date(Date.UTC(2000, 0, 1)),
            Score: 9999
        }];
        $.postJson("@Url.Content("~/home/send")",
            players, function (res) {
            alert(res);
        });
    });
</script>
}

MVC Action的邏輯很簡單,用StreamReader.ReadToEnd()讀取Request.InputStream內容,將讀到的字串回傳給前端驗證:

public ActionResult Send()
{
using (var sr = 
new StreamReader(Request.InputStream))
    {
return Content("POST Body=" + sr.ReadToEnd());
    }
}

輕鬆搞定!

後來,有個新需求-每次呼叫時需側錄Request內容存證,方便日後除錯或做為呈堂證供。

這也難不倒我,寫個IActionFilter就可以搞定:(這裡以Debug.Write示意,實際應用會寫Log)

publicclass LogPostBodyAttribute : FilterAttribute, IActionFilter
{
publicvoid OnActionExecuted(ActionExecutedContext filterContext)
    {
    }
publicvoid OnActionExecuting(ActionExecutingContext filterContext)
    {
        var req = filterContext.HttpContext.Request;
if (req.HttpMethod == "POST")
        {
            var inp = req.InputStream;
using (var sr = new StreamReader(inp))
            {
                var postBody = sr.ReadToEnd();
//將POST內容寫入Log檔,此處以Debug.WriteLine示意
                Debug.WriteLine("SrcIp: " + req.UserHostAddress);
                Debug.WriteLine("Content: " + postBody);
            }
        }
    }
}

寫好IActionFilter,在Action加註[LogPostBody]即可生效:

[LogPostBody()]
public ActionResult Send()
{
using (var sr = 
new StreamReader(Request.InputStream))
    {
return Content("POST Body=" + sr.ReadToEnd());
    }
}

測試執行,果然在Output視窗觀察到側錄內容,成功!

開心不到1分鐘,就發現Action被我弄壞了 弄壞了 弄壞了…

經過爬文研究,理解到一件長期被我忽略的StreamReader行為:

This method is called by the public Dispose method and the Finalize method. Dispose invokes the protected Dispose method with the disposing parameter set to true. Finalize invokes Dispose with disposing set to false.

When the disposing parameter is true, this method releases all resources held by any managed objects that the StreamReader object references. This method invokes the Dispose method of each referenced object. 
from StreamReader.Dispose(bool disposing)

This implementation of Close calls the Dispose method passing a true value.
from StreamReader.Close()

當我們呼叫StreamReader.Close()或StreamReader.Dispose()時,背後都會觸發Dispose(true),導致正在使用的基底Stream物件也被Dispose()。而我在FilterAction中用using (var sr = new StreamReader(InputStream)) { … },using範圍結束時將呼叫StreamReader.Dispose(),InputStream跟著被Dispose(),之後輪到Send Action讀取InputStream內容時,由於InputStream已被銷毁,因此讀不到內容。

查了MSDN文件,得知Finalize()及Dispose(false)可以結束StreamReader但不Dispose底層的Stream,但受限於protected宣告無法從外部呼叫,故必須移除using並避免呼叫Close()、Dispose(),而讀取完畢記得Seek(0, SeekOrigin.Begin)將位置歸零。

publicvoid OnActionExecuting(ActionExecutingContext filterContext)
{
    var req = filterContext.HttpContext.Request;
if (req.HttpMethod == "POST")
    {
        var inp = req.InputStream;
        var sr = new StreamReader(inp);
        var postBody = sr.ReadToEnd();
//讀取完畢要將讀取位置還原到起始點
        inp.Seek(0, SeekOrigin.Begin);
//將POST內容寫入Log檔,此處以Debug.WriteLine示意
        Debug.WriteLine("SrcIp: " + req.UserHostAddress);
        Debug.WriteLine("Content: " + postBody);
//注意:不要呼叫sr.Close()或sr.Dispose(),
//否則StreamReader會終結底層的Stream物件釋放資源
    }
}

看到StreamReader沒被using包起來,心裡總覺得怪怪的… 我猜微軟RD也有人跟我一樣龜毛,所以從.NET 4.5起,StreamReader多了一個建構式,接受bool leavingOpen參數:StreamReader 建構函式 (Stream, Encoding, Boolean, Int32, Boolean) (System.IO)

當 leavingOpen == true,StreamReader在Dispose()時就不會強制關閉使用的Stream,故程式可改為:

publicvoid OnActionExecuting(ActionExecutingContext filterContext)
{
    var req = filterContext.HttpContext.Request;
if (req.HttpMethod == "POST")
    {
        var inp = req.InputStream;
//https://goo.gl/A6AKGC .NET 4.5新增leaveOpen參數
//當leaveOpen=true,StreamReader結束時將不呼叫底層Stream.Dispose()
using (var sr = new StreamReader(inp,
            Encoding.UTF8, true, 1024, true))
        {
            var postBody = sr.ReadToEnd();
//讀取完畢要將讀取位置還原到起始點
            inp.Seek(0, SeekOrigin.Begin);
//將POST內容寫入Log檔,此處以Debug.WriteLine示意
            Debug.WriteLine("SrcIp: " + req.UserHostAddress);
            Debug.WriteLine("Content: " + postBody);
        }
    }
}

嗯,看起來順眼多了,收工。

2015石碇馬

$
0
0

去年被37度高溫烘烤好幾個小時的記憶猶新,光是回想皮膚還有灼熱感,但究竟為了什麼,今年還是硬要報名再烤一次呢?其中奧妙,只有被烤過的才知道…

報名時心想,六月底跑,怎麼也比七、八月好些,孰料今年六月特別熱,35度以上天數超過十六天,每天都在刷新記錄,而比賽日(6/28)也沒漏氣,氣象預報多雲時晴高溫36度。

本屆路線大改,出發點由石碇國小改到華梵大學,起跑點接近去年高點,先下坡再爬回來,總爬升高度更是讓你一次爬到爽。

先看去年高度圖:

再看今年的高度圖:

兩次250米下上照舊,本來一趟的400米的陡坡下上改成20公里內兩次連發,加量不加價,跑友們賺很大,真是超棒der~

沒辦法,比起跑在大馬路吸廢氣、跑在河濱聞淤泥,我更愛馳騁山野的新鮮空氣,爬點坡算什麼?這一年我愛上「小而美」類型的賽事,報名不擔心秒殺,賽道沒有人擠人,不必擔心補給站被洗刧一空,人少容易看到熟面孔,水站少了大軍壓境的兵荒馬亂,總能聽到志工、跑友間的哈啦,重溫路跑熱潮前那種馬場專屬的人情味。

夏至剛過,早上五點半天色亮如白晝。主會場設在華梵大學運動場。由於沒有預發寄物卡,需由工作人員分別貼號碼在包包跟號碼布上,但寄物區設在體育館,出入通道狹窄,促成近百公尺的排隊人龍,難以多工(變成單一執行緒來著)。工作人員想到一個好辦法,沿著隊伍發貼紙給跑友自行貼在包包跟號碼布上,在寄物處直接丟包就走,大幅改善消化速度,順利化解危機。

選舉將至,上台的民代、長官較多,遲了幾分鐘,6:05開跑。

這一幕很有趣,起跑後的領先隊伍跑錯,衝上叉路左側的上坡,發現此路不通趕緊回頭,幸好只多跑了一兩百公尺,不像英國北方馬拉松的一人跑對,4999人跑錯沒成績的大鳥龍。但大軍回頭的場面難得一見,把大家都逗樂了。

起跑沒多久就來個長上坡,這場想爬坡有的是,不用急不用趕,慢慢走就好,別讓大腿小腿不開心。

鋼鐵人出現!長袖長褲頭盔裝備齊全,最讓人佩服的是只露出眼睛的黑色頭套,光用看的我都噴汗了,不愧是超級英雄。

由這張圖看得出上坡有多陡,正常人都知道要用走的,所以警察伺機在旁監看,發現有人用跑的上山,若非通緝在案心虛就是精神狀況異常,一律帶到旁邊盤查身分… (以上是我瞎扯的,感謝警察大哥協助交管)

蜘蛛人,你朋友鋼鐵人在後面,要跟他會合嗎?

爬坡的代價是可以看到壯闊的山景。

還有啤酒,吼搭啦~ 下酒菜補給品有西瓜(超甜!)、蕃茄、香蕉、芭樂、檸檬、可樂、沙士、蘋果西打、豆漿、粉圓… 其實我也沒記得很清楚。馬拉松跑多了,看待補給的態度也變了,不像菜鳥時代,一看到新奇花俏的種類就「哇」,什麼要嚐一下,把馬場當成Buffet餐廳,如今跑馬只求不渴不餓,足矣。想到之前讀一些馬場老前輩的跑馬心得文,對於補給也都只會輕描淡寫帶過,忽然有種自己變世故的錯覺 XD

依循慣例,大會發了環保杯(去年出發前領取,今年則在第一個水站提供),路上少見紙杯狼藉,清爽許多。但路上目賭一位跑友的水杯墜地破裂,杯底都掉了 XD 希望他在下個水站有領到備品。

另外,補給站冰塊充足,有冷飲消暑,還提供塑膠袋讓跑友冰敷,加上好幾台機車來回巡邏機動補給,啾甘心A。

跑在這段林蔭區,忽見前方跑友受到驚嚇,很快用手掃過上衣,一塊灰黑色物體掉落地面,停了兩秒不明物體飛了起來,跑近發現是隻雞蛋大小的獨角仙,受了驚嚇到加上逆風,胡亂飛了一陣子又掉到路中央,擔心被其他跑友誤踩,我抓起牠放到路旁樹上,被後方跑友撞見,直誇我有愛心,說要幫我按個讚,怪不好意思的。

雖然氣溫不低,但老天爺很給面子,薄雲微陰的時間比出太陽的時間多很多,偶而有風,跑起來痛苦指數不若去年。但跟去年腳傷近月未練跑,今年賽前幾乎每天5K,身體狀況差異也很有關係。

去年的下坡跑過的浪漫八卦茶園。

最後5K,終點華梵大學就在左方遠遠的山頭,今天上坡幾乎全用走的,心率維持在中速檔,氣力有剩。終點在望,趁著緩長上坡試催油門小跑個幾百公尺再停下來用走的,反覆操作,想多拉幾個名次,超車時被其他跑友笑稱「英雄!」「到這裡還有力氣跑的都是英雄!」(小得意)

去年看過的灌模塑像工廠,又見面了。

6:06:53跑完,總排名343,六小時完賽,在全部818名選手還排在前42%,足見戰況之慘烈。(事後看跑友分享,得知大會延長時限到八小時)

到寄物區領物,體育館空調得宜,不冷不熱,歷經正午36度高溫洗禮,這裡彷彿天堂。於是我跟許多跑友一樣,領了寄物就靠在牆角納涼,何似在人間?可惜下午公司有事趕著走,不然應該沖個澡(今年一樣有更衣盥洗區),在體育館吃完便當小睡,更加完美!

頗特別的大會紀念品,運動太陽眼鏡一付。

賽後檢討,這場賽事硬歸硬,遇上36度高溫,卻成績不差,無傷無痛完跑,是近來最身心舒暢、最盡興的一場。賽前跑得勤(每天固定5K)狀況不錯固然是原因,但更重要的關鍵在於「配速得宜」。請不要誤會,我上坡就走,下坡散步,只有偶爾小跑,Pace亂得一塌糊塗,何來配速?我所謂的「配速」指的是「調配補水的速度」,前幾場仗著背水袋隨時可喝,有時進水站嫌人多就跳過不喝,補水不足,跑完體重掉一兩公斤,大半天不用上廁所,明顯缺水。事後仔細精算,水袋只有兩公升,依我的飆汗量,一個小時恐得補水近一公升才夠,六小時扣掉水袋兩公升,還得額外補充四公升。石碇馬約2.5-3K就有一站,每站補足400-500cc才保險,水袋只做輔助。依這個公式,每個水站固定喝兩杯,賽後再補充500,跑完沒多久就想上廁所,證明身體未處於缺水狀態。以此做為日後夏日跑馬之補水原則,欽此!

如何使用遠端桌面讓Windows 8睡眠、關機或重新開機?

$
0
0

小技巧一則。

使用遠端桌面登入Windows 8,會發現平常用來執行睡眠、關機或重開的操作介面,只剩下中斷連線及登出選項:

此時如要讓Windows 8睡眠、關機或重新開機,除了使用Shutdown指令,有一個更簡便的方法是在桌面空白處點擊一下,再按Alt-F4叫出完整開關機選單,就搞定囉~

註:用遠端桌面執行睡眠或關機,之後想再連線也連不上,猜想Windows可能就是擔心誤選會造成困擾才拿掉選項,故操作前請確定知道自己在做什麼。 XD
我是用在VPN連回公司完工後用這招讓主機睡眠,節約能源兼延年益壽,下次連線前再使用網路喚醒


HttpCookieCollection的foreach陷阱

$
0
0

我想在ASP.NET MVC裡用foreach列舉所有Cookie,HttpRequestBase.Cookies是不二人選。Cookies屬性的型別為HttpCookieCollection,既然是HttpCookieCollection,foreach拿到的應該就是HttpCookie吧?很自以為是地寫好以下程式準備收工:

public ActionResult TestCookies()
{
    var req = this.HttpContext.Request;
    StringBuilder sb = new StringBuilder();
foreach (HttpCookie cookie in req.Cookies)
    {
        sb.AppendFormat("{0}: {1}\n", cookie.Name, cookie.Value);
    }
return Content(sb.ToString());
}

不料,程式噴出以下錯誤:

無法將類型 'System.String' 的物件轉換為類型 'System.Web.HttpCookie'。

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

例外狀況詳細資訊: System.InvalidCastException: 無法將類型 'System.String' 的物件轉換為類型 'System.Web.HttpCookie'。

原始程式錯誤:


行 63:             var req = this.HttpContext.Request;
行 64:             StringBuilder sb = new StringBuilder();
行 65:             foreach (HttpCookie cookie in req.Cookies)
行 66:             {
行 67:                 sb.AppendFormat("{0}: {1}\n", cookie.Name, cookie.Value);

.NET抱怨我霸王硬上弓把string轉型成HttpCookie,硬把繩子當餅乾。

這麼說來,foreach HttpRequestBase.Cookies會取得string而不是HttpCookie?

查了HttpCookieCollection的說明,文件範例寫for (int i = 0; i < cookies.Count; i++),不是用foreach,再追到HttpCookieCollection所繼承的父類別NameObjectCollectionBaseGetEnumerator()方法提到「這個列舉值會將集合的索引鍵當做字串傳回。」

謎底揭曉:用foreach列舉HttpCookieCollection,得到的是索引鍵字串(也就是Cookie名稱)而非HttpCookie物件,所以程式應改為:

public ActionResult TestCookies()
{
    var req = this.HttpContext.Request;
    StringBuilder sb = new StringBuilder();
foreach (string cookieName in req.Cookies)
    {
        sb.AppendFormat("{0}: {1}\n", cookieName, req.Cookies[cookieName]);
    }
return Content(sb.ToString());
}

上了一課,BlahCollection跑foreach傳回的不一定是Blah型別,使用請先詳閱公開說明書。

Web Essentials 2015不再支援SCSS/LESS自動編譯

$
0
0

VS2015RTM了!像是拿到新玩具的小孩,裝好VS2015,迫不及待地開啟現有專案來玩玩。

身為一頭前端攻城獅,裝好VS2015後第一件事就是下載安裝Web Essentials 2015,少了Web Essentials就像沒穿防彈背心上戰場一樣讓人不安。但沒多久,我就遇到網站改用VS2015開發面臨的第一項改變。

在專案編輯SCSS,按下儲存鈕,在網頁卻不見修改生效,嗯,再按大力一點試試(喂!),還是沒效… 檢查檔案時間,發現.css、.min.css等檔案均未更新,確定SCSS語法正確無誤,應非編譯失敗所致。深入追查,在WE2015的套件說明發現一段駭人文字:

Web Essentials 2015 no longer contains features for bundling and minifying of JS, CSS and HTML files as well as compiling LESS, Scss and CoffeeScript files. Those features have been moved to their own separate extensions that improves the features greatly. Download them here:

Bundler & Minifier
Web Compiler

意思是WE2015不再負責LESS/SCSS/CoffeeScript編譯及JS/CSS的打包壓縮(登楞!),不過不用驚慌,工作是被移交給另外兩個獨立套件處理。SCSS的編譯改由Web Compiler負責,但運作方式與WE2013時代有些差異,開發者需要花點時間適應。

安裝Web Compiler後,在.scss檔案按右鍵,選單會多出「Compile file」選項:

SCSS編譯寫入.css的檔名、路徑可自由指定,不若以往強迫要跟.scss放在一起,彈性變大是項改良。若是專案第一次設定要編譯的.scss/.less,Web Compiler會專案根目錄新增compilerconfig.json設定檔,記錄來源路徑、輸出路徑、是否壓縮、要不要建立sourceMap等設定,每個編譯目標一筆設定,組成陣列存成JSON格式:(未來將會看到愈來愈多設定用JSON儲存,XML要慢慢淡出舞台了)


[
  {
"outputFile": "Content/Site.css",
"inputFile": "Content/Site.scss",
"minify": {
"enabled": true
    },
"includeInProject": true,
"sourceMap": false,
"options": {}
  }
]

copyText();

在compilerconfig.json按右鍵可以選取「Re-compile all files」重新編譯全部的LESS/SCSS。

若希望每次建置專案時都重新編譯,可啟用「Enable compile on build」,專案需從NuGet下載安裝BuildWebCompiler套件,並設定每次建置專案時重新編譯SCSS/LESS。

註:啟用自動編譯後檢視.csproj可看到以下設定
<Import Project="..\packages\BuildWebCompiler.1.0.73\build\BuildWebCompiler.targets" Condition="Exists('..\packages\BuildWebCompiler.1.0.73\build\BuildWebCompiler.targets')" />

SCSS在加入Web Compiler編譯清單後,每次儲存檔案時就會自動重新編譯,這個行為跟以前一樣,大家可以安心了。而右鍵選單則提供了「Re-compile file」及「Remove from compilation…」允許強制重新編譯或將其從編譯清單移除:

靠著Web Compiler,就能重現Web Essentials 2013時代的SCSS即時編譯功能,大家可以繼續安心戰鬥囉~

PS:發現VS2015針對自動產生的檔案,會加上斗大的Generated浮水印,想起以前曾有改程式改昏頭想改SCSS誤開CSS的經驗,算是一項貼心設計。

VS2015 Cordova初探

$
0
0

Apache Cordova是一個用HTML、CSS、JavaScript打造行動裝置App的開發平台,點滿網頁開發技能的前端攻城獅也能快速轉職為App開發人員,不必從頭苦學Objectivc-C/Swift/Java也能為手機、平板寫App,實在是一大福音。

期待以久VS2015終於在7/20 RTM,手上的網站專案有轉成手機App的潛在需求,因此我特别關注VS2015內建支援Cordova專案,不必砍掉重練(都到這把年紀,還能砍幾次呢?orz)就能寫好App,功德無量。而號稱地表最強開發工具的Visual Studio,是否仍沿襲一貫傳統,將原本令人頭皮發麻的複雜操作細節,包裝成One-Click搞定?頗令人期待!拿到VS2015,當然要試試Cordova專案。

由於Cordova不在預設安裝選項,安裝時記得選自訂安裝,需額外勾選「HTML/JavaScript(Apache Cordova)」,勾選時VS2015後一併幫你選取Emulator for Android等必要選項:

安裝好VS2015,就可以準備新增Cordova專案。VS2015提供兩種Cordova專案範本,JavaScript及TypeScript,它們被歸整類在Other Language/JavaScript及Other Language/TypeScript下,我花了點時間才找到,另外也可以在下圖右上角「Search Installed Templates」搜尋欄輸入「Cordova」快速找到它們:

即便有JavaScript與TypeScript兩種Cordova專案選項,對我而言,有雞腿誰要吃菜脯蛋?二話不說就選TypeScript囉~

VS2015所新增的空白Cordova專案結構如下,merges用來存放各平台(Android、iOS、Windows Phone...)專屬的程式碼、res為平台專屬的圖示及載入畫面(Splash)圖檔、www則為CSS/圖檔/HTML/Scripts等打造UI要用的網站程式,TypeScript部分則被集中於scripts目錄。底下還有bowser.json、confit.xml、taco.json、package.json等檔案。先不用擔心這堆新東西,現階段還不需要深入了解,一樣能用VS2015寫出Cordova App。

空白Cordova專案有一個非常簡單的index.html,裡面只有一行文字「Hello, your application is ready!」,但具備Cordva基本的JavaScript架構,可以做為衍生各式應用的基礎。我們先不做修改,直接執行它體驗VS2015提供的巧妙整合。

如下圖,在Debug工具列可選擇要執行測試的平台。除了Android模擬器,VS2015也整合了Ripple,值得大力推薦!

Apache Ripple是Apache開發的一組Chrome擴充模組,可模擬Cordova App行動裝置執行環境,提供手機/平板解析度、裝置實體翻轉、搖動、電池電量、網路頻寬、地理位置…等多項控制參數,大幅簡化測試複雜度。

而最棒的一點,Ripple也支援Chrome F12偵錯!(身為網頁開發老鳥,看到這裡忍不住起立鼔掌)

註:在機器上第一次編譯Cordova專案,花費的時間會遠超過你的預期,理由是編譯Cordova專案需要下載安裝一堆node.js模組,但是放心,一如往例,VS2015會在背後打理一切,不勞開發者費心,請放鬆心情耐心等待。

Ripple啟動速度快,又相容Chrome F12開發工具,有利草創時期的開發測試。但使用Chome檢視畢竟與實機有段差距(例如:行動裝置有其專屬的輸入法操作UI),免不了還是需要用模擬器驗證。VS2015內建了Hyper-V Anroid模擬器,VS2015再一次展現它的貼心,不用費心下載、安裝、設定,只需選取Debug平台「VS Emulaotr 7" Kitkat (4.4) XHDPI Table」或「VS Emulator 5" Kitkat (4.4) XXHDPI Phone」,就能自動啟動Android VM、部署程式進行測試,值得一提的是VS2015還提供類似瀏覽器F12開發者工具的DOM檢視器及JavaScript Console,在JavaScript Console下個alert(),會在模擬器彈出對話框,等於在模擬器上也有F12開發者工具可用,甚至可以在TypeScript上設定中斷點,除錯找碴射茶包很方便。

歷經初步小試,VS2015沒讓人失望,Cordova專案開發及測試的整合性維持了一貫的流暢與貼心,初學者也能快速上手,不愧是Visual Studio!

不過,即使VS2015帶來極為順暢的入門體驗,大幅消除初學者的挫折感,但要在真實戰場生存,搞懂Visual Studio背後默默做掉的那一堆骯髒粗活兒,是邁向進階開發的不二法門,這代表又有一拖拉庫的新東西要學了,加油。

【茶包射手日記】VS2015程式檔編碼問題

$
0
0

裝好VS2015後,陸續將VS2013維護的專案改用VS2015開啟,原以為可完全無痛移轉,踩到小刺一根。

某個用VS2013開發多時的專案,移到VS2015出現編譯錯誤!以下面的程式為例:

Console.WriteLine那行發生Unrecoginzed escape sequence錯誤。

看到Unrecongized escape又看了看問題字串,程式老骨頭心中警鈴聲大作:

我被「許功蓋」偷襲了?

使用Notepad++開啟C#原始檔,果不其然,檔案編碼被設成BIG5(ANSI)。

依開發規範,程式檔都該存成UTF8編碼,避免中文難字或非中文的Unicode字元變成亂碼。出問題的.cs檔案有點歷史,漏了修改,但從VS2005時代至今多年,相安無事。由此推論,VS2013面對BIG5編碼原始碼檔案有較高的包容性,VS2015的處理邏輯改變(可能是編譯器改用Roslyn的緣故),遇到許功蓋等包含"\"字元的BIG5中文字,發生解析錯誤。而依文字出現位置的不同,產生的錯誤也可能不同,例如:若將以上程式改成Console.WriteLine("測試成功"),錯誤訊息則會變成:

1>e:\L1\Lab1\Program.cs(13,31,13,31): error CS1010: Newline in constant
1>e:\L1\Lab1\Program.cs(13,43,13,43): error CS1003: Syntax error, ',' expected
1>e:\L1\Lab1\Program.cs(14,27,14,28): error CS1026: ) expected

【結論】VS2015處理ANSI編碼程式檔邏輯與VS2013不同,若專案搬移至VS2015後出現Unrecoginzed escape sequence或字串結尾識別失敗等相關錯誤,請優先檢查是否程式檔被存成ANSI(BIG5)編碼,轉為UTF8即可排除。

改善VS2013/VS2015圖示混淆問題

$
0
0

陸續將VS2103的專案移至VS2015開發,開始一段「有些專案用VS2013開啟,有些用VS2015編輯」的並行時期,遇到一個小困擾:

如上圖所示,由於VS2015沿用VS2013的圖示,同時用來開啟專案,工具列就會出現兩個一模一樣的Visual Studio項目。年紀大記性不佳,老在VS2013項目裡找VS2015專案,發現找錯門牌才想到要找的專案住隔壁,五分鐘後,同樣的錯又再犯一次… orz

想到一個解決辦法,把VS2013的圖示改掉好了!

開啟Visual Studio 2013「內容」選單:

選擇「變更圖示」:

系統提示需要系統管理者權限才能變更:

變更完成後重新登入桌面(或用工作管理員砍掉並重啟explorer.exe),VS2013與VS2015從此涇渭分明,不會再搞錯了。
PS:把VS2013改為帶有向上小箭頭的圖示,有督促自己早日升級之意,我真是用心良苦呀~XD

取得Cordova專案編譯失敗訊息

$
0
0

在家裡電腦體驗過無比順暢的VS2015 Cordova專案經驗,準備在公司展開Cordova大冒險,萬萬沒想到,公司機車特殊的網路環境(之前已被SSL中間人憑證搞過多次),原本簡單的自動下載編譯部署執行,變成可歌可泣的天堂路。喵的,我又想唱金包銀惹…

上回提過,VS2015封裝了複雜繁瑣的npm Cordova模組下載、安裝過程,按下編譯或執行鈕,Visual Studio就會默默搞定一堆雜工,使用者眼不見心不煩,當個快樂的小傻瓜尊貴的上流開發者就好。家庭背景特殊的小孩總是比較早熟,網路環境特殊的開發人員也是,無法茶來伸手飯來張口,動手寫程式前得先陪Visual Studio蹲在地上做雜工。

在公司環境,VS2015安裝過程順利,建立Cordova專案也沒什麼問題,但按下編譯鈕,Output視窗出現以下訊息:

1>------ Build started: Project: BlankCordovaApp1, Configuration: Debug Android ------
1>  Your environment has been set up for using Node.js 0.12.3 (x64) and npm.
1>  ------ Ensuring correct global installation of package from source package directory: C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\Extensions\ApacheCordovaTools\packages\vs-tac
1>  ------ Name from source package.json: vs-tac
1>  ------ Version from source package.json: 1.0.0
1>  ------ Package already installed globally at correct version.
1>  ------ Cordova tools 4.3.1 already installed.
1>  ------ Build Settings:
1>  ------ Build Settings:
1>  ------    platformConfigurationBldDir: x:\temp\CordovaLab\BlankCordovaApp1\bld\Android\Debug
1>  ------    platformConfigurationBinDir: x:\temp\CordovaLab\BlankCordovaApp1\bin\Android\Debug
1>  ------    buildCommand: prepare
1>  ------    platform: Android
1>  ------    cordovaPlatform: android
1>  ------    configuration: Debug
1>  ------    cordovaConfiguration: Debug
1>  ------    projectName: BlankCordovaApp1
1>  ------    projectSourceDir: x:\temp\CordovaLab\BlankCordovaApp1
1>  ------    npmInstallDir: C:\Users\jeffrey\AppData\Roaming\npm
1>  ------    language: en-US
1>  ------ Adding platform: android
1>  No version supplied. Retrieving version from config.xml...
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

呃,只知道Build 1 failed,完全沒有失敗原因。原來是因為Visual Studio預設只顯示最少的編譯訊息,而Cordova專案依賴外部編譯器編譯,編譯過程的詳細訊息預設不會顯示。需調整如下圖,將「MSBuild project build output verbosity」由Minimal改為Normal以上等級:

重新執行一次,便可在Output視窗找到錯誤原因:npm http GET httqs:/registry.npmjs.org/cordova-android/3.7.2時發生"Unable to fetch platform android: Error: certifiate not trusted"。

不用懷疑,又是公司網管設備偷換中間人SSL憑證搞的鬼,之前雖然設過npm cafile,知何故不管用。另外,還發現Cordova編譯過程也會從github下載東西,也可能有HTTPS問題。MSDN文件寫得挺詳細,有提到如何設定Proxy解決npm及git連線問題,依著這個線索及同樣原理,試著暫時停用npm及git的SSL驗證排除問題:

npm set strict-ssl false
git config http.sslVerify "false"

呼,終算編譯成功~

Windows檔案總管圖檔預覽失效

$
0
0

系統最近不太穩,桌面(Explorer.exe)會不定期衝高CPU並造成畫面凍結,不勝其煩決定重新開機。重開用了一陣子,發現圖檔預覽功能壞了-檔案總管右方預覽窗格一律顯示圖檔圖示(即下圖右方的三角錐加方塊加球的圖案),切到超大圖示、大圖示等版面配置也只有圖檔圖示,不會顯示圖檔內容:

經檢查,原來是資料夾的「一律顯示圖示,不顯示縮圖」選項不知為何被啟用(不排除當機造成),關閉後問題排除。


Windows 2012 R2 x64執行ASP經驗分享

$
0
0

Windows 2003於今年7月技術支援終止(12年,夠本了 XD),公司一批千年老妖等級ASP被勒令搬遷到Windows 2012R2 x64主機,過程還算順利,幫忙處理掉幾枚小茶包,記錄如下:

應用程式集區設定

經實測,「.NET CLR Version」設v2.0、v4.0或是No Managed Code,「Managed Pipeline Mode」設Integrated或Classic都不影響ASP執行,但ASP少不了要使用ActiveX物件,而ActiveX是32位元時代的產物,故記得要開啟「Enable 32-Bit Applications」設定,否則很快就會觸礁。

顯示ASP詳細錯誤訊息

ASP出錯時,預設將不會顯示詳細訊息,只會出現以下含糊說明:

這類似ASP.NET的customErrors功能,用意在隱藏詳細訊息,避免敏感資訊對駭客說歡迎光臨。不過,如果想看訊息射茶包,ASP沒有web.config,設定要到哪裡調?

使用IIS管理員檢視ASP應用程式設計,左上角的ASP圖示靜靜躺在那裡多年,終於第一次打開它…

打開Compilation/Debugging Properties,前面瀏覽器看到的那段訊息由「Script Error Message」決定,底下有個「Send Errors To Browser」,預設為False,如要偵錯可暫時打開。(正常營運建議保持False較安全)

開啟後,瀏覽器就能看到詳細錯誤訊息了。(在這個案例中,程式讀取的Registry位置就跑出來了,證明確有風險,顯示錯誤功能務必謹慎使用)

Registry機碼位置

某段讀取Registry的VBScript冒出如上找不到機碼錯誤,同事則確定在HKLM/SOFTWARE已加入所需設定。

<html>
<head><title>Registry Test</title></head>
<body>
<%
Dim Str
Function GetRegistry()
Dim myObj
Set myObj = CreateObject("WScript.Shell")
 GetRegistry = myObj.RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\DARK\Blah")
Set myObj = Nothing
EndFunction
Str = GetRegistry()
%>
Registry=<%= Str %>
</body>
</html>

召來茶包一哥Process Monitor,茶包當場現形:

ASP為32位元程式,原本Windows 2003 x86時代放在HKLM\SOFTWARE的Registry,必須改到HKLM\SOFTWARE\Wow6432Node\下。這是老問題,一時沒意會到,多虧老戰友幫忙破案,謝謝你,9527 Process Monitor。

【茶包射手日記】TFS工作區資料夾更名導致狀態無效

$
0
0

當初為TFS版控建立工作區時沒想太多,取了TFSWorkspace當資料夾名稱。隨著接觸專案變多,跨越多個Project Collection,每個都需要自己的工作區,這才意識到原先的命名大有問題,工作區資料夾應加上Project Collection名稱才合理,之後再設工作區,IM-ERP Projection Collection的工作區資料夾就叫TFS-IM-ERP,以此類推。於是乎,磁碟上有一堆TFS-IM-***資料夾,和一個很突兀的TFSWorkspace,怎麼看都不順眼。

某一天終於忍不住,動手將TFSWorkspace更名,依循Project Collection名稱改為TFS-IM-OA,並同步修改工作區對應路徑,看似完美,但有個後遺症。如下所示,AFA專案是我一手建立的,所有檔案好好地躺在X:\TFS-IM-OA\AFA目錄下,但TFS認定我在本機上沒有這些檔案:

更麻煩的是,使用Visual Studio開啟專案,會因Binding狀態Invalid無法簽入簽出:

實驗並請教同事,得到結論:此時最佳解法是用Get Latest當成新電腦新工作區重新取回檔案,原則上本機檔案與TFS的版本應一致,不會出現衝突,等TFS下載比對完全部檔案,問題即可排除。

【茶包射手日記】Word文件出現SCDSA002亂碼

$
0
0

接獲報案,某封來自韓國e-mail的附件Word檔,開啟時出現亂碼警示:

直接檢視檔案內容,資料以SCDSA002起首,看似無規則亂碼,與我所知的檔案格式都不吻合:

上網用"SCDSA002"爬文,找到一些討論文章,有兩大特色:1) 同樣的SCDSA002 Pattern曾出現在Word、PPT、Excel甚至PDF檔案,清一色都是文件類 2) 討論文章有極高的比例是韓文。而我從搜尋結果找到另一個關鍵字「DRM」,使用Google翻譯讀了幾篇文章(其中有一篇還是韓國微軟MVP寫的  :-P),獲得結論-SCDSA002起首檔案是被DRM軟體加密的結果,必須解密才能閱讀。告知苦主後,本案在對方重送解密版本後將問題排除。

不過,留下一個謎:為什麼所有SCDSA002的相關討論都是韓文?

依我推測,最有可能是因為它是韓國當地流行某個DRM軟體所定義的註記,故極少在其他國家形成困擾。

最後,一篇韓國Microsoft Answer論壇文章證實了我的假設,以下是用Google翻譯後的部分內容:

"SCDSA ***" jindamyeon will be expected to demonstrate that encrypts the encryption capabilities of Office, but by the SoftCamp's document security DRM. SoftCamp's encryption feature, you should see if the assistance by contacting the manufacturer, not Microsoft.

以此推論,文件極可能是用韓國公司SoftCamp的DRM產品加密。謎團解開,Case Closed!

await與Task.Result/Task.Wait()的Deadlock問題

$
0
0

async/await是.NET 4.5+加入的新玩意兒。.NET 4推出的Task簡化了非同步程序的撰寫,async/await則讓程式碼簡潔度更上一層樓。如果大家對Thread、Task、aysnc、await還不熟悉,我找到兩篇還算淺顯易讀的對岸文章-async & await 的前世今生异步编程 In .NET,文章完整涵蓋C#在多執行緒程式撰寫上的演進,從.NET 1.1到.NET 4.5,能做到的事跟背後運用機制依舊,寫法卻愈來愈簡潔,身為.NET開發人員是件幸福的事。(老鳥每每想到這些都得壓抑一下情緒,不然會變成把「唉,你們很好命囉!當年我們哪有白飯可吃,都嘛吃蕃薯籤…」掛嘴邊的碎唸老人)

這兩年我的主戰場都在前端,對async、await這些新東西仍一知半解,最近就踩了一個地雷! 在一段MVC程式搞出如下寫法:

using System.Threading.Tasks;
using System.Web.Mvc;
 
namespace MVC.Controllers
{
publicclass HomeController : Controller
    {
public ActionResult Index()
        {
            var res = GetRemoteData().Result;
return Content("Result=" + res);
        }
 
        async Task<int> GetRemoteData()
        {
int res = 0;
            await Task.Run(() =>
            {
//假裝執行某個耗時程序後取得結果
                Task.Delay(1000);
                res = 32767;
            });
return res;
        }
    }
}

猜猜會發生什麼事?程式會卡死,瀏覽器永遠等不到網頁回傳Result=32767。

想簡化茶包重現程序,將同樣程式搬到Console Application執行,結果卻完全不同,Result=32767如預期顯現,毫無障礙。

看到這裡,應該有不少人跟我一樣丈二金剛摸不著腦袋(知道發生什麼事的同學請到講台領獎品,可以下課去操場玩囉),陸續讀了一些文章,才搞懂怎麼一回事。

await關鍵字必須搭配Awaitable物件使用,Task/Task<TResult>則是最常用的.NET內建Awaitable(如果有需要,你也可以自訂Awaitable類別)。當使用await關鍵字,Awaitable會自動偵測目前所處的SynchronizationContext並記錄下來,確保非同步作業完成後繼續用SynchronizationContext指定的Thread執行後續程式,這點對Window Form等受UI Thread限制的情境非常重要。

Awaitable如何決定SynchronizationContext?在Async and Await一文找到簡要說明:

口語版:

  1. 如果在UI Thread執行,就用當下的UI Context。
  2. 如果在ASP.NET Request裡執行,就使用ASP.NET Request Context。
  3. 若非以上情境,則使用Thread Pool Context.。

術語版:

  1. 當SynchronizationContext.Current不為null,就採用Current所指的Context。
    (在UI及ASP.NET環境,SynchronizationContext.Current會分別指向UI Context及ASP.NET Request Context)
  2. 若SynchronizationContext.Current為null,就使用TaskScheduler.Default。
    (即Thread Pool Context)

以上差異即為「程式在Console Application執行OK,移到MVC就壞掉」結果的關鍵,我們分別在Console Application與MVC中檢測SychronizationContext,可以證實其在Console Application中Current為null:

在MVC中Current為AspNetSynchronizationContext:

那麼,為何遇到AspNetSynchronizationContext會讓程式卡死?在另一篇文章Don't Block on Async Code,我找到解答並試著依樣畫葫蘆,描繪問題爆發的過程:

  1. HomeController.Index()呼叫async方法GetRemoteData() (處於ASP.NET Context)
  2. GetRemoteData()用Task.Run()執行模擬的遠端呼叫,立即傳回還沒執行完成的Task (仍在ASP.NET Context)
  3. GetRemoteData() 使用await等待Task.Run裡面的程序跑完(被放了Task.Delay(1000),要跑一秒),先抓取當下的ASP.NET Context(確保Task.Run跑完的後續動作繼續用ASP.NET Context執行),await指令列以下的程式被暫緩執行,GetRemoteData()先回傳還沒跑完的Task給呼叫端。
  4. 呼叫端Index()使用.Result要求同步化取回結果,此舉將Block(阻擋)ASP.NET Context Thread。(用.Wait()也會Block)
  5. Task.Run內部的Task.Delay(1000)結束,res設為32767,Task作業完成。
  6. GetRemoteData()察覺await在等待的Task作業做完了,準備用ASP.NET Context的Thread處理後續作業,將結果傳回呼叫端。
  7. 轟!Deadlock!
    Index() Block住ASP.NET Context Thread靜候GetRemoteData()傳回結果;GetRemoteData()等著用ASP.NET Context Thread處理結果傳回Index() ,偏偏該Thread已被Index() Block住動彈不得,僵持至死。

文章提到兩種解決方案:第一種是為Task.Run(() => …)加上.ConfigureAwait(false),一旦指定為false,GetRemoteData()在await Task完成後將改用ThreadPool繼續執行,不堅持使用原ASP.NET Context Thread,即可避開Deadlock。在我們的情境不需限定Thread,改用ThreadPool是OK的。

static async Task<int> GetRemoteData()
        {
int res = 0;
            await Task.Run(() =>
            {
                Task.Delay(1000);
                res = 32767;
            }).ConfigureAwait(false); //加設ConfigureAwait
return res;
        }

第二種做法則是將Index()改為async,取資料部分由GetRermoteData().Result改為await GetRemote(),避免Block ASP.NET Context執行,改為非同步等待,也可以順利過關。

using System.Threading;
using System.Threading.Tasks;
using System.Web.Mvc;
 
namespace MVC.Controllers
{
publicclass HomeController : Controller
    {
public async Task<ActionResult> Index()
        {
            var res = await GetRemoteData();
return Content("Result=" + res);
        }
 
static async Task<int> GetRemoteData()
        {
int res = 0;
            await Task.Run(() =>
            {
                Task.Delay(1000);
                res = 32767;
            });
return res;
        }
    }
}

以上兩種方法都可避免Deadlock,而文章裡提到二者併用可以達到更好的效能及反應速度,是個好主意。(不鎖定特定Context/Thread,任由系統自動分配,有利效能最佳化)

最後補充,這類Deadlock常見於混用await及Task.Wait()/Task.Result的場合,一般建議使用await取代傳統會Block Thread的Task.Wait()/Task.Result,一方面可獲得更好的效能表現,另一方面也避免混用二者產生Deadlock,改用await的方式如同上述第二種解法所示範,先將使用Wait()/Result的函式宣告為async,再將原本的Task.Wait()改為await Task.Wait(),var res = Task.Result改為var res = await Task.Result即可。關於更多的非同步程式設計指南,推薦一篇MSDN文章-Async-Await - Best Practices in Asynchronous Programming

潛盾機-解決VS2015程式檔BIG5相容問題

$
0
0

改用VS2015後沒多久就發現它處理BIG5(ANSI)編碼程式碼的原則不同於以往(推測與編譯器改用Roslyn有關),導致部分使用BIG5編碼存檔的古老程式檔,會因許功蓋造成編譯錯誤。

PO文隔兩天同事跟我說,他們換VS2015後也射了好一陣子茶包,最後爬文又爬回我的文章。XD 後來聊到可以寫程式把所有BIG5編碼程式檔轉成UTF8一勞永逸,同事說檔案沒幾個,手動另存就搞定了,還不需要養乳牛。

這兩天,收到網友留言詢問VS2015是否會修正這個問題;也有網友提到手上專案有成千上萬個cs,改了一個BIG5,還有千千萬萬個BIG5,只好跟VS2015說Goodbye。

首先,雖然不確定VS2015會不會針對此一Issue進行修正,但依我之見,BIG5編碼已經過時多年,除了在VS2015產生不相容,遇到中文難字及其他語系文字都得額外處理(在Visual Studio.NET 2003時代就有處理過)。因此,不管VS2015會不會修正,將程式碼統一改存UTF8編碼是正確的方向。

問題來了,如網友所說,若專案有上萬支cs,一一手動轉存UTF8的浩大工程確實令人心寒。我最愛打造這種可以省時省力的潛盾機,就寫支批次轉檔程式搞定吧!

這裡我假設專案的.cs檔案分成兩類,一部分已經是UTF8或Unicode編碼,其餘則為BIG5編碼(假設全部都是BIG5,排除摻雜簡體中文、日文等其他ANSI編碼的情況)。因此,可以用Directory.GetFiles()搜尋找出特定目錄(含子目錄)下的指定檔案型別(例如:cs及js),檢查檔案註記略過UTF8或Unicode編碼檔案,找到BIG5檔案後先用BIG5 Encoding讀取,再以UTF8 Encoding寫入就完成轉檔,而原來的檔案則加上.big5.bak副檔名備份保留。


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace B2UBatchConverter
{
class Program
    {
publicclass AnalyzeResult
        {
publicstring Content;
public Encoding Encoding;
        }
 
//REF:http://goo.gl/jAJgIr by Rick Strahl
publicstatic AnalyzeResult AnalyzeFile(string srcFile)
        {
//預設為Big5
            Encoding enc = Encoding.GetEncoding(950);
 
//由前五碼識別出UTF8、Unicode、UTF32等編碼,其餘則視為
byte[] buffer = newbyte[5];
using (FileStream file = new FileStream(srcFile, FileMode.Open))
            {
                file.Read(buffer, 0, 5);
                file.Close();
 
if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf)
                    enc = Encoding.UTF8;
elseif (buffer[0] == 0xfe && buffer[1] == 0xff)
                    enc = Encoding.Unicode;
elseif (buffer[0] == 0 && buffer[1] == 0 && 
                         buffer[2] == 0xfe && buffer[3] == 0xff)
                    enc = Encoding.UTF32;
elseif (buffer[0] == 0x2b && buffer[1] == 0x2f && buffer[2] == 0x76)
                    enc = Encoding.UTF7;
            }
//使用指定的Encoding讀取內容
returnnew AnalyzeResult()
            {
                Content = File.ReadAllText(srcFile, enc),
                Encoding = enc
            };
        }
 
staticvoid Main(string[] args)
        {
//args = new string[] { "D:\\Lab\\L805\\ConApp" };
string path = args[0];
//列舉要搜尋轉碼的副檔名
            var scanFileTypes = "cs,js".Split(',');
//略過不處理的資料夾名稱
            var skipFolders = "bin,obj".Split(',');
foreach (var file in
//列舉所有子目錄下的檔案
                Directory.GetFiles(path, "*.*", SearchOption.AllDirectories))
            {
//取得副檔名
                var ext = Path.GetExtension(file).TrimStart('.').ToLower();
//若非預先指定的副檔名就略過不處理
if (!scanFileTypes.Contains(ext)) continue;
//處於\bin\* \obj\*目錄下的檔案也一律略過
if (skipFolders.Any(o => file.Contains(
                    Path.DirectorySeparatorChar + o + Path.DirectorySeparatorChar))) 
continue;
 
//讀取檔案內容並識別編碼
                var analysis = AnalyzeFile(file);
if (analysis.Encoding.CodePage == 950) //BIG5編號檔案才要處理
                {
                    Console.Write("Process File {0}...", file);
//將原檔更名為*.big5.bak
                    var bakFile = file + ".big5.bak";
if (File.Exists(bakFile)) File.Delete(bakFile);
                    File.Move(file, bakFile);
//重新以UTF8寫入
                    File.WriteAllText(file, analysis.Content, Encoding.UTF8);
                    Console.WriteLine(" done!");
                }
else
                {
                    Console.WriteLine("Skip File {0} / {1}", file, 
                        analysis.Encoding.EncodingName);
                }
            }
        }
    }
}

copyText();

專案編譯成Console Application,執行時提供路徑名稱作為參數,轉換程式就會掃瞄該目錄下所有的.cs及.js,一口氣將BIG5編碼程式碼轉存成UTF8。

執行範例如下,原本BIG5編碼的B5Class.cs另存成B5Class.cs.big5.bak,而B5Class.cs已改為UTF8編碼,就算有成千上萬支程式要轉碼也不怕。

希望這個工具能解決一些朋友的困擾。

【提醒】批次轉換前請務必先備份,以避免轉換出錯造成資料遺失。

Viewing all 2433 articles
Browse latest View live


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