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

【茶包射手日記】Managed ODP.NET在ASP.NET發生ORA-12154錯誤

$
0
0

故事要從前幾天學會讓ODP.NET查詢加速10倍的密技說起,原始問題在於Dapper查詢效能不佳,正想把新發現套用在Dapper上… 登楞!Dapper透過IDbConnection擴充方法提供功能,根本沒機會對OracleCommand或OracleDataReader動手腳啊!(抱頭)

打開Dapper原始碼,想研究有沒有地方傳FetchSize參數(還在裡面看到華麗的Emit特技,嘆為觀止),由於FetchSize非通用ADO.NET屬性,無功而返。

另一條路從環境設定著手,倒有點收獲:OLE DB時代,連線字串可加上FetchSize參數,但ODP.NET已不支援。要指定ODP.NET的FetchSize全域設定,可以使用Registry[參考]:HKLM\Software\Oracle\ODP.NET\ version\FetchSize,只是如此將影響該機器上所有使用ODP.NET的程式,執意調高擔心有副作用。最後,發現Managed ODP.NET可以經由config指定FetchSize[參考],將影響範圍縮小到特定應用程式,是較可行的做法。

開了一個小Console Application專案做測試,用NuGet抓了12.1.021的Oracle官方Managed ODP.NET[參考]:

安裝後,app.config自動補上相關設定:

直接執行,程式抱怨無法解析連絡ID:

ORA-12154: TNS: 無法解析指定的連線ID
ORA-12154: TNS:could not resolve the connect identifier specified.

上回提過 Managed ODP.NET 有一套尋找TNSNAMES.ORA的順序,決定使用規則2,在config手動補上TNS_ADMIN路徑參數,順便加上FetchSize:

<oracle.manageddataaccess.client>
<versionnumber="*">
  <dataSources>
    <dataSourcealias="SampleDataSource"descriptor="(DESCRIPTION…省略…) "/>
  </dataSources>
  <settings>
    <settingname="TNS_ADMIN"value="X:\Oracle\product\12.1.0\client\Network\Admin"></setting>
    <settingname="FetchSize"value="1048576"/>
  </settings>
</version>
</oracle.manageddataaccess.client>

補上TNS_AMDIN及FetchSize,Dapper果真變成光速前進。測試成功,如法修改使用Dapper的專案,由於為ASP.NET網站,原本web.config沒有<oracle.manageddataaccess.client>,我將測試成功的版本搬過去。以為需一併加上<configSections>的OracleInternal.Common.ODPMSetionHandler一起般去,結果加上反而會出現configSection重複定義錯誤,要移掉才OK。看起來web.config本來認得<oracle.manageddataaccess.client>,沒想太多,跑了網頁,卻一直冒出ORA-12154: TNS: 無法解析指定的連線ID錯誤。

反覆試了很久,結論是在web.config中放入的<oracle.manageddataaccess.client>沒發揮任何作用,改用IIS測試蒐集到新事證:當AppPool改用x64,web.config必須補上ODPMSetionHandler configSection,但可以成功解析連線ID,正確查詢DB。

拼湊以上線索,我想到一件事,應該是之前安裝Oracle ODAC32時,一併安裝了Managed ODP.NET, 導致32位元與64位元.NET設定不同。打開C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\machine.config,薑薑薑薑~

一開頭就有configSection設定

結尾則有鎖定4.121.1.0版號的TNS_ADMIN設定

這解釋了為什麼在Visual Studio,web.config不用加configSection也認得<oracle.manageddataaccess.client>(VS是x86環境)。但為什麼加入的TNS_ADMIN沒作用成謎。試著加入<dataSrouce alias>也無作用,感覺web.config的整個<oracle.manageddataaccess.client>設定被忽略。但又為什麼Console Application跑x86測試也會成功?

經過一番比對測試,找到關鍵差異。NuGet安裝Managed ODP.NET時一併加入bindingRedirect設定:

<runtime>
    <assemblyBindingxmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <publisherPolicyapply="no"/>
        <assemblyIdentityname="Oracle.ManagedDataAccess"
publicKeyToken="89b483f429c47342"culture="neutral"/>
        <bindingRedirectoldVersion="4.121.0.0 - 4.65535.65535.65535"
newVersion="4.121.2.0"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>

將這段設定也加進web.config,Managed ODP.NET就順利找到TNSNAMES.ORA了。

回頭追了一陣子,每法對為何加入bindingRedirect將ODAC裝的4.121.1版Managed OPD.NET強轉為4.121.2能解決問題提出合理解釋,決定停損,將此謎團先歸入X檔案…


解決BlueStacks RPC:S-7:AEC-0錯誤

$
0
0

應小閃光要求幫忙註冊LINE帳號,由於LINE限定必須持有平板或手機等行動裝置才能註冊,可以在PC跑各式Android App的BlueStacks顯然是最簡便的選擇。BlueStacks的安裝步驟很簡單,但安裝LINE時卻踢到鐵板:

從伺服器擷取資訊時發生錯誤。[RPC:S-7:AEC-0]

爬文查到不少類似案例,大部分文章提到的解決方式是刪除Google帳號再重新輸入,但我反覆試了三次,無功而返。挖掘更多文章,耗了個把鐘頭才找到解決之道,寫篇筆記供遇到類似問題的朋友參考。

簡單來說,排除問題的關鍵在於停止「Google Play商店」及「Google服務架構」兩項服務並清除資料,做法如下:

1.進入「高級設置」選單

2.開啟「設定」/「應用程式」

3.拖曳上方頁籤列,最右方有個「全部」,在其中可找到「Google Play商店」及「Google服務架構」

4.開啟「Google Play商店」,先按「強制停止」,再按「清除資料」

5.對「Google服務架構」也進行相同動作(強制停止+清除資料)

6.重新啟動BlueStacks後,就能順利安裝App囉~

【茶包射手日記】LINQ Contains查詢引發ORA-01425錯誤

$
0
0

網友報案:使用 ODAC112030 與 Visual Studio 建立 Entity Framework Model,在 C# 使用 LINQ 語法.Where(o => o.ColName.Contains(someVar)),被轉換成以下SQL語法:

WHERE ("Extend1"."FOO" LIKE :p__linq__0 ESCAPE '\')

送到Oracle 9i執行(是的,侏儸紀時代的Oracle 9),冒出ORA-01425錯誤「escape character must be character string of length 1」。

爬文找到一些線索,但活化石Oracle 9.2難尋,特別裝了一台VM驗證。經過一番測試,證實遇到的狀況應屬Oracle 9.2 NVARCHAR2欄位處理LIKE ESCAPE子句的特性(或Bug)。以下是我的測試與分析:

使用以下指令建立一個包含NVARCHAR2欄位(FOO)及VARCHAR2欄位(BAR)的測試資料表:

CREATETABLE BLAH (
    FOO NVARCHAR2(8) NOTNULL,
    BAR VARCHAR2(16) NOTNULL,
CONSTRAINT PK_BLAH PRIMARYKEY (FOO)
)

在VS2010建立ADO.NET Data Entity Model進行以下測試。依序查詢並顯示Oracle DB版本,執行.Where(o => o.BAR.Contains(keywd)),再執行.Where(o => o.FOO.Contains(keywd)),並用ToTraceString()觀察產生的SQL語法。

using (var ent = new Ora9Entities())
            {
                var ver = ent.ExecuteStoreQuery<string>(
"select banner from v$version").First();
                Console.WriteLine("Version={0}", ver);
                var keywd = "A";
 
                var query = ent.BLAHs.Where(
                    o => o.BAR.Contains(keywd));
                var sql = ((ObjectQuery)query).ToTraceString();
                Console.WriteLine("SQL={0}", sql);
 
                var count = query.Count();
                Console.WriteLine("Count={0}", count);
 
 
                query = ent.BLAHs.Where(
                    o => o.FOO.Contains(keywd));
                sql = ((ObjectQuery)query).ToTraceString();
                Console.WriteLine("SQL={0}", sql);
 
try
                {
                    count = query.Count();
                    Console.WriteLine("Count={0}", count);
                }
catch (Exception ex)
                {
                    Console.WriteLine("Error:{0}", 
                        ex.InnerException.Message);
                }
 
            }
            Console.ReadLine();
        }

連接Oracle 9.2.0.8 Server,執行結果如下:

FOO與BAR的查詢都產生LIKE :p__linq__0 ESCAPE '\'語法,但BAR的查詢過關,FOO的查詢噴出ORA-01425錯誤。FOO與BAR差在一個是NVARCHAR2、一個是VARCHAR2。以此原理,直接用SQLPlus測試也能得到同樣結果,FOO LIKE時ESCAPE '\'需改成ESCAPE N'\'才能正常執行(或寫成ESCAPE '\\'也可以):

同樣的測試搬到Oracle 10g Server執行,FOO LIKE查詢不管寫ESCAPE N'\'或ESCAPE '\'都不會出錯。更進一步,我們可以簡化查詢語法,不需任何資料表就能重現同樣問題,突顯關鍵為LIKE比對的對象,型別是否NVARCHAR2,而出錯的狀況只限於Oracle 9.2:

SQL> select 1 from dual where cast ('1' as varchar2(1)) like 'A%' escape '\';
no rows selected

SQL> select 1 from dual where cast ('1' as nvarchar2(1)) like 'A%' escape '\';
select 1 from dual where cast ('1' as nvarchar2(1)) like 'A%' escape '\'
                                                                     *
ERROR at line 1:
ORA-01425: escape character must be character string of length 1

SQL> select 1 from dual where cast ('1' as nvarchar2(1)) like 'A%' escape N'\';
no rows selected

由以上觀察,結論為「Oracle Server 9.2要求NVARCHAR2欄位LIKE時要寫ESCAPE N'\'或ESCAPE '\\'才不致出錯,而Oracle Server 10以上已無此限制」。顯然,ODAC112030的Entity Framework在產生SQL語法時未將Oracle 9i納入考量,造成此一現象。但依Oracle文件,Oracle 11.2 Client連9.2 Server的組合已不非官方支援範圍,Oracle 12 Client更是完全連不上9.2 Server,說OPD.NET 11.2不支援Oracle 9.2並不為過。

網路上找到一些解法,對.Contains(keywd)做一些手腳即可避開這個問題,例如:改寫成.Contains(keywd.Trim())會轉成

WHERE (( NVL(INSTR("Extent1"."FOO", LTRIM(RTRIM(:p__linq__0))), 0) ) > 0)

雖然避開問題,但LIKE變成了對每一筆資料欄位行INSTR,效能可能變差,應用時要當心。另一種做法,是改用Stored Procedure執行所需比對,或改用ExecuteStoredQuery也可納入考量。(再三提醒:如要自行撰寫SQL查詢,LIKE對象請以參數傳入,不要直接組字串,否則一旦產生SQL Injection漏洞,甚至會有家破人亡的風險。)

再談jQuery傳送物件JSON給ASP.NET MVC

$
0
0

使用jQuery傳送物件JSON到ASP.NET MVC的做法之前介紹過,但最近我在專案又遇到新難題。

例如有一個參數物件,ArgObject,內含Name屬性及SubArg屬性,SubArg有其專屬型別SubArgObject,基於特殊需要,SubArgObject使用[JsonProperty]及[JsonIgnore]自訂JSON轉換邏輯(實際專案用的是[JsonConverter(...)],此處簡化為[JsonProperty],指定PropB在JSON中需更名為PropX):

publicclass ArgObject
    {
publicstring Name { get; set; }
public SubArgObject SubArg { get; set; }
    }
 
publicclass SubArgObject
    {
publicstring PropA { get; set; }
        [JsonProperty("PropX")]
publicstring PropB { get; set; }
        [JsonIgnore]
publicstring PropC { get; set; }
    }

如果是Web API Controller,什麼都不用做就能順利接收參數。依Web API的Binding規則,參數如為複雜型別(Complex Type),將依Content-Type選擇適當的Media Type Formatter進行轉換[參考]。在本例中application/json會使用JsonMediaTypeFormatter,其核心為Json.NET,且已知轉換目標型別為ArgObject,故可精準轉換[JsonProperty("PropX")]無誤。

publicclass WebApiController : ApiController
    {
public ArgObject PostJson(ArgObject args)
        {
return args;
        }
    }

同樣參數與POST內容,套用在MVC Action結果截然不同:(補充:傳回結果採用Json.NET版JsonResult,使用Json.NET序列化,避用預設的JavaScriptSerializer)

publicclass MvcController : Controller
    {
public ActionResult PostJson(ArgObject args)
        {
returnnew Newtonsoft.JsonResult.JsonResult()
            {
                Data = args
            };
        }
    }

JSON中的PropX值沒有正確對應到PropB:

這個差異源自ApiController及Controller處理Model Binding的機制不同。Controller使用Value Provider概念解析Query Sting參數、HTML Form欄位、XML或JSON,再對應成輸入參數,而JSON由JsonValueProviderFactory負責解析,其內部使用的是JavaScriptSerializer。網路上有將JsonValueProviderFactory換成Json.NET版JsonDotNetValueProviderFactory的做法,但實測無法克服問題。因為MVC預設的Model Binding機制,會先用JsonValuProviderFactory將JSON內容轉成包含三組值的Dictionary:

Name: "Jeffrey",
SubArg.PropA: "AAA",
SubArg.PropX: "XXX"

再從Dictionary取值映對到新建的ArgObject及SubArgObject物件,導致[JsonProperty("PropX")]無從發揮作用。

評估之下,還是得回歸自訂Model Binder。執行IModelBinder.BindModel()時,因轉換對象型別已知,可善用Json.NET的.DeserializeObject(jsonString, bindingContext.ModelType),指定目標型別讓[JsonProperty]、[JsonConverter(…)]等設定發揮作用。

仿效System.Web.Http.FromBodyAtrribute,我做了一個Afet.Mvc.FromBodyAtrribute,將整個JSON內容轉成單一型別。

另外,藉此機會也順便解決實務上另一項常見困擾。為了傳送JSON到SomeAction(Type1 arg1, Type2 arg2),通常要另外宣告一個暫用彙總型別:

class AggTypes {
    public Type1 arg1 { get; set; }
    public Type2 arg2 { get; set; }
}

接收端再改成SomeAction([Afet.Mvc.FromBody]AggTypes args)。既有自訂Model Binder,若允許由JSON中只取出arg1及arg2,分別轉成Type1及Type2,就可以省去多宣告無意義彙總型別的困擾:

ActionResult SomeAction([Afet.Mvc.FromPartialBody]Type1 arg1, [Afet.Mvc.FromPartialBody]Type2 arg2)

FromBodyAttribute及FromPartialBodyAttribute的程式碼如下:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http.Controllers;
using System.Web.Http.Validation;
using System.Web.Mvc;
 
namespace Afet.Mvc
{
    [AttributeUsage(AttributeTargets.Parameter)]
publicclass FromBodyAttribute : CustomModelBinderAttribute
    {
protectedbool DictionaryMode = false;
 
public FromBodyAttribute(bool dictionaryMode = false)
        {
this.DictionaryMode = dictionaryMode;
        }
 
publicoverride IModelBinder GetBinder()
        {
returnnew JsonNetBinder(DictionaryMode);
        }
 
publicclass JsonNetBinder : IModelBinder
        {
 
privatebool DictionaryMode;
public JsonNetBinder(bool dictionaryMode)
            {
this.DictionaryMode = dictionaryMode;
            }
 
conststring VIEW_DATA_STORE_KEY = "___BodyJsonString";
 
publicobject BindModel(ControllerContext controllerContext,
                ModelBindingContext bindingContext)
            {
//Verify content-type
if (!controllerContext.HttpContext.Request.
                    ContentType.ToLower().Contains("json"))
returnnull;
                var stringified = controllerContext.Controller
                                .ViewData[VIEW_DATA_STORE_KEY] asstring;
if (stringified == null)
                {
                    var stream = controllerContext.HttpContext.Request.InputStream;
using (var sr = new StreamReader(stream))
                    {
                        stream.Position = 0;
                        stringified = sr.ReadToEnd();
                        controllerContext.Controller
                            .ViewData.Add(VIEW_DATA_STORE_KEY, stringified);
                    }
                }
if (string.IsNullOrEmpty(stringified)) returnnull;
 
try
                {
if (DictionaryMode)
                    {
//Find the property form JObject converted from body
                        var dict = JsonConvert.DeserializeObject<JObject>(stringified);
if (dict.Property(bindingContext.ModelName) == null)
returnnull;
//Convert the property to ModelType
return dict.Property(bindingContext.ModelName).Value
                            .ToObject(bindingContext.ModelType);
                    }
else
                    {
//Convert the whole body to ModelType
return JsonConvert.DeserializeObject(stringified,
                            bindingContext.ModelType);
                    }
                }
catch
                {
 
                }
returnnull;
            }
        }
    }
 
    [AttributeUsage(AttributeTargets.Parameter)]
publicsealedclass FromPartialBodyAttribute : FromBodyAttribute
    {
public FromPartialBodyAttribute()
            : base(true)
        {
        }
    }
}

使用方法如下:

public ActionResult PostJson2(
            [Afet.Mvc.FromBody]ArgObject args)
        {
returnnew Newtonsoft.JsonResult.JsonResult()
            {
                Data = args
            };
        }
 
public ActionResult PostJson3(
            [Afet.Mvc.FromPartialBody]string Name,
            [Afet.Mvc.FromPartialBody]SubArgObject SubArg
            )
        {
returnnew Newtonsoft.JsonResult.JsonResult()
            {
                Data = new
                {
                    ParamName = Name,
                    ParamSubArg = SubArg
                }
            };
        }

有了這兩項工具,使用Json.NET解析MVC輸入參數就方便多了。

【茶包射手日記】KO 3.3升級不相容之偵錯練習

$
0
0

專案由Knockout.js  3.2升級到3.3後,某個網頁原本使用<!-- ko if … -->切換元素顯示的功能失效,抓了一陣子才發現原來是寫法有誤,原本該寫<!-- ko if: someFalg -->卻寫成<!-- ko if (someFlag) -->,修改為<!-- ko if: someFlag -->問題即告排除。

