C# ile Uygulama Kurtarma

   En hatasız yazılan uygulamalar bile zaman zaman hata vererek sonlanabilir, bu hepimizin bildiği bir gerçektir. Uygulamalarımızda bir hata tespit ettiğimizde yapılacak en doğru şey, şüphesiz ki, bu hatayı düzelmek olacaktır. Peki biz bunu yapana kadar son kullanıcılar ne yaşıyorlar? Düşünün bir kere; bilgisayarınıza bir uygulama kurdunuz, pek çok işlem yaptınız; fakat işlerinizi kaydetmeden hemen önce programda hata oluştu ve windows’un uygulamayı sonlandırma penceresi karşımıza geldi… Normalde bu durum son kullanıcı açısından tam bir kabus olacaktır, saatlerdir yaptığı her şey kaybolacak… Peki size bunun bir çözümü olduğunu söylesem 😉

   Windows geliştiricileri bu sıkıntının farkına vararak Windows Vista ve üstü işletim sistemlerinde uygulamaların bu gibi hatalarda durum bilgilerini saklamak/kurtarabilmek adına bir API geliştirmişler; Application Recovery API.

   Bu API, geliştiricilere uygulamalarının bir hata nedeniyle sonlanacağını bilme ve kritik bilgileri saklayabilme şansı vermekte. Dolayısıyla da son kullanıcı açısından, deyin yerindeyse, daha iyi bir uygulama hata deneyimi sunmakta. Eğer Office uygulamalarının güncel sürümlerini kullanıyorsanız bunu fark etme olasılığınız oldukça yüksek. Hatta, Visual Studio ile çalışıyorsanız (ki sitemi takip ediyorsanız öyle olduğunuzu düşünmem kaçınılmazdır) bunu pek çok kereler görmüşsünüzdür; Visual Studio’nun çalışması hata nedeniyle sonlanması durumunda Windows yeniden başlatmak isteyip istemediğimizi sormakta ve olumlu yanıtta da yeniden başlatmakta. Üstelik bunu yaparken de hata öncesi açık olan proje, kurtarma verisi ile birlikte, yeniden açılmakta. Benzer bir işleve kendi geliştirdiğimiz uygulamalarımızın da sahip olması ne kadar güzel olurdu değil mi! Örneğin; kullanıcı uygulamamızda login olmuş ve  x,y,z ekranlarını açmış. Ardından aldığı hata sonrasında uygulama yeniden başlatıldığında yeniden login olmaya ve ekranları açmaya gerek kalmadan uygulama bu işlemleri otomatik gerçekleştirse!!?? İşte başarılı bir uygulama, yüksek kullanıcı deneyimi diye buna denir, değil mi! Bu makalemde işte tam da bu konuya değiniyor olacağım; hata anında uygulamalarımızda nasıl daha iyi bir kullanıcı deneyimi sunabileceğimizi anlatıyor olacağım.

   Örnek bir uygulama üzerinden anlatıyor olmak, sanırım size konuyu daha rahat anlatabilmemi sağlayacaktır. Olabildiğince konuya odaklanabilmek adına bir Windows Forms uygulaması yerine bir konsol uygulamasını ele alalım;

using System;
using System.Threading;

namespace UygulamaKurtarma {
    public class Program {
        private static string kullaniciAdi;
        private static string sifre;

        static void Main(string[] args) {
            var kullaniciBilgileriDogru = false;

            while (!kullaniciBilgileriDogru) {
                Console.Write("Lütfen kullanıcı adını giriniz : ");
                kullaniciAdi = Console.ReadLine();

                Console.Write("Lütfen şifrenizi giriniz : ");
                sifre = Console.ReadLine();

                kullaniciBilgileriDogru = KullaniciBilgileriDogruMu(kullaniciAdi, sifre);
            }

            Console.WriteLine();
            Console.WriteLine("===============================");
            Console.WriteLine("Uygulamaya hoş geldin, " + kullaniciAdi);

            Thread.Sleep(60000);

            throw new Exception("Hiç beklenmedik bir hata");
        }

        private static bool KullaniciBilgileriDogruMu(string kullaniciAdi, string sifre) {
            return kullaniciAdi == "fatih" && sifre == "1234";
        }
    }
}

   Yukarıdaki örnek konsol uygulaması kullanıcı adı ve şifre bilgilerini alarak kontrol etmekte, bilgilerin doğru olması durumunda kullanıcıyı karşılamakta. örnek iş mantığını temsilen uygulama 60 saniye boyunca bekletilmekte, ardında da bir hata oluşmakta. Bu uygulamayı Visual Studio dışından çalıştırıp kullanıcı adı olarak fatih, şifre olarak da 1234 girdiğimizde 60 saniye sonra aşağıdaki ekranla karşı karşıya kalırız;

