TomoProgの技術書

底辺プログラマーが達人プログラマーになるまで

C#で自作ロガーを作ってみる

皆さん
こんにちは、こんばんは
TomoProgです。

最近WindowsPCが新しくなり、気分がいいTomoProgです。

以前Pythonで自作ロガーを作成した以下の記事を書きました。
tomoprog.hatenablog.com

今回はC#で自作ロガーを作ってみたので、それについて書いていこうと思います。

それでは頑張っていきましょう。

自作ロガーの仕様を決める

まずは自作ロガーの仕様を決めていきます。
何を出力しようか考えましたが、とりあえず以下の4つを出力することにしました。

  • 日時
  • ファイル名
  • 行番号
  • 任意のログメッセージ

ほぼPythonの自作ロガーと同じです。

ログファイルに出力する内容を取得する

仕様も決まったので、早速作成していきましょう。

日時の取得

まずは日時から取得していきます。
C#で現在の日時を取得する場合はDateTime構造体のNowプロパティを参照すると簡単に取得できます。

string dateTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff");

ToStringメソッドに書式を渡してあげることで、様々な形の日時文字列を作ることができます。

書式については以下のサイトを参考にさせていただきました。
note.phyllo.net

ファイル名の取得

次はファイル名を取得してみます。
呼び出し元のファイル名を取得するにはStackTraceクラスを使用します。

StackTrace st = new StackTrace(1, true);
string fileName = st.GetFrame(0).GetFileName()

StackTraceクラスについてですが、
第1引数に1を指定することで呼び出し元からファイル名や行番号を解析できるようです。
また、第2引数にはファイル名、行番号をキャプチャする場合はtrueを指定します。

なんとなく分かるような分からないような・・・

第1引数に1を指定したため、GetFrameの引数には0(呼び出し元のフレーム)を指定しています。

詳しくは以下のサイトを参考にしてみてください。
StackTrace コンストラクター (Int32, Boolean) (System.Diagnostics)

行番号の取得

最後に行番号です。
とは言っても行番号はファイル名と同じやり方で取得できます。

StackTrace st = new StackTrace(1, true);
string lineNumber = st.GetFrame(0).GetFileLineNumber().ToString();

これで呼び出し元の行番号が取得できます。

Loggerクラスを作成する

欲しい情報はすべて手に入ったので、実際にLoggerクラスを作って呼び出してみます。

Loggerクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Diagnostics;

namespace MyUtility
{
    public class Logger
    {
        /// <summary>
        /// ファイルパス
        /// </summary>
        string _filePath;

        /// <summary>
        /// エンコーディング
        /// </summary>
        Encoding _enc;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="logFilePath">ログファイルパス(ログファイル名込み)</param>
        /// <param name="enc">ログファイルのエンコーディング</param>
        public Logger(string logFilePath, Encoding enc)
        {
            _filePath = logFilePath;
            _enc = enc;
        }
        
        /// <summary>
        /// ログファイル書き込み
        /// </summary>
        /// <param name="writeContents">書き込み内容</param>
        public void Write(string writeContents)
        {
            StackTrace st = new StackTrace(1, true);
            string dateTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff");
            string fileName = Path.GetFileName(st.GetFrame(0).GetFileName());
            string lineNumber = st.GetFrame(0).GetFileLineNumber().ToString();
            //string methodName = st.GetFrame(0).GetMethod().ToString();

            writeContents = (dateTime + " " + fileName + " Line:" + lineNumber + " " + writeContents);

            try
            {
                using (StreamWriter sw = new StreamWriter(_filePath, true, _enc))
                {
                    sw.WriteLine(writeContents);
                }
            }
            catch
            {
                // 握りつぶす
            }
        }
    }
}
呼び出し用Mainクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Diagnostics;
using MyUtility;

namespace ConsoleApp1
{
    class Program
    {
        static Logger l = new Logger("test.txt", Encoding.UTF8);
        static void Main(string[] args)
        {
            l.Write("test文字列");
            method1("method1を呼び出したよ。");
        }
        static void method1(string str)
        {
            l.Write(str);
        }
    }
}
実際に実行した結果
2017/02/26 21:25:04.198 Program.cs Line:17 test文字列
2017/02/26 21:25:04.201 Program.cs Line:22 method1を呼び出したよ。