但這激起我的好奇心,想找出3.2到3.3造成此一行為差異的根源改變。(其實是手癢想練習JavaScript偵錯技巧)

用一段小程式重現問題:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>if/ifnot Test</title>
</head>
<body>
<inputtype="checkbox"data-bind="checked: Flag"/>
  Flag = <spandata-bind="text: Flag"></span>
<div>
<!-- ko if (Flag) -->
  Flag On
<!-- /ko -->
<!-- ko ifnot (Flag) -->
  Flag Off
<!-- /ko -->
</div>
<scriptsrc="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script>
  ko.applyBindings({ Flag: ko.observable(true) });
</script>
</body>
</html>

測試結果,使用KO 3.2可以順利切換Flag On及Flag Off,使用KO 3.3則if/ifnot失效,Flag On及Flag Off永遠出現。

開啟F12偵錯,設定中斷點,一步一步挖進去,終於找出差異禍首:

KO 3.3調整了內部函式parseObjectLiteral()的邏輯,這個函式負責分析縏結字串轉為Key/Value配對,以找出對應的繫結處理器,例如:"text: Prop"會轉成Key="text",Value="Prop",代表要使用text繫結,繫結對象是Prop。

設定程式中斷點,直接呼叫parseObjectLiteral()進行測試。程式誤寫的非典型繫結字串"if(Flag)"(正確應為"if:Flag"),使用KO 3.2的parseObjectLiteral()還是可以正確解析出Key="if", Value="Flag"。

到了KO 3.3,因parseObjectLiteral()內部有個if條件修改,"if(Flag)"已不再視為有效,會被歸為unknown。至於if:Flag在KO 3.2跟3.3都可正確解析。

成功鎖定目標,偵錯練習完畢。

【茶包射手日記】Nexus 7鏡頭維修記

$
0
0

Nexus 7二代平板入手一年多,幾個月前,後鏡頭開始無法對焦,自動對焦時焦點不斷前移後移,就是抓不到清晰點,初步研判是對焦元件或程式模組故障,算算時間應該過保固沒多久,打電話給客服,確認Nexus 7只有一年保固而且我的平板已過保固無誤 orz。雖然對焦模糊但勉強還是能掃QRCode,心想它的拍照效果不如Nokia 920,派上用場機會本來就少,另一方面考量3C產品折舊速度驚人,就不花時間花錢處理,將就著用就是了。

之後有一天,無意間發現鏡頭怎麼被一環狀墊片遮去大半?原來這才是對不到焦的真正原因,摸了摸一體成型的外殼,不知要用什麼特殊工具才能拆開,雖知問題卻無力維修,也只能作罷。

今天想測試某個App的桌面版登入需掃瞄QRCode。開啟照相模式,鏡頭一片潻黑,大事不妙,墊片位移已把整個鏡頭都遮住… 不能拍照事小,喪失掃QRCode功能問題就大條了。

爬文想找坊間維修店家評估修復成本,這才知道,原來Nexus 7的背蓋用指甲就可以打開!

先用指甲撬出空隙,順著邊撐出縫來,再小心拉開。(提醒:USB插口處有類似卡榫的設計,故剝開時要由鏡頭那一端開始,裝回去要先對準USB接口。這是我用殼裂角缺換來的血淚小訣竅,跟大家分享 orz)

金蟬脫殼圖:

找到禍首!原來是個絨紙或不織布材質的自黏圓環,不知為何移位遮到鏡頭,一開始影響對焦,之後變成「國防布」-完全沒有畫面。

小心取下圓環重貼,Nexus 7的照相功能大復活,本以為會殘眼終老一生的平板,就這麼就修好了,真是意外~

使用Fiddler偵察Mac Safari HTTP傳輸

$
0
0

從還是Eric Lawrence業餘作品的時代(當時他在IE Team工作),我就知道Fiddler這件網頁傳輸偵錯好工具,之後它的功能愈來愈強大,可以解SSL加密、安裝外掛、過濾及修改封包內容,在2012年Fiddler被Telerik收購成為正式產品(但Telerik承諾Fiddler會永遠提供免費下載使用),Eric也加入Terelik專心開發Fiddler。只是近年來,傳輸側錄已是各家瀏覽器F12網頁偵錯工具的必備內建功能,滿足大部分網頁傳輸偵察需求,動用Fiddler的機會少了許多。

前幾天遇到一個反覆載入網頁造成iOS Safari瀏覽器Crash的詭異茶包(錯誤訊息顯示為JavaScript指令造成EXC_BAD_ACCESS/KERNL_INVALID_ADDRESS。依我理解,網頁的JavaScript執行環境受限,不太有機會讓瀏覽器崩潰,但Safari顯然例外,而這已是我第二次遇到),iPhone/iPad上可用的除錯工具有限(用USB線連上Mac開Safari開發者工具遠端偵錯是我知道的唯一作法),操作及調查都很不方便。直到成功用Mac Safari重現問題,偵察進度才步入正軌。但令人困擾的是,出錯時Safari以Crash收場,開發者工具也跟著關閉,什麼記錄都沒留下。但問題出現在網頁載入階段,我想到若能掌握網頁載入特定JS時出錯的證據,就有機會抓出真兇。既然開發者工具派不上用場,側錄網路封包是第二選擇,第一直覺想到的是Wireshark Mac版,但很快地,我想起老朋友-Fiddler。

Fiddler採用Proxy方式攔載封包,只要設定妥當,偵察對象不限瀏覽器也不限同一台主機,任何經由Proxy存取HTTP的傳輸都可以被記錄下來。但Fiddler安裝在Windows上,想隔空抓藥的第一步需確認Fiddler的遠端連線選項已開啟:

回到Mac端,使用 telnet fiddler_所在IP 8888測試連線OK(若不行,請檢查是否被Windows防火牆擋掉),下一步是指定Safari使用代理伺服器(Proxy)進行連線:

輸入IP與Port:

設定完成,開啟Safari瀏覽網頁,Safari的HTTP傳輸內容就在Fiddler的掌握之中。

成功設定Fiddler後刻意重現當機情境,發現Safari發生Crash的時機不固定,有時甚至第一個JS還沒下載完就崩潰中斷;而開瀏覽器第一次執行則永遠成功,重新載入才會出錯,看來可能與Cache有關,狀況比想像複雜許多 orz 不過,這又是另一段故事了…

Protractor Windows安裝補充

$
0
0

打算用Protractor做網頁End-to-End測試。Protractor是Angular的主流測試工具,End-to-End測試時會開啟瀏覽器連上網站模擬各種操作,以使用者角度實際驗證功能是否正常。

第一步要在開發機器Windows 8安裝node.js跟Protractor,主要參考Paul Li圖文並茂的介紹文-Protractor - 環境設定篇,安裝大致順利,但過程遇到一些小亂流,特筆記之。

第一個問題來自公司的特殊網路環境,基於資安監管要求,網管設備攔截HTTPS傳輸從中抽換SSL憑證,換上的中間人憑證被npm打搶,得到CERT_UNTRUSTED certificate not trusted錯誤。

解決方法有二,第一種是使用指令npm config set strict-ssl false關閉憑證安全檢查,但此舉對資安有小潔癖的我如同脫光衣服上街,嗯湯呀嗯湯。故我採用比較迀迴但安全的第二種做法,讓npm認可公司網管設備的SSL憑證。方法是匯出網路設備使用的CA根憑證:(以下為示意圖)

格式請Base64編碼的CER:

接著使用npm config set cafile=c:\folder_name\ca_cert_file.cer信任網路設備憑證,終於能如願下載模組。

此時,我遇到第二個問題:

安裝相關模組node-gyp(可跨平台編譯二進位程式庫的工具)時,冒出Can't find Python executable "python", you can set the PYTHON env variable錯誤。依官方文件,node-gyp需要Python 2.7(Python最新版為3.4,但node-gyp適用舊版)及Visual Studio C++ 2010,心想我的Visual Studio 2013有裝C++環境,應該也能Build吧!結果:

冒出錯誤error MSB8020: The build tools for Visual Studio 2010 (Platform Toolset = 'v100') cannot be found. To build using the v100 build tools, pleaswe install Visual Studio 2010 build tools.  Alternatively, you may upgrade to the current Visual Studio tools by selecting the Project menu or right–click the solution, and then selecting "Upgrade Solution…". [C:\Users\jeffrey\AppData\Roaming\npm\node-modules\protrator\node_modules\selenium-webdriver\node_modules\ws\node_modules\bufferutil\build\bufferutil.vcxproj]

修改bufferutil.vcxproj調成改用VS2013編譯是最直覺最快的解決方案,但發現bufferutil.vcxproj為動態下載取得產生,安裝失敗就消失,無從改起。很不甘心為此要多安裝VS2010,所幸npm夠貼心,有個msvs_version參數可以克服問題。使用指令:npm config set msvs_version 2013,終於順利安裝好Protractor。


使用TypeScript撰寫Protractor測試

$
0
0

裝好Protractor,就可以動手寫測試了。Protractor預設使用Jasmine,之前寫Angular單元測試時玩過,語法並不陌生,但是用慣神兵利器,遇上超過一百行的JavaScript,少了Visual Studio + TypeScript拎杯根本活不下去,啊啊啊啊~(顯示為鎚子被沒收的雷神索爾在地上打滾)