uygulama_hatasi

   Windows hatayı fark etmekte ve hata hakkında bilgi toplamakta. Kısa bir bekleyiş ardından hata ayıklama ya da programı kapatma seçenekleri karşımıza gelecektir;

uygulama_hatasi2

   Bu durumda, elimizde kaynak kodlar yoksa (ki son kullanıcıda olmayacağı kesin) programı kapatmak dışında da bir seçeneğimiz kalmıyor. Uygulamayı yeniden başlattığımızda, hata öncesinde kaldığımız noktaya yeniden gelebilmek için yapabileceğimiz tek şey bu bilgileri yeniden vermek olacaktır.

   Bu noktadan sonra; uygulamamıza kurtarma desteği sunabilmek adına Application Recovery API’sini kullanabiliriz. Kernel32.dll altında yer alan bu API fonksiyonlarına aşağıdaki şekilde C# içerisinden ulaşabiliriz;

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
static extern uint RegisterApplicationRestart(string pwzCommandLine, RestartFlags dwFlags);

[DllImport("kernel32.dll")]
static extern uint RegisterApplicationRecoveryCallback(APPLICATION_RECOVERY_CALLBACK pRecoveryCallback, object pvParameter, int dwPingInterval, int dwFlags);

[DllImport("kernel32.dll")]
static extern uint ApplicationRecoveryInProgress(out bool pbCancelled);

[DllImport("kernel32.dll")]
static extern uint ApplicationRecoveryFinished(bool bSuccess);

[Flags]
enum RestartFlags {
    NONE = 0,
    RESTART_CYCLICAL = 1,
    RESTART_NOTIFY_SOLUTION = 2,
    RESTART_NOTIFY_FAULT = 4,
    RESTART_NO_CRASH = 8,
    RESTART_NO_HANG = 16,
    RESTART_NO_PATCH = 32,
    RESTART_NO_REBOOT = 64
}

delegate int APPLICATION_RECOVERY_CALLBACK(object pvParameter);

   API’yi kullanırken yapmamız gereken ilk iş Windows’a uygulamamızın hata durumunda yeniden başlatılmayı istediğini belirtmektir. Bunun için API içerisinde yer alan RegisterApplicationRestart fonksiyonunu kullanabiliriz. Ardından hata anında kurtarma bilgisini saklayabileceğimiz için bu durumu bize bildirecekleri bir geri bildirim fonksiyonu belirtmeliyiz; ki bunu da RegisterApplicationRecoveryCallback yardımıyla yapabiliriz. Bu geri bildirim sırasında kurtarma verisini oluşturmak dışında açılmış olan işletim sistemleri kaynaklarının kapatılması da (örneğin açık dosyaları kapatmak) gerçekleştirilebilir. Kurtarma işlemi sırasında da uygulamanın kilitlenebileceğini ve bir death lock oluşabileceğini hesaba katan işletim sistemi geliştiricileri, bu durumu engellemek adına, belirli aralarla uygulamanın yanıt verip vermediğini kontrol etmekte ve yanıt vermemesi durumunda uygulamayı sonlandırmaktadır. Uygulama belirli aralıklarla yanıt verebildiğini kanıtlayabilmek adına ApplicationRecoveryInProgress fonksiyonu yardımıyla kurtarma işlemini devam ettiğini belirtmelidir. Tüm süreç sona ererek gerekli kurtarma işlemlerinin bittiği işletim sistemine ApplicationRecoveryFinished fonksiyonu yardımıyla iletilmelidir. Herhangi bir sebeple, uygulamamızın kurtarma desteğinin kalktığının işletim sistemine belirtmek için ise UnregisterApplicationRestart fonksiyonu kullanılmalıdır.

   RegisterApplicationRestart fonksiyonu bizden iki parametre isteyecektir;

  • pwzCommandLine : Hata sonrası uygulama yeniden başlatılırken uygulamaya geçilmesi istenen komut satırı parametreleri. Bu parametreler uygulama normal şekilde başlatılırken verilmiş olanlar olabileceği gibi uygulamanın bir hata sonrası yeniden başlatıldığını belirten bir parametre de olabilir.
  • dwFlags : Yeniden başlatma tercihini belirtir.

   Bu bilgileri uygulamamıza uygulayacak olursak Main fonksiyonumuz aşağıdaki şekilde olacaktır;

private const string HATA_SONRASI_BASLATMA = "hataSonrasi";