例外処理などかなり雑な部分はありますが、
ログファイルへ書き込むことができました!!

まとめ

  • 自作ロガーを作成できた
  • StackTraceクラスを使うことで呼び出し元の情報を取得できる

今回はC#で自作ロガーを作成してみました。
実際に使用する際にはログレベルなども必要かもしれませんが、個人で使用する分には十分な機能だと思います。

GitHubにもアップしていますので、使いたい方おられましたらご自由にどうぞ。
github.com


それではまた。

TomoProg

GitHub
TomoProg (TomoProg) · GitHub

Twitter
TomoProg (@tomoprog_xxx) | Twitter

無限 sudo gem install rails にハマりました

皆さん
こんにちは、こんばんは
TomoProgです。

久しぶりに連日の更新です。

業務でRuby on Railsを使用することになりそうなので、
自分のMacRailsをインストールしてみました。

今回はそのときにハマったことをメモしておこうと思います。

それでは頑張っていきましょう。

とりあえずRuby on Railsのインストール

まずは自分の環境にRailsがインストールされているのか確認してみます。

$ rails -v

インストールされていればRailsのバージョンが出力されるのですが、
私の環境では以下のようなメッセージが出ました。

Rails is not currently installed on this system. To get the latest version, simply type:

    $ sudo gem install rails

You can then rerun your "rails" command.

とりあえず、Railsはインストールされてないようです。
というわけで、実際にコマンドを実行してみます。

$ sudo gem install rails

数分後、無事にインストールできたメッセージが表示されました。
(インストール完了メッセージはメモし忘れた・・・)

無限 sudo gem install rails 地獄

インストールの完了メッセージも出たので再度、バージョン確認をしてみます。

$ rails -v

すると・・・

Rails is not currently installed on this system. To get the latest version, simply type:

    $ sudo gem install rails

You can then rerun your "rails" command.

え!?
さっきインストールしたのに!
どういうこと!?

解決方法

こういうときはググってみよう。
ということで、ググってみると以下のサイトを発見しました。
qiita.com

私のMacOS X EI Capitanですが、rbenvでのRubyの環境や
メッセージも同じことから、おそらくこの記事と同じ状況でしょう。

つまり、gemコマンドでrailsをインストールしたのはいいのですが、
rbenvのRubyのgemではなく、Mac本体のRubyのgemが起動され、
railsMac本体にインストールされてしまったということ。

ということで、解決策として書かれている

export PATH="$HOME/.rbenv/shims:$PATH"

を.bash_profileに記述し、再度railsをインストール後、
バージョンを表示してみます。

$ rails -v
Rails 5.0.1

やっとRailsのバージョンが出力されました!

まとめ

  • Railsをインストールする際は、rbenvのRubyMac本体のRubyに注意

今回はRuby on Railsをインストールした際にハマったことをメモしておきました。
Qiitaで記事にしてくださったw7treeさんに感謝です。

それではまた。

TomoProg

GitHub
TomoProg (TomoProg) · GitHub

Twitter
TomoProg (@tomoprog_xxx) | Twitter

もう1年が経ちました

皆さん
こんにちは、こんばんは
90日以上更新がないと広告が出ることに驚いているTomoProgです。

気がつけば2016年が終わり、
2017年も1ヶ月が過ぎましたね。

このブログを始めたのが去年の1月30日。
もうあれから1年経ちました。

結局スタートはいい感じでしたが、
案の定、後半は失速し、最後の方はほぼ更新出来ませんでした。

ただ、あまり更新しなくなった今でも、毎日少しアクセスがあるようで、
こんな記事でも少しでも誰かの役に立っているのであれば嬉しいことです。

1年目の投稿は39記事。
2年目はこれを超えることを目標にしたいと思います。

底辺プログラマーが達人プログラマーになるまで。

それではまた。

TomoProg

GitHub
TomoProg (TomoProg) · GitHub

Twitter
TomoProg (@tomoprog_xxx) | Twitter

自動実装プロパティを読み取り専用にする方法