因此,首要任務得找出使用TypeScript撰寫Protractor測試的方法。我選擇用VS2013開啟一個空白Web Apllication專案,不啟用MVC、Web API也不啟用WebForm,圖的只是網站專案的TypeScript編譯支援及對專案環境的熟悉度,操作起來比較順手。

偷懶就用Protractor的官方教學當範例,github有現成範例網站可跑測試,方便又省事。(該範例很精簡易懂又可馬上動手實測,是極佳的Protrator入門教材,值得一讀)

第一步要安裝相關定義檔。現在用Manage NuGet Packaged GUI介面查尋套件的速度愈來愈慢,建議改用Package Manager Console較有效率。Package Manager Console支援自動完成,例如:輸入install-package angularjs.type按Tab即可自動帶出angularjs.TypeScript.DefinitelyTyped:

使用Install-Package指令安裝angular-protractor.TypeScript.DefintelyTyped(它會一併安裝selenium-webdriver.TypeScript.DefinitelyTyped)、node.TypeScript.DefintelyTyped、jasmine.TypeScript.DefintelyTyped,共需要四組定義檔:

node.js定義檔有個小問題,除了最新版的node.d.ts(12版)之外,一併附了0.8.8、0.10、0.11等舊版,在專案並存會導致編譯錯誤,需將舊版.d.ts檔案刪除。

在專案新増一個lab1.spec.ts小試一下,在測試腳本可以寫Lambda語法( () => { … } ),Visual Studio也認得it、element、by.id 等函式,不用擔心手滑打錯字。

嘖嘖嘖… 回到阿斯嘉王國的感覺真好~

Protractor、TypeScript與Chai

$
0
0

搞定用TypeScript寫Protractor測試,陸續參考一些範例,發現蠻多人偏好使用Chai程式庫。原本Protractor預設的寫法expect(foo).toEqual(5),改用Chai之後變成:

  • expect(foo).to.be.a('string');
  • expect(foo).to.equal('bar');
  • expect(foo).to.have.length(3);
  • expect(tea).to.have.property('flavors') .with.length(3);

寫程式像在講話,幾乎就等同BDD Feature裡Then的口語描述,我猜想這是Chai受歡迎的原因。例如以下的BDD Feature範例:

Scenario:Add two numbers
    Given I have entered 50 into the calculator
    Then I have entered 70 into the calculator
    When I press add
    Then the result should be 120 on the screen

言歸正傳,要在Protractor改用Chai語法,可在conf.js的onPrepare()用require動態載入chai及chai-as-promised模組,將global.expect換掉(細節可參考Paul Li的介紹):

exports.config = {
    seleniumAddress: 'http://localhost:4444/wd/hub',
    specs: ['lab1.spec.js'],
    onPrepare: () => {
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
        chai.use(chaiAsPromised);
        global["expect"] = chai.expect;
    }
};

回到TypeScript,要讓Visual Studio認得Chai的to.equal()這種語法,就必須載入cha.d.ts及cha-as-promised.d.ts兩個定義檔。老樣子,用Manage NuGet Packages或Package Manager Console安裝chai-as-promised.TypeScript.DefinitelyTyped,它會一併安裝chai.TypeScript.DefinitelyTyped,定義檔就備齊了。

由於chai及chai-as-promised為node.js的外部模組(External Module),應用時跟平常網站TypeScript使用<script src="…">載入的概念有點不同,想深入了解的同學可以參考TypeScript官方文件,這裡只會提到滿足測試撰寫的必要做法。

node.js透過require("…")方式動態載入元件,對應到TypeScript要寫成import chai=require("chai"),由於chai.d.ts只有定義檔不包含實作,故這個import指令目的在匯入chai.d.ts的型別宣告,作用類似宣告/// <reference path="…" />,不會產生實際的JavaScript指令(測試腳本實際上也不需要載入chai,真正的載入及設定動作是在conf.js onPrepare()完成)。

要使用import,專案的TypeScript編譯選項要做些調整,Module System需選擇CommonJS:

接著,我們要宣告declare var expect: Chai.ExpectStatic,將全域變數expect函式定義成Chai版的expect,以便使用Chai的is.equal()串接語法。跟import指令一樣,declare指令不產生JavaScript,只決定TypeScript撰寫階段的變數型別。

還有一個小問題,在Protractor裡,expect()應傳回Promise形式的Assert函式,故得寫成is.eventually.equal()。故一開始的import指令要改成import chaiAsPromised = require("chai-as-promise"),Visual Studio才認得expect().is.eventually.equal()。

完整測試程式碼如下:

import chaiAsPromised = require("chai-as-promised");
declare var expect: Chai.ExpectStatic;
 
describe('Protractor Demo App', function () {
 
var firstNumber = element(by.model('first'));
var secondNumber = element(by.model('second'));
var goButton = element(by.id('gobutton'));
var latestResult = element(by.binding('latest'));
 
    beforeEach(function () {
        browser.get('http://juliemr.github.io/protractor-demo/');
    });
 
    it('should have a title', function () {
        expect(browser.getTitle()).is.eventually.equal("Super Calculator");
    });
 
    it('should add one and two', function () {
        firstNumber.sendKeys("1");
        secondNumber.sendKeys("2");
        goButton.click();
 
        expect(latestResult.getText()).is.eventually.equal("3");
    });
 
});

總算Visual Studio也認得to.eventual.equal()的chai-as-promise語法,編譯成功,收工!

Unable to Modify KendoAutoComplete Selected Value

$
0
0

After selecting a hint item in KendoAutoComplete, I can't change selected text by backspace key.  It will be restored to the original selected value repeatly until you select all text and type a different charactor.  This bug will be fixed after upgrade from 2015.1.318 to 2015.1.408+.  Just a note in case anyone meets this issue.

使用KendoAutoComplete遇到一個小問題,當有多個相近提示項目,選取其中之一再用Backspace(倒退鍵)刪去字元,KendoAutoComplete會自動補填回剛才的選取項目,像是中了詛咒,必須全選輸入不同字元才能脫身。如以下展示,問題在改用2015.4.408版後消失,推斷為2015.1.318版的Bug,小問題一枚卻花了半個多小時抽絲剝繭才確定,筆記分享以防有其他人踩到。

在Windows 2008跑ASP.NET MVC 4

$
0
0

接獲報案,ASP.NET MVC 4(.NET 4.5)部署到Windows 2008/IIS 7無法正常運作,存取png或css等靜態檔案OK,而MVC的controller/action路由沒生效,連上根目錄出現HTTP 403.14,推測為home/index路由失效,又沒有index.html、default.aspx等預設文件導致:

很類似「IIS6 缺少萬用字元應用程式對應」造成的現象(參考),但在我的經驗,只有Windows 2003需要額外手工設定,IIS 7以上應該內建支援MVC路由才對。

經過一番調查,發現問題癥結所在-問題主機是Windows 2008 SP2,不是常見的Windows 2008 R2,而且它的IIS是IIS 7.0不是IIS 7.5!

若對應到Windows客戶端版本,Windows 2008相當於Vista、Windows 2008 SP2相當於Vista SP2,Windows 2008 R2才對應到Windows 7。Windows 7是不少技術的作業系統最低要求,而我平常摸到的Windows 2008都是R2以上版本,就沒意識到Windows 2008非R2版本存在支援度不足的問題。

知道問題癥結就好辦,在web.config加入以下設定即可解決問題:

<system.webServer>
<modulesrunAllManagedModulesForAllRequests="false"/>
<!-- 略 -->
</system.webServer>

而另一個問題是-為什麼ASP.NET MVC 4 web.config檔少了這個設定?當年不就會害一堆人踩到雷?再深入研究,這源自Visual Studio ASP.NET MVC 4專案範本 .NET 4版與.NET 4.5版的差異(參考:保哥的文章)。若採用.NET 4版ASP.NET MVC 4,web.config有內含<modules runAllManagedModulesForAllRequests="false" />,部署到Windows 2008 SP2 + IIS 7可以過關;但網站演進到新一代的.NET 4.5 ASP.NET MVC 4,基於效能考量已拿掉runAllManagedModulesForAllRequests設定,沒料想到會遇到遲未升級的古老Windows 2008 SP2,就…

如果擔心加入runAllManagedModulesForAllRequests影響效能,另一個解決方案是安裝KB980368,讓IIS 7也能處理MVC這類沒有副檔名的路由URL,會是較完美的做法。

2015海山馬拉松

$
0
0

第27馬,在哪裡跌倒,在哪裡站起來-海山馬!

重回生涯至今唯一一次落馬的傷心地,本應跑它個精彩以求雪恥,但這陣子天氣漸熱,體能狀況下滑得厲害,已無本錢立定雄心壯志,只求平安輕鬆完賽就好。

前一天氣溫衝上36度,幸好週日轉陰回到30出頭,清晨四五點來了一陣大雨,開跑前雨停並逐漸轉晴,但一上午仍以陰天為主,全程只曬到幾分鐘太陽,雖然氣溫近30度又沒什麼風,老天爺已經給足面子,五月跑馬有此待遇,不敢再奢求。