static void Main(string[] args) {
    var kullaniciBilgileriDogru = false;

    if (args.Length > 0 && args[0] == HATA_SONRASI_BASLATMA) {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Uygulama Hata sonrası yeniden başlatıldı!");
        Console.ResetColor();

        bilgileriKurtar();

        kullaniciBilgileriDogru = KullaniciBilgileriDogruMu(kullaniciAdi, sifre);
    }

    uint islemSonucu = RegisterApplicationRestart(HATA_SONRASI_BASLATMA, RestartFlags.NONE);
    if (islemSonucu == 0) {
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.WriteLine("Uygulama hata kurtarma kaydı yapıldı...");
        Console.ResetColor();

        islemSonucu = RegisterApplicationRecoveryCallback(HataVeriKurtarma, null, 50000, 0);

        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.WriteLine("Uygulama hata kurtarma geri bildirim kaydı yapıldı...");
        Console.ResetColor();
    }
    else {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Uygulama hata kurtarma kaydı yapılamadı...");
        Console.ResetColor();
    }

    while (!kullaniciBilgileriDogru) {
        Console.Write("Lütfen kullanıcı adını giriniz : ");
        kullaniciAdi = Console.ReadLine();

        Console.Write("Lütfen şifrenizi giriniz : ");
        sifre = Console.ReadLine();

        kullaniciBilgileriDogru = KullaniciBilgileriDogruMu(kullaniciAdi, sifre);
    }

    Console.WriteLine();
    Console.WriteLine("===============================");
    Console.WriteLine("Uygulamaya hoş geldin, " + kullaniciAdi);

    Thread.Sleep(60000);

    throw new Exception("Hiç beklenmedik bir hata");
}

   Görüldüğü gibi uygulamamızın hemen başında uygulamanın bir hata sonrası yeniden başlatılıp başlatılmadığı kontrol edilmekte. Bunu belirlemek için de uygulamaya geçilen bir parametre (HATA_SONRASI_BASLATMA) kullanılmakta. Eğer uygulama hata sonrası yeniden başlatıldı ise veriler bilgileriKurtar fonksiyonu yardımıyla kurtarılmakta. Bu kontrol sonrasında ise RegisterApplicationRestart fonksiyonu yardımıyla işletim sistemine uygulamanın hatalarda kurtarma verisi için işlem yapacağını belirtecektir.  Devamında RegisterApplicationRecoveryCallback çağrısı yapılarak hata durumunda tetiklenecek olan fonksiyon belirtilmekte.

RegisterApplicationRecoveryCallback fonksiyonu bizden dört parametre isteyecektir;

  • pRecoveryCallback : Hata durumunda tetiklenecek olan fonksiyonu belirtir
  • pvParameter : Hata durumunda tetiklenecek olan fonksiyona geçilmek istenen bir değer varsa bu parametre yardımıyla belirtilebilir.
  • dwPingInterval : İşletim sisteminin hangi sıklıkla veri kurtarma işleminin devam edip etmediğini kontrol edeceğini belirtir, milisaniye olarak verilir. Varsayılan olarak 5 saniye olan bu parametre, maksimum da 5 dakika olabilir.
  • dwFlags : İleriye dönük kullanımlar için ayrılmıştır, 0 değeri verilmelidir.

   Aşağıda, hata olması durumunda işletim sistemince çağırılacak olan HataVeriKurtarma fonksiyonunu bulabilirsiniz;

private static int HataVeriKurtarma(object parametre) {
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine("Uygulama verileri kurtarılmak üzere saklanıyor ... ");
    Console.ResetColor();

    var timer = new Timer(CanliTut, null, 1000, 1000);

    //Kritik veriler saklanıyor
    using (var kurtarmaVeriDosyasi = File.CreateText(KURTARMA_VERI_DOSYASI_ADI)) {
        kurtarmaVeriDosyasi.WriteLine(kullaniciAdi);
        kurtarmaVeriDosyasi.WriteLine(sifre);
    }

    File.Encrypt(KURTARMA_VERI_DOSYASI_ADI);

    Thread.Sleep(20000);

    ApplicationRecoveryFinished(true);

    return 0;
}

   Buradaki en kritik nokta veri kurtarmanın bitmesi sonrasında ApplicationRecoveryFinished fonksiyonunun çağrılarak işletim sistemine işlemin bittiğinin haber verilmesidir. Hatırlarsanız veri kurtarma işlemi sırasında belirli aralıklarla işlemin devam ettiğinin işletim sistemine bildirilmesi gerektiğinden bahsetmiştim. Yukarıdaki fonksiyonda bu işlem, oluşturulan timer nesnesi üzerinden yapılmaktadır. Timer belirli aralıklarla içeriği aşağıda verilen  CanliTut fonksiyonunu tetiklemektedir.

