[Windows Azure] 在雲端執行你的命令列應用程式
最近很常被問的問題,不外乎是怎麼將現有應用程式移轉到 Windows Azure Cloud 上面,但其中有一種類型的問題,就是如果應用程式是非 Web 型的應用程式要如何移轉到雲端,比例最多的就是命令列應用程式 (Command-line application, Console Application),這些應用程式可能是舊有應用程式,也有可能是包裝了某些演算法 (例如 Matlab Application) 的應用程式,這些應用程式有些可能是沒有原始碼,有些也可能是移轉不方便或改寫會耗費太多時日,因此如果能夠將它直接移轉到 Windows Azure 雲端 VM 的話,對應用程式開發廠商來說是很棒的。
現在我可以告訴你,這個答案是肯定的,而且作法早在今年三月就已經有人提出。這次我在交大的課程間,也有不少學生來詢問這方面的問題,原訂要現場實作一支命令列程式給大家看的,但因時間來不及而作罷 … 而且關鍵的部份一直沒有方向,直到今天 Carol 寄來一個連結,令我茅塞頓開啊。
在 Windows Azure VM 中執行 Console Application 是一定可行的,而且作法也很簡單,只要用 Process 類別來啟動 Console Application 即可,但是關鍵的點是如何探知在雲端 VM 中應用程式被放在哪裡,這個變數在 Windows Azure SDK 中並沒有提及,同時它是在 VM 中的環境變數,名稱為 RoleRoot,它會傳回 Role 所在的磁碟機路徑。請注意,VM 中應用程式的路徑並不是在 C:\,而是在 %RoleRoot%\approot\ 中,所以不可以直接把路徑設死 (至少 RoleRoot 不可以設死)。另外,開發人員也需要在應用程式封裝中加入該可執行檔,如此檔案才可以被發布在雲端 VM。
多說無益,來個範例給大家看。
1. 首先,建立一個 console application,我使用的是很簡單的應用程式,這個應用程式可以傳回目前系統時間:
namespace TimeGenerator 
{ 
    class Program 
    { 
        static void Main(string[] args) 
        { 
            Console.Out.Write(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")); 
        } 
    } 
}
2. 建立一個雲端應用程式,並加入一個 Worker Role,我在這裡所建立的是 WCF 應用程式,並且利用 REST API 方式顯露方法,這個服務會啟動前面所建立的應 Console 應用程式,請注意 FileName 的設定,這是執行 Console 應用程式的最重要關鍵程式碼:
using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.IO; 
using System.Linq; 
using System.Text; 
using System.ServiceModel; 
using System.ServiceModel.Activation; 
using System.ServiceModel.Description; 
using System.ServiceModel.Channels; 
using System.ServiceModel.Web; 
using System.Threading; 
using Microsoft.WindowsAzure; 
using Microsoft.WindowsAzure.ServiceRuntime; 
namespace TimeGeneratorService 
{ 
    [ServiceContract] 
    public interface ITimeGenerator 
    { 
        [OperationContract, WebGet] 
        string GetCurrentTime(); 
    } 
    class TimeGeneratorService : ITimeGenerator 
    { 
        public string GetCurrentTime() 
        { 
            Process p = new Process(); 
            p.StartInfo = new ProcessStartInfo() 
            { 
                FileName = Path.Combine(Environment.GetEnvironmentVariable("RoleRoot"), "approot", "TimeGenerator.exe"), 
                RedirectStandardOutput = true, 
                CreateNoWindow = true, 
                UseShellExecute = false 
            }; 
            p.Start(); 
            p.WaitForExit(); 
            string result = p.StandardOutput.ReadToEnd(); 
            p.Close(); 
            return result; 
        } 
    } 
}
3. 將第一步所建立的應用程式加入到 Worker Role 的專案中,方法可以是直接在 Visual Studio 中使用 [加入 > 現有項目] 的方式,或是將執行檔複製到專案目錄,再由 Visual Studio 顯示所有檔案再加入專案的方式,但不論是哪種方式,都要確認該檔案的建置動作 (Build Action) 是內容 (Content),同時也要設定複製到輸出目錄 (Output Folder) 為永遠複製 (Copy always) 或是有更新時複製 (Copy if newer):
4. 在 Worker Role 進入點設定將服務掛載起來:
private WebServiceHost _serviceHost = null;
public override bool OnStart() 
{ 
    // Set the maximum number of concurrent connections 
    ServicePointManager.DefaultConnectionLimit = 12; 
DiagnosticMonitor.Start("DiagnosticsConnectionString");
    // For information on handling configuration changes 
    // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357. 
    RoleEnvironment.Changing += RoleEnvironmentChanging; 
    this._serviceHost = new WebServiceHost( 
        typeof(TimeGeneratorService), new Uri("http://localhost:8080/")); 
    _serviceHost.Open(); 
    return base.OnStart(); 
}
並在 app.config 中加入 WCF 的設定(這裡剛好也是 WCF 4.0 的 Simple Configuration 功能):
<system.serviceModel> 
  <behaviors> 
    <serviceBehaviors> 
      <behavior> 
        <serviceMetadata httpGetEnabled ="true"/> 
      </behavior> 
    </serviceBehaviors> 
  </behaviors> 
  <protocolMapping> 
    <add binding="wsHttpBinding" scheme ="http"/> 
  </protocolMapping> 
</system.serviceModel>
5. 按 F5 啟動 Visual Studio Debugger 以啟動 Development Fabric,並且在瀏覽器中輸入 http://localhost:8080/GetCurrentTime,
瀏覽器回傳的會是由 Console Application 所傳回的時間。
Reference: 
http://social.msdn.microsoft.com/Forums/en-IE/windowsazure/thread/2755d7c8-984a-43d4-9652-f6c82d8c4b2b 
http://social.msdn.microsoft.com/Forums/en/windowsazure/thread/5bc5f411-5940-44ce-ac49-a3eb999bd51b