皆さん
こんにちは、こんばんは
TomoProgです。

気づけば2ヶ月以上更新してなかったですが、
久しぶりに更新していきましょう!

今回はC#のプロパティを読み取り専用にする方法が分かったので、
そのことを書いていこうと思います。

それでは頑張っていきましょう。

【重要】
今回紹介するやり方はC#6.0以前のバージョンでの使用方法です。
C#6.0から読み取り専用の自動実装プロパティを定義出来る構文が追加されています。

プロパティを使ってメンバーにアクセスする

C#にはクラスのメンバーにアクセスしたいとき、
プロパティを介してアクセスするということがよくあります。

public class Human
{
    /// <summary>名前</summary>
    private string _name;
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
        }
    }

    /// <summary>年齢</summary>
    private int _age;
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            if(value > 99)
            {
                value = 99;
            }
            _age = value;
        }
    }
}
class MainClass
{
    public static void Main(string[] args)
    {
        Human h1 = new Human();
        h1.Name = "Yamada";
        h1.Age = 22;

        Human h2 = new Human();
        h2.Name = "Sato";
        h2.Age = 100;

        Console.WriteLine("h1.Name:{0} h1.Age:{1}", h1.Name, h1.Age);
        Console.WriteLine("h2.Name:{0} h2.Age:{1}", h2.Name, h2.Age);
    }
}
実行結果:
h1.Name:Yamada h1.Age:22
h2.Name:Sato h2.Age:99

プロパティである「Name」と「Age」を介してprivate変数へアクセスしています。

プロパティを使うメリットは
値に応じて処理を記述できる
ということです。

今回はAgeプロパティに99より大きい値を入れられた場合は
99に補正するようにsetを定義しています。

このようにget、setの中に処理を記述することで
整合性チェックなどの処理を逐次書かなくても
クラスを安全に使用することができます。

自動実装プロパティを使う

get、setの中に処理を記述するのであればこの書き方は良いのですが、
Nameプロパティのように値を取得、設定するだけの場合は
いちいちget、setを書くのは正直面倒です。

そんなときは自動実装プロパティを使用すると簡単に書くことが出来ます。

/// <summary>
/// 人間クラス
/// </summary>
public class Human
{
    /// <summary>名前/summary>
    public string Name { get; set; }
    
    /// <summary>年齢</summary>
    private int _age;
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            if(value > 99)
            {
                value = 99;
            }
            _age = value;
        }
    }
}

変更された点はNameのプロパティ定義です。
private変数の記述が不要になり、
Nameプロパティだけで先程の例と同じように使用することが出来ます。
(実際には自動的にprivateのメンバーが生成されているらしいですが、そのあたりは今回は触れません。)

自動実装プロパティで読み取り専用にしたい

さて、自動実装プロパティの使い方も分かったところでようやく今回の本題です。

例えばこのクラスに血液型を追加で持たせたとします。

名前や年齢は変わるものですが、血液型は変わらないので、
変更出来ないように読み取り専用にしておきたいです。

というわけで、getだけを持つ血液型プロパティを記述してみたいと思います。

/// <summary>
/// 人間クラス
/// </summary>
public class Human
{
    /// <summary>名前</summary>
    public string Name { get; set; }
    
    /// <summary>血液型</summary>
    public string Blood { get; }
    
    /// <summary>年齢</summary>
    private int _age;
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            if(value > 99)
            {
                value = 99;
            }
            _age = value;
        }
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public Human(string name, int age, string blood)
    {
        Name = name;
        Age = age;
        Blood = blood;
    }
}

こんな感じで血液型プロパティを追記しました。

ついでに人間クラスを作るときに名前、年齢、血液型を
指定できるようにコンストラクタを記述したので、
それに応じてメインクラスも書き換えます。

class MainClass
{
    public static void Main(string[] args)
    {
        Human h1 = new Human("Yamada", 22, "A");
        Human h2 = new Human("Sato", 100, "B");

        Console.WriteLine("h1.Name:{0} h1.Age:{1} h1.Blood:{2}", h1.Name, h1.Age, h1.Blood);
        Console.WriteLine("h2.Name:{0} h2.Age:{1} h2.Blood:{2}", h2.Name, h2.Age, h2.Blood);
    }
}