六點起跑,一樣的拱門,沒寫年份的布條,比對舊文至少已連用三年,這樣不浪費講環保也好。

     

剛起跑,下過雨的地面有股濕氣向上蒸,悶熱逼人,直到人潮跑散才好些。在堤防牆面看到一幅壁畫(半成品?)眼睛一亮,是我愛的速寫風格。

來到七公里處,去年停下來拍照不慎拐腳落馬的傷心地,特别駐足憑弔一分鐘。去年扭到腳的事故地點在照片左轉叉路處,已封閉加上圍籬,故今年路線有改,改為直行。而距可靠消息來源指出,歷經一年的爭取,新北市府終於通過預算於上個月動土,將於該處建立三十公尺高的「黑大落馬紀念碑」。(特大誤)

     

途經碼頭水景,好些之前沒看過(或是看過但忘個精光)的新設施。上右照片拍到的紅衣黑褲戴頭巾跑友是馬場名人-韋海浪醫師。

     

向南跑到中正橋折返,耗了四小時才再經過浮洲橋會場。出門匆忙忘記貼透氣膠帶,擔心上演「血奶頭」慘劇,繞進會場醫護站想要點凡士林救急,醫護小姐先是楞了一下,表示沒這種東西… 暈。幸好揹了水袋,胸前背帶扣緊有不錯的固定效果,衣服與皮膚磨擦較少,尼頗(Nipple)兄弟得以安全下莊。XD

跑到三峽35K折返點拍照留念,剛好遇上去年救駕的大哥,同一部機車、同樣的背心、同一頂安全帽,一年後重逄,心中仍是感謝~

最後,5:41:51回到終點,大會名次395/823。領了獎牌(公版,樣式跟前年相同只是改成古銅色,我就不拍照了)、便當、證書、小毛巾,再下一馬。

跑了5:40,排名仍在前50%,好奇大家成績都落在何處?索性抓了大會成績匯入Excel,繪製樞紐分析圖如下:

有成績者共669人,各時段跑完人數大致符合常態分配的鐘形,而我所在的5:40-5:49有61人達到高峰,恰巧也是50%所在,與常態分配模型吻合。有趣的是,4:30-4:39、4:50-4:59、6:20-6:29出現異常突起,推測是430、SUB5、關門等心理關卡效應,而結果顯示挑戰430失敗率不低。

以上為黑暗執行緒不負責分析報告。

Coding4Fun-Facebook動態消息精簡化顯示

$
0
0

電影鐘點戰有一段一直令我印象深刻,在時間成為貨幣且貧富懸殊的未來世界,窮人們不敢賴床,不穿有繁瑣鈕扣配件的衣服、隨時隨地都在小跑步… 這些年來,工作與生活進入另一種平衡,但老覺可支配時間少得可憐,雖然手臂上沒有一組隨時在倒數的數字(仔細想想,打從一出生就有個看不到的數字在倒數了),無形中養成早起、快食、走路速度逼近小跑步的習慣。如果我在時間財M形化社會身處窮人這一端,另一端的好野人應該是早上睡到十二點,瞇著眼穿拖鞋搖搖晃晃散步去吃早餐的那群大學生吧?:P

這些年,臉書取代部落格成為我吸收技術、八卦時事、知識及人際訊息的主要管道,平日走路都習慣小跑步的時間窮鬼,無法容忍自己好整以暇待在FB動態消息頁面慢慢捲,又生怕漏看錯過重要資訊,兩相矛盾之下,不意外地演變為開著FB網頁拼命滾滑鼠,視窗快速向下捲動,根本開賽車來著 XD

久而久之,我發現有很大一部分被直接略過的訊息,多半是朋友在其他親友的公開貼文按讚、回應或被標註。這是FB設計的優點也是缺點,讓你有機會認識朋友的朋友,接收新訊息,擴大人脈,實踐社群網路的神聖精神。而缺點呢?要一路逛遍陌生人(朋友的朋友)旅遊打卡遛小孩吃下午茶聽演唱會換大頭貼照片的漫長清單,從中過濾找出想看的資訊。雖然不乏經由陌生人貼文認識有趣人、事、物的經驗,但它們畢竟來自陌生人,又常夾雜柴米油鹽等生活瑣事,資訊含量偏低,加上圖文並茂就會將動態時報撐得老長,我的食指滾滑鼠滾輪都快滾出六塊肌了(誤)。當然,將朋友分群組實現資訊減量是種解法,時間不夠就只看重要群組,其餘放生。但你知道的,對資訊焦慮症患者「寧可錯看一百,也不可錯放一個」才是終極奧義 orz

最近,我想出一個貌似兩全其美的做法,寫了個Tampermonkey/Greasemonkey外掛,用JavaScript技巧將朋友說讚、回應、被標註等三類訊息的本文隱藏起來,只留前64個字元當摘要,摘要本身是閞關,點下可展開原文,如下圖所示:

如此,動態消息的頁面長度大幅縮短,減輕食指操勞,而看到有興趣的仍可展開閱讀,不會錯失訊息,魚與熊掌兼得了啊!(喜極而泣)

我把腳本放在Greasy Fork,如果你的瀏覽器有裝Tampermonkey (Chrome) 或 Greasemonkey(Firefox)(堅持用IE的朋友請靜待Microsoft Edge,使用iOS/Android看臉書的朋友請當我沒說),可以點選這裡安裝腳本試用。

這個外掛以自製自用為宗旨,大家如果有意見可以回饋給我(在這裡或FB專頁留言),我會勉力修正強化,如有未盡人意之處,腳本原始碼是公開的,就"Use the source, Luke!"囉~


// ==UserScript==
// @name       Facebook動態消息精簡化顯示
// @namespace  http://blog.darkthread.net/
// @version    0.9
// @description  隱藏動態消息頁面的間接訊息(說讚、回應、被標註),只顯示64字元摘要,但可視需要展開原文
// @match      https://www.facebook.com/
// @copyright  2015+, Jeffrey Lee
// ==/UserScript==
 
//REF:http://stackoverflow.com/questions/15329167/closest-ancestor-matching-selector-using-native-dom
//模擬jQuery.closest()
function closest(elem, selector) {
var matchesSelector = elem.matches || elem.webkitMatchesSelector || 
                          elem.mozMatchesSelector || elem.msMatchesSelector;
while (elem) {
if (matchesSelector.bind(elem)(selector)) {
return elem;
        } else {
            elem = elem.parentElement;
        }
    }
returnfalse;
}
//取得頁面總長
function getDocHeight() {
return document.documentElement.scrollHeight;
}
var lastLen = getDocHeight();
//隱藏元素,取得64個字元作為摘要,並提供展開功能
//PS:此段很脆弱,一旦FB修改DOM格式就會失效
function hidePost(elem) {
var content = elem.parentElement.parentElement.parentElement
                  .nextElementSibling.nextElementSibling;
var friend = content.querySelector("h6").innerText.replace(/\n/g, "");
varabstract = content.querySelector(".userContent").innerText;
if (abstract.length > 64) abstract = abstract.substr(0, 64) + "...";
abstract = abstract.replace(/\n/g, " ");
var spn = document.createElement("a");
    spn.innerText = friend + " - " + abstract;
    spn.onclick = function() { 
        content.style.display = "";
        spn.remove();
//更新頁面長度
        lastLen = getDocHeight();
    };
    elem.parentElement.appendChild(spn);
    content.style.display = "none";
}
var busy = false;
setInterval(function() {
//長度未改變時不動作
if (getDocHeight() == lastLen || busy) return;
    busy = true; //防止重覆執行
var ary = document.querySelectorAll(
"div[id^=topnews_main_stream] h5 span.fcg:not([data-shrink])");
for (var i = 0; i < ary.length; i++) { 
        ary[i].setAttribute("data-shrink", "Y"); //處理完畢加上註記
var t = ary[i].innerText;
if (t.indexOf("說這個讚") > -1 || t.indexOf("被標註") > -1 || 
            t.indexOf("回應了") > -1) 
            hidePost(ary[i]);
    }    
//移除推薦貼文
    ary = document.querySelectorAll("div[id^=topnews_main_stream] ._5g-l");
for (var i = 0; i < ary.length; i++) {
        closest(ary[i], ".userContentWrapper").innerText = ary[i].innerText;
    }
    lastLen = getDocHeight();
    busy = false;
}, 3*1000);

copyText();

Coding4Fun-運動筆記賽事距離篩選器

$
0
0

馬拉松需要提前好幾個月前報名,熱門賽事報名又常上演秒殺戲碼,因此全盤掌握未來半年賽事資訊,妥善規劃「檔期」、留意報名時間是愛馬士(愛跑馬人士)的必備功課。我個人則偏愛運動筆記的賽事列表,除了資訊完整,程式部分採用jQuery/AJAX,介面富有濃厚的HTML5風格,甚至還用了Font Awesome呢!(謎之聲:這些跟馬拉松比賽是有什麼關係啦?)

唯一美中不足-運動筆記的賽事網頁沒有距離篩選功能,台灣這幾年路跑賽事多如牛毛,查詢結果項目超過200筆,網頁本身提供地點、類型、年份篩選,卻沒有鎖定特定距離(全馬、半馬)的功能。對我這種鎖定全馬拼業績的人,得在一堆比賽挑出全馬,有點小麻煩。

會寫網頁有一項額外好處:遇到網頁不如己意,動手改到自己開心!

我寫了一個小外掛,為運動筆記賽事網頁偷偷加上自製距離篩選器,篩選器可掃瞄所有賽事加以分類,分成5K、5K+(5K以上,21K以下)、半馬、超半馬、全馬及超馬,預設為全部顯示,點選距離項目右側的小叉叉可排除該分類。如下圖所示,清單可以只顯示有全馬項目的比賽:

如果想使用這個篩選功能,Chrome瀏覽器需先安裝Tampermonkey(Firefox則要安裝Greasemonkey),接著可連Grease Fork點選「安裝腳本」:

以Tampermonkey為例,按下「安裝腳本」後,會進入程式碼檢視頁面,請點選「安裝」:

之後,重新進入運動筆記賽事頁面,Tampermonkey圖示會顯示紅字的1,點選圖示展開選單可見「運動筆記賽事清單路跑距離篩選」,若想停用可點選前方的綠色數字鈕。

這時可以看一下網頁,年份篩選器的下方會默默多出距離選項,Enjoy It!

外掛畢竟還是奇技淫巧,運動筆記直接內建距離篩選功能才是最完美的解決方案。附上程式碼,並將它設成CC0(公眾領域貢獻宣告)授權,對於參考、引用、改寫(且不需加註原作者)均不設限,希望能有一丁點貢獻。


// ==UserScript==
// @name       運動筆記賽事清單路跑距離篩選
// @namespace  http://blog.darkthread.net/
// @version    0.9.2
// @description  為運動筆記之賽事清單加入路跑距離篩選器 by Jeffrey Lee, 黑暗執行緒
// @match      http://tw.running.biji.co/index.php?q=competition*
// @license    http://creativecommons.tw/cc0
// ==/UserScript==
 
//依長度不同區分為六大類
var distCatgs = {
"5K": "5k", "5K+": "5k-plus", "半馬": "half-ma", 
"超半馬": "ultra-half-ma", "全馬": "std-ma", "超馬": "ultra-ma"
};
function scanEvents() {
//若先前已掃瞄過,略過不處理
if ($(".com_detail_info:first").hasClass("scan")) return;
//掃瞄所有賽事,依其距離分類,以class標註於資料列元素
//場數統計表
var stats = {}; 
  $(".com_detail_info .competition_event .event_item").each(function() {
var match = /[0-9.]+K/.exec(this.innerText);
if (!match) return;
var dist = match[0];
var km = parseInt(dist.replace("K", ""));
// 分類成 5K, 5K+, 半馬, 超半馬, 全馬, 超馬
var catg = "5K";
if (km > 5 && km <21) catg = "5K+";
elseif (km == 21) catg = "半馬";
elseif (km > 21 && km < 42) catg = "超半馬";
elseif (km == 42) catg = "全馬";
elseif (km > 42) catg = "超馬";
// 在資料列元素上以class標示
    $(this).closest(".com_detail_info").addClass(distCatgs[catg]);
if (stats.hasOwnProperty(catg)) stats[catg]++;
else stats[catg] = 1;
  });
  console.log("Scanned");
//更新統計數於篩選鈕後方
var $fltrBar = $("#distance_filter");
  $.each(Object.keys(distCatgs), function(i, c) {
    $fltrBar.find("." + distCatgs[c] + " .counter").text(stats[c]);
  });
//在第一筆賽事做記號註記已經掃瞄
  $(".com_detail_info:first").addClass("scanned");
}
// 加入篩選器
$("#distance_filter").remove(); //若已存在先移除之
var h = [];
$.each(Object.keys(distCatgs), function(i, d) {
  h.push(
'<div class="float_select ' + distCatgs[d] + '" data-catg="' + distCatgs[d] + '">' +
'<div class="filter_choose_item com_type filter_select_item">' + d + 
' <span style="font-size:80%">(<span class="counter"></span>)</span>' +
'</div>' +
'<div class="drop_choose fa fa-remove" style="display:block"></div>' +
'</div>');
});
$("#menu_choose_bar .filter_item:last").before(
'<div class="filter_item" id="distance_filter">' +
'<div class="filter_type">距離</div>' +
'<div class="filter_value">' +
  h.join() +
'</div></div>');
 
var $filterBar = $("#distance_filter");
$filterBar.on("click", ".filter_choose_item", function() {
  $(this).addClass("filter_select_item").next().show();
  scanEvents();
  filter();
returnfalse; //防止觸發原始網頁的查詢
}).on("click", ".fa-remove", function() {
  $(this).hide().prev().removeClass("filter_select_item");
  scanEvents();
  filter();
returnfalse;
});
//執行篩選
function filter() {
var $all = $(".com_detail_info");
  $all.removeClass("show").hide(); //先全部隱藏
var selectedCatgs = $.map($filterBar.find(".filter_select_item"), 
function(elem) { //有選取的距離加上show
var catgCss = $(elem).parent().attr("data-catg");
      $all.filter("." + catgCss).addClass("show") 
    });
  $all.filter(".show").show(); //顯示有標上show者
}
scanEvents();

【茶包射手日記】ORA-12514鬼打牆記

$
0
0

接獲報案,本機測試無誤的程式部署到測試台,Managed ODP.NET開啟連線時冒出ORA-12514錯誤,依照上回處理經驗,直覺又是IIS/machine.config相關的問題。

ORA-12514: TNS: 監聽器目前不知道連線描述區中要求的服務.
ORA-12514: TNS:listener does not currently know of service requested in connect descriptor

同事有提到錯誤代碼與文章所寫不同,12514 vs 12154,一時鬼迷心竅將二個錯誤代碼歸為同類,還是一頭栽進去調查為什麼讀不到web.config設定?(TNS名稱的解析順序可參考這裡

<oracle.manageddataaccess.client>
<versionnumber="*">
<dataSources>
<dataSourcealias="MYDB"descriptor="
                           (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)
                           (HOST=192.168.1.79)(PORT=1521))
                           (CONNECT_DATA=(SERVICE_NAME=MYDB))) "/>
</dataSources>
</version>
</oracle.manageddataaccess.client>

加上陰錯陽差一開始沒找對連線字串,刻意改壞設定也不會出錯讓人一頭霧水,東改西改鬼打牆半天,最後才發現是個低級錯誤:192.168.1.89被誤打成192.168.1.79! 理應來自複製貼上的設定值為什麼IP會打錯是個謎,但最讓我氣結的是-ORA-12514跟ORA-15154相似度高達「3A2B」,根本是完全不同的錯誤類型啊!

ORA-12154: TNS: 無法解析指定的連線ID
ORA-12154: TNS:could not resolve the connect identifier specified.

ORA-12514: TNS: 監聽器目前不知道連線描述區中要求的服務.
ORA-12514: TNS:listener does not currently know of service requested in connect descriptor

訊息本身說得清楚,web.config找不到alias或TNSNAMES.ORA會是ORA-12154,無法解析連線ID;而ORA-12514提到監聽器(Listener)不認得服務描述,明顯為連上ORACLE但找不到指定的服務名稱。

如果一開始有意識到ORA-12514與12154的明顯差異,仔細確認dataSource alias,就不會鬼打牆這麼久。悔不當初,特PO文以為警惕。

TFS組建定義刪除重設導致工作區重複

$
0
0

有個TFS組建定義(Build Service)怪怪的,無法下載原始碼,幾經嘗試無解,索性刪掉再重設一次。(推薦VS TFS Power Tools,組建定義選單多出「Clone Definition」可以複製現有組建定義修改,不用從頭做起)

不料,新増相同原始碼來源的組建定義後,執行出現以下錯誤:

Exception Message: Unable to create the workspace '9_1_VM-BLD-SVC12' due to a mapping conflict. You may need to manually delete an old workspace. You can get a list of workspaces on a computer with the command 'tf workspaces /computer:%COMPUTERNAME%'. Details: The path D:\Works\1\PRJ\PRJQ-Test\src is already mapped in workspace 8_1_VM-BLD-SVC12. (type MappingConflictException)

大意是先前被刪掉的建置定義已將該專案原始碼對應到8_1_VM-BLD-SVC12工作區,新増的組建定義企圖另建一個新的9_1_VM-BLD-SVC12工作區對應原始碼,因而產生衝突,錯誤訊息提示需使用tf.exe工具刪除舊工作區解決。(在同一台電腦上,工作區與原始碼版控儲存區必須保持一對一的唯一對應)

遠端登入TFS Build Service主機,開啟Visual Studio 2013工具的開發者命令提示字元(Developer Command Prompt for VS2013),輸入tf workspaces可列出目前已建立的工作區,8_1_VM-BLD-SVC12名列其中,Owner為bldsvc.user。使用tf workspace /delete 8_1_VM-BLD-SVC12;bldsvc.user將它砍了,問題排除。

C:\Program Files (x86)\Microsoft Visual Studio 12.0>tf workspaces
Collection: httq://tfs:8080/tfs/prj-one
Workspace        Owner       Computer     Comment
---------------- ----------- ------------ -------------------------------------
1_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build
2_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build
3_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build
4_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build
5_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build
6_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build
7_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build
8_1_VM-BLD-SVC12 bldsvc.user VM-BLD-SVC12 Workspace Created by Team Build

C:\Program Files (x86)\Microsoft Visual Studio 12.0>tf workspace /delete 8_1_VM-
BLD-SVC12;bldsvc.user
A deleted workspace cannot be recovered.
Workspace '8_1_VM-BLD-SVC12;bldsvc.user' on server 'httq://tfs:8080/tfs/prj-one' 
has 0 pending change(s).
Are you sure you want to delete the workspace? (Yes/No) y

C:\Program Files (x86)\Microsoft Visual Studio 12.0>

【茶包射手筆記】Windows 8當機原因的簡易調查

$
0
0

新裝某個第三方工具後有點小後悔,對其來歷沒有十足把握,擔心其中暗藏木馬病毒,邊想邊開Chrome瀏覽網頁、下載檔案,說時遲那時快,我的Windows 8.1冒出詭異哭臉,靠!BSOD!(Blue Screen of Death,就是俗稱的藍白當機畫面)

(沒來得及拍照片,從網路找到訊息相同的BSOD畫面,來源

重開機後一切看似正常,本可一笑置之,但當機前一刻才懷疑某個軟體藏毒,時機巧合到令人不安。經使用windbg檢查,初步排除木馬病毒嫌疑,這才安了心。

回頭檢視,使用Windows Debugger檢查記憶體傾印檔(Memory Dump)的步驟並不算複雜,不一定要工程師才能處理,突發奇想,整理出簡單步驟,讓非技術咖的朋友也有緣藉此初步診斷當機原因。(註:還是得看綠份,如果讀完本文仍一頭霧水,代表機緣尚未成熟,施主請回)

當機畫面出現collecting some error info…字樣以及執行百分比,代表Windows已將關鍵的記憶體內容保存寫入磁碟(稱為Memory Dump,記憶體傾印檔),就像行車記錄器拍下事故發生現場的畫面,方便事後調查分析。但這種「行車記錄畫面」需要特殊程式解讀,這支特殊程式叫做Windows Debugger(WinDbg),程式名稱跟操作畫面都很可怕,WinDbg讓專家得以剖析Memory Dump找出當機細節,追查肇事責任,但此非一般人能力所及(我也沒這本事)。所幸,WinDbg也提供簡易的傻瓜模式,能約略指出當機由哪一支程式或那個環節造成的,對我們來說,這樣就很夠了。

首先,要找出Memory Dump所在位置,事件檢視器可以幫上一些忙:(在Windows 8.1的開始鈕右鍵選單可開啟事件檢視器,它屬於系統事件)

如果你從來沒有安裝過Windows Debugger,要先下載安裝Windows 8.1 SDK。安裝時務必選取Debugging Tools for Windows,其他項目可不選:

安裝完畢,執行WinDbg(X64):

分析Memory Dump時需從微軟網站下載必要的符號檔(Symbol),第一次啟用前需設定Symbol伺服器及Symbol檔存放位置,請先選Symbol File Path,輸入SRV*X:\Symbol*http://msdl.microsoft.com/download/symbols(X:\Symbol為存放Symbol檔的資料夾,路徑請自行修改),按下OK,並執行Save Workspace讓WinDbg記住設定:

以上操作只需做一次,之後不管當機一百次一千次(呸呸呸!),直接Open Crash Dump開啟Memory Dump就可以分析。

剛才在事件檢視器已知dmp檔被放在C:\Windows\MiniDump:

開啟後,WinDbg將下載必要的Symbol檔,過程需要一些時間,請耐心等侯。Symbol載入完成後,下方的0: kb>輸入框可以輸入分析指令(在此之前會出現Debuggee not connected字樣,代表尚在準備中),WinDbg很貼心地在訊息中給了個「!analyze -v」超連結,這是我們唯一要用的101個指令,按下去將執行自動化分析,如果分析不出什麼,也就沒招了。所幸在經驗中,它的分析結果已能提供足夠的當機線索。

在本案例,!analyze -v指出當機是由Chrome引起的!而我想起,在安裝第三方工具前Chrome就已經出現多次卡住凍結,而當機前一刻也在用Chrome在瀏覽網頁,以此推論,初步排除第三方工具有毒的嫌疑。

追加另一則當機分析,分析結果可看出為Win8驅動程式出錯引起:

以上兩次當機個案,經由WinDbg對Memory Dump的簡單分析,雖然只非常粗略地指出當機範圍(Chrome、驅動程式),但參酌當機前夕對電腦的調整與操作,仍遠勝「怎麼死的都不知道」。

希望以上介紹能減少大家在面對Windows藍白當機畫面時的茫然,願原力與你同在~

【茶包射手筆記】Dictionary.Add()效能問題調查

$
0
0

依據MSDN文件:

If Count is less than the capacity, this method approaches an O(1) operation. If the capacity must be increased to accommodate the new element, this method becomes an O(n) operation, where n is Count.

只要Dictionary資料筆數沒超過Capacity(Capacity可在建構時指定),資料新增屬O(1)操作,每次呼叫的執行速度不因Dictonary資料量多寡改變,理論上為定值。

網友Cb Atm提供了一則打破O(1)規則的奇妙案例。經我簡化改寫如下:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace DictKey
{
class Program2
    {
constint CAPACITY = 20000;
publicstruct Index
        {
publicstring StkNo;
publicbyte TypeCode;
publicint Idx;
        }
static Random rnd = new Random(32767);
static List<Index> stocks =
            Enumerable.Range(0, CAPACITY).Select(
                o => new Index()
                {
                    Idx = o,
                    StkNo = rnd.Next(1000, 1011).ToString(),
                    TypeCode = (byte)rnd.Next(0, 10)
                }).ToList();
staticvoid Test1()
        {
            var dictStruct = new Dictionary<Index, int>(CAPACITY);
            Action<int> test = (count) =>
            {
                Stopwatch sw = new Stopwatch();
                dictStruct.Clear();
                sw.Start();
for (var i = 0; i < count; i++)
                {
                    dictStruct.Add(stocks[i], i);
                }
                sw.Stop();
                Console.WriteLine("{1:n0} items: {0:n0}ms",
                    sw.ElapsedMilliseconds, dictStruct.Count);
            };
            test(10000);
            test(CAPACITY);
        }
staticvoid Main(string[] args)
        {
            Test1();
            Console.Read();
        }
    }
}

程式宣告一個Index Struct作為Dictionary的TKey型別,測試時使用亂數產生兩萬筆資料(但StkNo屬性值限定介於1000-1010,TypeCode限定0-9,Idx欄位取流水號具唯一性),分別測試Dictionary<Index, int>.Add()一萬筆及兩萬筆,依O(1)理論,後者的時間應為前者兩倍,事實不然:

10,000 items: 1,993ms
20,000 items: 7,869ms

塞入兩萬筆耗時為塞入一萬筆的四倍左右,為什麼?

做了一個小改寫,移除StkNo亂數取值的範圍限制,改成StkNo = rnd.Next().ToString()(實際範圍放大成int.MinValue到int.MaxValue),得到完全不同結果!

10,000 items: 6ms
20,000 items: 5ms

除了執行速度加快數百倍,加入一萬筆與加入兩萬筆的時間不分上下。(表示Add()所耗時間已非瓶頸,差異微小到可被忽略)

再試改寫成StkNo = rnd.Next(1000,1100).ToString(),讓StkNo介於1000到1099,結果則為:

10,000 items: 247ms
20,000 items: 877ms

速度比最初版本快8-9倍,塞入兩萬筆的時間為塞入一萬筆的3.5倍。

過去對雜湊表原理了解有限,歸納可知StkNo多樣性影響效能甚鉅,但不知其所以然。爬文查到Struct作為TKey時,鍵值比對時與GetHashCode()有關,但再多理論都比不上Source Code權威及清楚明瞭。使用JustDecompile揭開Dictionary<TKey, TValue> .Add()神祕面紗:

privatevoid Insert(TKey key, TValue value, bool add)
        {
int num;
if (key == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
if (this.buckets == null)
            {
this.Initialize(0);
            }
int hashCode = this.comparer.GetHashCode(key) & 2147483647;
int length = hashCode % (int)this.buckets.Length;
int num1 = 0;
for (int i = this.buckets[length]; i >= 0; i = this.entries[i].next)
            {
if (this.entries[i].hashCode == hashCode && 
this.comparer.Equals(this.entries[i].key, key))
                {
if (add)
                    {
                        ThrowHelper.ThrowArgumentException(
                              ExceptionResource.Argument_AddingDuplicate);
                    }
this.entries[i].@value = value;
                    Dictionary<TKey, TValue> dictionary = this;
                    dictionary.version = dictionary.version + 1;
return;
                }
                num1++;
            }

依其程式邏輯,Insert時(Add背後會呼叫Insert(key, value, true))先用GetHashCode()取得key參數的hashCode值,接著依Bucket分群規則(取hashCode除以Bucket個數的餘數,餘數相同者放在同一Bucket)比對同一Bucket的現存Key值,若hashCode相同且新舊Key值.Equals()也相等,依add參數決定要拋出鍵值重複錯誤還是取代現值。延伸閱讀

由此可知:

  1. hashCode可能重複,而多個hashCode可能同屬一個Bucket
  2. 每次比對時先由hashCode找出其所屬Bucket,一一比對該Bucket下是否已存在相同鍵值
  3. 比對鍵值是否相同的邏輯為 this.entries[i].hashCode == hashCode && this.comparer.Equals(this.entries[i].key, key),當hashCode相同才會進行第二階段Equals()比對。

故可推論,當hashCode重複的機率愈高,鍵值會集中在少數Bucket中,額外執行Equals()的次數愈高,勢必拖累效能。

所以新增一萬筆會耗時近2秒是因為StkNo變化太少導致Hash Code大量重複嗎?用以下段程式驗證:

staticvoid Test2()
{
    var hashCodeStats = new Dictionary<int, int>();
for (var i = 0; i < 20000; i++)
    {
        var hashCode = stocks[i].GetHashCode();
if (hashCodeStats.ContainsKey(hashCode))
            hashCodeStats[hashCode]++;
else
            hashCodeStats.Add(hashCode, 1);
    }
    Console.WriteLine("共有{0}種HashCode", hashCodeStats.Count);
}

測試結果:

StkNo = rnd.Next(1000, 1011).ToString() 兩萬筆資料只有11種HashCode
StkNo = rnd.Next(1000, 1100).ToString() 兩萬筆資料只有100種HashCode
StkNo = rnd.Next().ToString() 兩萬筆資料有兩萬種HashCode

雖然兩萬筆資料有唯一的Idx屬性,還配上不同TypeCode,但GetHashCode()似乎只跟StkNo有關,使用以下程式進一步證實這點:

staticvoid Test3()
{
    var idx1 = new Index()
    {
        StkNo = "AAAA",
        Idx = 1,
        TypeCode = 2
    };
    var idx2 = new Index()
    {
        StkNo = "AAAA",
        Idx = 100,
        TypeCode = 200
    };
    var idx3 = new Index()
    {
        StkNo = "AAAB",
        Idx = 1,
        TypeCode = 2
    };
    Console.WriteLine("{0} vs {1} vs {2}",
        idx1.GetHashCode(), 
        idx2.GetHashCode(), 
        idx3.GetHashCode());
}

結果為80968112 vs 80968112 vs -651627219,StkNo相同GetHashCode()就相同。

由以上分析,想改善新増效能有兩個解決方向:1) 改用唯一值Idx當鍵值 2) 解決Index.GetHashCode()大量重複的狀況

第一種做法可將Dictionary<Index, int>改為Dictionary<int, int>:

staticvoid Test1A()
{
    var dictStruct = new Dictionary<int, int>(CAPACITY);
    Action<int> test = (count) =>
    {
        Stopwatch sw = new Stopwatch();
        dictStruct.Clear();
        sw.Start();
for (var i = 0; i < count; i++)
        {
            dictStruct.Add(stocks[i].Idx, i);
        }
        sw.Stop();
        Console.WriteLine("{0:n0} items: {1:n0}ms {2:n0}ticks",
            dictStruct.Count,
            sw.ElapsedMilliseconds, 
            sw.ElapsedTicks);
    };
for (var i = 0; i < 5; i++)
    {
        test(10000);
        test(CAPACITY);
    }
}

修改後,一萬筆及兩萬筆的執行時間都變成0ms,速度快到測不出來,需改比ElapsedTicks。程式跑五次,排除第一次暖機期,兩萬筆的執行時間約為一萬筆的兩倍。

10,000 items: 1ms 3,471ticks
20,000 items: 0ms 2,248ticks
10,000 items: 0ms 545ticks
20,000 items: 0ms 1,127ticks
10,000 items: 0ms 561ticks
20,000 items: 0ms 1,140ticks
10,000 items: 0ms 540ticks
20,000 items: 0ms 1,082ticks
10,000 items: 0ms 573ticks
20,000 items: 0ms 1,113ticks

但修改成Dictionary<int, int>有後遺症,取得鍵值數字後需另外查表才態拿到Index型別,程式需配合改寫。還有第二種做法,我們override Index的GetHashCode()傳回更具唯一性的雜湊值(流水號整數Idx是很好的選擇),如下:

publicstruct Index
 {
publicstring StkNo;
publicbyte TypeCode;
publicint Idx;
publicoverrideint GetHashCode()
     {
return Idx;
     }
 }
staticvoid Test1()
 {
     Action<int> test = (count) =>
     {
         var dictStruct = new Dictionary<Index, int>(CAPACITY);
         Stopwatch sw = new Stopwatch();
         dictStruct.Clear();
         sw.Start();
for (var i = 0; i < count; i++)
         {
             dictStruct.Add(stocks[i], i);
         }
         sw.Stop();
         Console.WriteLine("{0:n0} items: {1:n0}ms {2:n0}ticks",
             dictStruct.Count,
             sw.ElapsedMilliseconds,
             sw.ElapsedTicks);
     };
for (var i = 0; i < 5; i++)
     {
         test(10000);
         test(CAPACITY);
     }
 }

測試結果如下,自訂GetHashCode()後,ticks數為Dictionary<int, int>的兩倍,但執行時間已加速到小於1ms,兩萬筆耗時約為一萬筆的2.5倍:

10,000 items: 3ms 11,730ticks
20,000 items: 0ms 2,292ticks
10,000 items: 0ms 1,471ticks
20,000 items: 0ms 2,035ticks
10,000 items: 0ms 1,089ticks
20,000 items: 0ms 2,749ticks
10,000 items: 0ms 1,064ticks
20,000 items: 0ms 2,247ticks
10,000 items: 0ms 1,078ticks
20,000 items: 0ms 2,677ticks

【結論】使用Struct作為Dictionary TKey,若GetHashCode()值重複性過高,將嚴重影響Add()效能,應考慮自訂GetHashCode()增加差異性加以改善,或改用string或int等單純型別以達最佳效能。

讓刪除確認通吃confirm及jQuery.Deferred

$
0
0

最近在寫的元件有個「彈出對話框經使用者確認再刪除」的需求,原本是小事一椿,但之前介紹過使用自訂確認對話框取代window.confirm的技巧已廣泛應用在專案裡,某些時候也可能只用window.confirm就打發,問題就變複雜了。

二者最大的差異是前者($.kendoConfirm)為非同步執行,刪除動作需放在jQuery Deferred.promise()的done()方法;window.confirm則為內建同步指令,瀏覽器會等待使用者回應才往下執行,故用 if 判斷 true/false 決定是否刪除即可。

以下程式可看出兩種做法的差異:線上展示(關於 $.kendoConfirm 的說明請參考舊文

    $(function() {
function deleteConfirm_Kendo() {
return $.kendoConfirm("刪除確認", "是否確定要刪除?");
      }
function deleteConfirm_Native() {
return confirm("是否確定要刪除?");
      }
      $("#btnTest1").click(function() {
        deleteConfirm_Kendo().done(function() {
          alert("刪除");
        });
      });
      $("#btnTest2").click(function() {
if (deleteConfirm_Native()) {
          alert("刪除");
        }
      });
    });

要讓元件好用,最理想的做法是大小通吃:若確認函式傳回true/false就用 if 同步執行;若傳回jQuery Promise就用.done()非同步執行。

例如:線上展示

    $(function() {
function deleteConfirm_Kendo() {
return $.kendoConfirm("刪除確認", "是否確定要刪除?");
      }
function deleteConfirm_Native() {
return confirm("是否確定要刪除?");
      }
function execDeleteAction() {
        alert("刪除");
      }
function doDelete(deleteConfirm) {
var res = deleteConfirm();
//若函式傳回Promise, 放入done()中執行
if (res && res.hasOwnProperty("done")) 
          res.done(execDeleteAction);
//若函式傳回boolean,由傳回結果決定resolve或reject
elseif (res) 
          execDeleteAction();
      }
      $("#btnTest1").click(function() {
        doDelete(deleteConfirm_Kendo);
      });
      $("#btnTest2").click(function() {
        doDelete(deleteConfirm_Native);
      });
    });

以上寫法有個小缺點,執行刪除的部分必須抽出來變成函式,分於兩處呼叫。

針對這個缺點,額外加個jQuery.Deferred串場就能克服:線上展示

function doDelete(deleteConfirm) {
var dfd = jQuery.Deferred();
var promise = dfd.promise();
var res = deleteConfirm();
//若函式傳回Promise, 替換原有Promise
if (res && res.hasOwnProperty("done")) 
          promise = res;
//若函式傳回boolean,由傳回結果決定resolve或reject
elseif (res) 
          dfd.resolve();
else
          dfd.reject();
        promise.done(function() {
          alert("刪除")
        });
      }

兩種做法都能讓刪除確認程序通吃window.confirm及jQuery.Deferred,擇一而用即可,以上提供大家參考。

Viewing all 2433 articles
Browse latest View live


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