private static void CanliTut(object parametre) {
    bool islemiIptalEt;
    ApplicationRecoveryInProgress(out islemiIptalEt);

    if (islemiIptalEt) {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Hata veri kurtarma işlemi iptal edildi");
        Console.ResetColor();

        Environment.FailFast("Hata veri kurtarma işlemi iptal edildi");
    }
}

   Dikkat edecek olursanız bu fonksiyon içerisinde işletim sistemine ApplicationRecoveryInProgress fonksiyonu yardımıyla işlemin sürdü bildirilmekte. Eğer bu işlem sırasında kullanıcı arayüz üzerinden işlemi iptal edecek olursa, bu bilgi geri bildirimde uygulamaya iletilmektedir.Uygulama, yukarıda islemiIptalEt parametresinde olduğu gibi dönen değere göre işleme devam etmeli ya da sonlandırmalıdır.

   Tüm bu adımlar sonrasında uygulamamızı çalıştırdığımızda bizi aşağıdaki ekran karşılayacaktır;

hata_kurtarma_oncesi

    Bu ekranda bir süre bekledikten sonra uygulamamızda hata oluşacak ve Windows’un hata kurtarma ekranı karşımıza çıkacak. Bu sırada Windows uygulamamız HataVeriKurtarma fonksiyonunu çağıracak ve gerekli işlemlerin tamamlanması beklenecektir. Kullanıcının bu ekranda iptal butonuna basması durumunda ise ApplicationRecoveryInProgress çağrısında false değeri uygulamaya iletilecek ve uygulamanın hata kurtarma işlemini sonlandırması beklenecektir.

hata_veri_kurtarma

   Başarılı bir hata kurtarma sonrasında, Windows uygulamamızı daha önceden belirttiğimiz HATA_SONRASI_BASLATMA parametresi ile yeniden başlatacaktır. Bu noktadan sonra uygulamamız bir hatadan kurtarıldığını fark ederek gerekli işlemleri yapacaktır.

hata_kurtarma_sonrasi

   En basit şekliyle sizlere anlatmış olduğum hata veri kurtarma süresi şüphesiz ki gerçek uygulamalarda daha karmaşık olacaktır; ama takip edilecek mantık temelde aynı olacaktır. Her ne kadar örneğimizde kurtarılması gereken verileri bir text dosyasında sakladıysakta, gerçek hayatta bir veritabanı ya da registery’nin de kullanılması mümkündür. Aşağıda, parça parça aktardığım bu uygulamanın son halini bulabilirsiniz;

using System;
using System.Threading;
using System.Runtime.InteropServices;
using System.IO;

namespace UygulamaKurtarma {
    public class Program {
        #region Data Members
        private static string kullaniciAdi;
        private static string sifre;

        private const string HATA_SONRASI_BASLATMA = "hataSonrasi";
        private const string KURTARMA_VERI_DOSYASI_ADI = "kurtarmaVerisi.txt";
        #endregion

        #region Application Recovery API
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        static extern uint RegisterApplicationRestart(string pwzCommandLine, RestartFlags dwFlags);

        [DllImport("kernel32.dll")]
        static extern uint RegisterApplicationRecoveryCallback(APPLICATION_RECOVERY_CALLBACK pRecoveryCallback, object pvParameter, int dwPingInterval, int dwFlags);

        [DllImport("kernel32.dll")]
        static extern uint ApplicationRecoveryInProgress(out bool pbCancelled);

        [DllImport("kernel32.dll")]
        static extern uint ApplicationRecoveryFinished(bool bSuccess);

        [Flags]
        enum RestartFlags {
            NONE = 0,
            RESTART_CYCLICAL = 1,
            RESTART_NOTIFY_SOLUTION = 2,
            RESTART_NOTIFY_FAULT = 4,
            RESTART_NO_CRASH = 8,
            RESTART_NO_HANG = 16,
            RESTART_NO_PATCH = 32,
            RESTART_NO_REBOOT = 64
        }

        delegate int APPLICATION_RECOVERY_CALLBACK(object pvParameter);
        #endregion