これで血液型はクラス作成時から変更できない
読み取り専用のプロパティとして定義出来たはず。

しかし、コンパイルしてみると、
Bloodプロパティは読み取り専用なので
クラス内でも代入できないと言われてしまいます。

実はBloodプロパティを以下のように変更するだけで
簡単に読み取り専用に出来てしまいます。

/// <summary>血液型</summary>
public string Blood { get; private set; }

これで実際に実行すると

h1.Name:Yamada h1.Age:22 h1.Blood:A
h2.Name:Sato h2.Age:100 h2.Blood:B

実行結果が表示されました。

メインクラス内でBloodプロパティに設定しようと
以下のようなコードを記述すると、

class MainClass
{
    public static void Main(string[] args)
    {
        Human h1 = new Human("Yamada", 22, "A");
        h1.Blood = "B";
    }
}

コンパイル時にsetアクセサーにアクセスできないと言われるため、
メインクラスからの設定は出来なくなります。

これで自動実装プロパティを読み取り専用にすることが出来ました!!

まとめ

自動実装プロパティは
public <型> <プロパティ名> { get; private set; }
で読み取り専用に出来る。

今回は自動実装プロパティを読み取り専用にする方法を書いていきました。
アクセスレベルを制限することで、安全にクラスを使っていきましょう。

それではまた。

TomoProg

GitHub
TomoProg (TomoProg) · GitHub

Twitter
TomoProg (@tomoprog_xxx) | Twitter

Listから条件に適応するものだけを抽出したい

皆さん
こんにちは、こんばんは
TomoProgです。

気がつけば2ヶ月ほどブログ更新していなかった!!
まぁいつもどおりあまり気にせず、ゆるく更新していきたいと思います。

今回はC#にて、List型から条件に適応するものを抽出するにはどうしたらいいのだろう?
ということで調べてみました。

それでは頑張っていきましょう。

素直にすべて走査して抽出する

Listの中身をすべて走査して、条件に合うものを抽出していきます。

using System;
using System.Collections.Generic;

namespace console
{
    class Student
    {
        public string name { get; }
        public int score { get; }
        public Student(string name, int score)
        {
            this.name = name;
            this.score = score;
        }
    }

    class MainClass
    {
        public static void Main(string[] args)
        {
            List<Student> studentList = new List<Student>();

            // リストに生徒を追加
            studentList.Add(new Student("Tanaka", 60));
            studentList.Add(new Student("Sato", 70));
            studentList.Add(new Student("Aoki", 60));

            // 70点未満の生徒のリストを作成
            List<Student> selectedList = new List<Student>();
            foreach (Student s in studentList)
            {
                if (s.score < 70)
                {
                    selectedList.Add(s);
                }
            }

            // 作成したリストの内容を出力
            foreach (Student s in selectedList)
            {
                Console.WriteLine("Name:{0} Score:{1}"
                  , s.name, s.score);
            }
        }
    }
}
実行結果:
Name:Tanaka Score:60
Name:Aoki Score:60

この方法だと、どのプログラミング言語でも出来そうですが、
せっかくC#を使っているので、C#っぽい書き方をしてみます。

Linqを使って抽出する

Listから条件に適応するものだけを抽出する際は、
LinqというC#独自の技術を使うと
先ほど書いたプログラムよりも圧倒的にキレイに書くことが出来ます。

using System;
using System.Collections.Generic;
using System.Linq;

namespace console
{
    class Student
    {
        public string name { get; }
        public int score { get; }
        public Student(string name, int score)
        {
            this.name = name;
            this.score = score;
        }
    }

    class MainClass
    {
        public static void Main(string[] args)
        {
            List<Student> studentList = new List<Student>();

            // リストに生徒を追加
            studentList.Add(new Student("Tanaka", 60));
            studentList.Add(new Student("Sato", 70));
            studentList.Add(new Student("Aoki", 60));

            // 70点未満の生徒のリストを作成
            var selectedList = from x in studentList
                               where x.score < 70
                               select x;

            // 作成したリストの内容を出力
            foreach (Student s in selectedList)
            {
                Console.WriteLine("Name:{0} Score:{1}"
                  , s.name, s.score);
            }
        }
    }
}
実行結果:
Name:Tanaka Score:60
Name:Aoki Score:60

70点未満の生徒のリストを作成するところが先ほどのプログラムと違うことがわかると思います。

Linqを使えばSQL文を発行するようにList型から条件に適応するものだけを抽出することが出来ます。

何事にも慣れは必要だと思いますが、
C#を使うのであればLinqを使って書いたほうが綺麗ですね。

Linqについて詳しく知りたい場合はこちらのサイトが良いと思います。
ufcpp.net

まとめ

今日はListから条件に適応するものだけを抽出するということで、
2つの方法を紹介しました。

どちらの方法でも結果は同じですが、
Linqを使った方が綺麗にコードが書けて可読性も高まると感じました。
C#使うときは積極的に使っていこうと思います。

それではまた。

TomoProg

GitHub
TomoProg (TomoProg) · GitHub

Twitter
TomoProg (@tomoprog_xxx) | Twitter

C#で見たことが無い型に遭遇した

皆さん
こんにちは、こんばんは
TomoProgです。

気がつけばもうすぐ6月も終わりですね。
今年も半分が過ぎようとしていると思うと早いものです。

最近ブログは週一ぐらいが自分のペースに合ってるなと思ってきました。
ですので、それぐらいのペースで更新していけたらなと思います。

それはさておき、
今回はC#で見たことの無い型を見つけたので、そのことについて書いていこうと思います。

それでは頑張っていきましょう。

戻り値の型がint?ってどういうこと!?

Xamarinを勉強するためにC#で書かれたサンプルコードを眺めていると以下の様なコードに遭遇しました。

static readonly string[] digits = {
    "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ"
};

static int? TranslateToNumber(char c)
{
    for (int i = 0; i < digits.Length; i++)
    {
        if (digits[i].Contains(c))
            return 2 + i;
    }
    return null;
}

なんじゃこりゃ!?
戻り値の型がint?ってどういうこと!?
と心の中で叫んだ私はとりあえず調べてみることにしました。

?を付けるとnullを許容できる

調べてみると以下のサイトに辿り着きました。
メモ帳: [C#] int?

なるほど。
?を付けるとその型の範囲値だけでなくnullを許容できるようになるらしいです。

int?型を使ってみる

本当にnullを許容できるようになるのか実際にコードを書いてみます。

using System;

namespace console
{
    class MainClass
    {
        public static void Main (string [] args)
        {
            int val = null;
            if(val == null)
            {
                Console.WriteLine("val is null");
            }
            else
            {
                Console.WriteLine("val is {0}", val);
            }
        }
    }
}

valがnullであれば"val is null"が表示されるはずですが、
コンパイルしてみると・・・

Error CS0037: Cannot convert null to `int' because it is a value type

nullからint型へ変換できないと言われてしまいました。

次にintに?を付けてみます。

using System;

namespace console
{
    class MainClass
    {
        public static void Main (string [] args)
        {
            int? val = null;
            if(val == null)
            {
                Console.WriteLine("val is null");
            }
            else
            {
                Console.WriteLine("val is {0}", val);
            }
        }
    }
}

これを実行すると、

val is null

nullを変数に入れる事ができ、正しく表示出来るようになりました!!

int?型を関数の戻り値として使ってみる

関数の戻り値でもint?型を使ってみたいと思います。

using System;

namespace console
{
    class MainClass
    {
        public static void Main (string [] args)
        {
            string str = string.Empty;
            char c = 'z';
            str = "123456abcdef";

            int? index = SearchIndex(str, c);
            if(index == null)
            {
                Console.WriteLine("'{0}' is not found", c);
            }
            else
            {
                Console.WriteLine("Index of '{0}' is {1}", c, index);
            }
        }

        private static int? SearchIndex(string str, char target_chara)
        {
            for (int i = 0; i < str.Length; i++)
            {
                if (str[i] == target_chara)
                {
                    return i;
                }
            }
            return null;
        }
    }
}
実行結果:
'z' is not found

文字列の中に対象文字が存在する場合はその位置を、存在しない場合はnullを返す関数を定義しています。
int?型を戻り値に指定することで、戻り値にnullを指定することが出来ました!!

まとめ

  • int型に?を付けるとintの範囲値だけでなく、nullも許容できるようになる

intだけでなくdouble、float、char、boolなどに使用出来るみたいです。
初めて見た型だったので少しビビりましたが、nullが使えるようになると覚えておけばとりあえず大丈夫だと思ってます。

それではまた。

TomoProg

GitHub
TomoProg (TomoProg) · GitHub

Twitter
TomoProg (@tomoprog_xxx) | Twitter

Azureを使ったXamarin.FormsアプリをiPhoneにインストールしてみた Part2

皆さん
こんにちは、こんばんは
TomoProgです。

Azureを使ったXamarin.FormsアプリをiPhoneにインストールしてみたPart2です。

前回でアプリのダウンロードまで終わったので、今回は実際に実機にインストールしてみたいと思います。

それでは頑張っていきましょう。

ダウンロードしたアプリをXamarin Studioで開く

Downloadボタンを押してアプリをダウンロードしたら、フォルダを開き、ソリューションファイル(.sln)をクリックします。f:id:TomoProg:20160622222154j:plain

ソリューションファイルを開くと自動的にXamarin Studioが開きます。
iPhoneをPCと接続し、左上のDebugモード、Releaseモードを選べるところからRelease | iPhoneを選択します。
選択したらその横にある実行ボタンを押します。
f:id:TomoProg:20160622224347j:plain

ビルドに失敗する・・・

実機で動かす時が来たと言いたいところですが、実はこのまま実行すると・・・
f:id:TomoProg:20160622225907j:plain

Error: No installed provisioning profiles match the installed iOS signing identities.
プロビジョニングプロファイルがインストールされていないというエラーが表示され、実機にインストールが出来ません。

私自身iPhoneアプリに詳しくは無いので、プロビジョニングファイルの詳しい説明は出来ませんが、検索したところアプリケーションのメタ情報や作成者の署名などを書いておくファイルらしいです。

というわけで、プロビジョニングファイルを作成していきます。

プロビジョニングファイルを作成する

プロビジョニングファイルを作成するにはXcodeを使います。
Xcodeを開いたらFile->New->Projectを選択し、新しいプロジェクトを作成します。
プロジェクトはiOS用のプロジェクトであればおそらく何でも構いません。
適当にプロジェクトを作成したら、Bundle Identifierに書かれている内容を確認します。
f:id:TomoProg:20160622234843j:plain

次にXamarin Studioに戻り、Info.plistを開きます。
Info.plistのBundle Identifierを先ほどXcodeで確認したBundle Identifierに書き換えます。
f:id:TomoProg:20160622235515j:plain

これで実機にインストールする環境が整いました!!

実機でアプリを使ってみる

インストールする準備が出来たので、早速実行してみます。
実行すると接続しているiPhoneにインストールされます。
f:id:TomoProg:20160623001450j:plain

アプリを開くとToDoリストアプリが開きます。
f:id:TomoProg:20160623001813j:plain

実際に何かToDoリストに登録してみます。
Item nameにやることを入力して横の「+」ボタンを押すと登録出来ます。
f:id:TomoProg:20160623001952j:plain

AzureでToDoリストのテーブル内容を確認する

登録が出来たので、Azureからテーブルを見てみようと思います。
Azureにログインし、
App Service->作成したアプリ名->設定->Easy Table->TodoItem
を選択します。
選択すると先ほど登録したToDoリストが登録されていることが確認できます。
f:id:TomoProg:20160623003403j:plain

まとめ

今回は2回に分けてAzureを使ったXamarin.FormsアプリをiPhoneにインストールしてみました。
Azureを使うことでバックエンドをほぼ意識せず構築出来ました。
Xamarinを使うときはAzureの選択はありだと思います。

Azureの使い方も少し分かったところで、これからは自作アプリ作りに励もうと思います。

それではまた。

TomoProg

GitHub
TomoProg (TomoProg) · GitHub

Twitter
TomoProg (@tomoprog_xxx) | Twitter