        static void Main(string[] args) {
            var kullaniciBilgileriDogru = false;

            if (args.Length > 0 && args[0] == HATA_SONRASI_BASLATMA) {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("Uygulama Hata sonrası yeniden başlatıldı!");
                Console.ResetColor();

                bilgileriKurtar();

                kullaniciBilgileriDogru = KullaniciBilgileriDogruMu(kullaniciAdi, sifre);
            }

            uint islemSonucu = RegisterApplicationRestart(HATA_SONRASI_BASLATMA, RestartFlags.NONE);
            if (islemSonucu == 0) {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Uygulama hata kurtarma kaydı yapıldı...");
                Console.ResetColor();

                islemSonucu = RegisterApplicationRecoveryCallback(HataVeriKurtarma, null, 50000, 0);

                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Uygulama hata kurtarma geri bildirim kaydı yapıldı...");
                Console.ResetColor();
            }
            else {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("Uygulama hata kurtarma kaydı yapılamadı...");
                Console.ResetColor();
            }

            while (!kullaniciBilgileriDogru) {
                Console.Write("Lütfen kullanıcı adını giriniz : ");
                kullaniciAdi = Console.ReadLine();

                Console.Write("Lütfen şifrenizi giriniz : ");
                sifre = Console.ReadLine();

                kullaniciBilgileriDogru = KullaniciBilgileriDogruMu(kullaniciAdi, sifre);
            }

            Console.WriteLine();
            Console.WriteLine("===============================");
            Console.WriteLine("Uygulamaya hoş geldin, " + kullaniciAdi);

            Thread.Sleep(60000);

            throw new Exception("Hiç beklenmedik bir hata");
        }

        private static bool KullaniciBilgileriDogruMu(string kullaniciAdi, string sifre) {
            return kullaniciAdi == "fatih" && sifre == "1234";
        }

        private static void bilgileriKurtar() {
            //Kritik veriler geri okunuyor
            File.Decrypt(KURTARMA_VERI_DOSYASI_ADI);
            using (var kurtarmaVeriDosyasi = File.OpenText(KURTARMA_VERI_DOSYASI_ADI)) {
                kullaniciAdi = kurtarmaVeriDosyasi.ReadLine();
                sifre = kurtarmaVeriDosyasi.ReadLine();
            }

            //veriler kurtarıldı, dosya silinebilir
            File.Delete(KURTARMA_VERI_DOSYASI_ADI);
        }

        private static int HataVeriKurtarma(object parametre) {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Uygulama verileri kurtarılmak üzere saklanıyor ... ");
            Console.ResetColor();

            var timer = new Timer(CanliTut, null, 1000, 1000);

            //Kritik veriler saklanıyor
            using (var kurtarmaVeriDosyasi = File.CreateText(KURTARMA_VERI_DOSYASI_ADI)) {
                kurtarmaVeriDosyasi.WriteLine(kullaniciAdi);
                kurtarmaVeriDosyasi.WriteLine(sifre);
            }

            File.Encrypt(KURTARMA_VERI_DOSYASI_ADI);

            Thread.Sleep(20000);

            ApplicationRecoveryFinished(true);

            return 0;
        }

        private static void CanliTut(object parametre) {
            bool islemiIptalEt;
            ApplicationRecoveryInProgress(out islemiIptalEt);

            if (islemiIptalEt) {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("Hata veri kurtarma işlemi iptal edildi");
                Console.ResetColor();

                Environment.FailFast("Hata veri kurtarma işlemi iptal edildi");
            }
        }
    }
}

Fatih Boy

Ankara'da yaşayan Fatih, bir kamu kurumunda danışman olarak çalışmaktadır. ALM süreçleri, kurumsal veri yolu sistemleri, kurumsal altyapı ve yazılım geliştirme konularında destek vermektedir. Boş zamanlarında açık kaynak kodlu projeler geliştirmeyi ve bilgisini yazdığı makalelerle paylaşmayı seven Fatih, aynı zamanda Visual C# ve Visual Studio teknolojileri konusundan Microsoft tarafından altı yıl üst üste MVP (En Değerli Profesyonel) ödülüne layık görülmüştür. İş hayatı boyunca masaüstü uygulamaları, web teknolojileri, akıllı istemciler gibi konularda Asp.Net, Php, C#, Java programlama dilleri ve MySql, MsSql ve Oracle gibi veritabanı yönetim yazılımları ile çalışmıştır. İngilizce ve Türkçe olarak yayınlanan makalelerini gerek İngilizce bloğunda, gerekse de Türkçe bloğunda bulabileceğiniz gibi web sitesinden de açık kaynak kodlu geliştirdiği yazılımlarına ulaşabilirsiniz. vCard - Twitter - Facebook - Google+

1 Yorum

  1. zkaraeli   •  

    gerçekten yararlı olucak bilgiler. Paylaşım için teşekkürler

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir