2020年9月18日金曜日

Pixel3aをAndroid11へアップデートしたらモバイルネットワークが頻繁に切れる件

Android11へアップデートしたことをきっかけに、Pixel3aのモバイル通信がとんでもなく不安定になってしまった(´・ω・`)

5秒おきくらいの頻度で、繋がったり切れたりを繰り返し、ろくに調べ物もできない。


探したら似たような症状の方が。この方はAndroid10で発症。

pixel3aをAndroid10にアップデート - https://fan.uqwimax.jp/topics/detail/1155

pixel3a(simフリー版)をAndroid10にアップデートした途端に UQのAPN設定が飛びました…(´・ω・`) 手打ちで再設定して復旧しましたが pixel3aユーザーの方は御注意の程を



APNの設定を見直したら、なんと全く契約していないOCNなんかに接続設定が変わってる〜!
APN設定が初期化された?



契約先のAPNへ設定を選び直したら、復活!

Andorid9から10へアプデした際は起こらなかったが、10から11へ上げた今回は発生。
何がトリガーとなるのかは、全く不明 ┐(´~`)┌

2016年3月18日金曜日

Google App Script で「getActiveForm を呼び出す権限がありません(You do not have permission to call getActiveForm)」エラーが出たら

Googleフォームで、フォーム送信者+特定のメールアドレスへ自動返信メールを送る処理が、なぜか失敗していた。

その操作を実行するには承認が必要です。

何かしら権限が足りずにエラーが発生しているようだが、これだけでは情報が足りない。



まずは、エラーが発生した際に、すぐにエラー内容を送信するように、トリガーを追加。


スクリプトエディタ > リソース > 現在のプロジェクトのトリガー




通知 > 起動失敗の通知







これでエラー時にメールが送られるようになったが、今度はエラー内容が、

getActiveForm を呼び出す権限がありません

へ変わっている。


あまり情報が無かったが、StackOverFlowで質問していた人がチラホラ見つかった。

Google App Script Error: You do not have permission to call getActiveForm? - Stack Overflow


コードへ有無を言わさず、getActiveForm() を実行する一文を挿入して、権限を尋ねるダイアログを出させるしかないようだ。

function submitForm(e){

//getActiveForm を呼び出す権限がありません」を解決するためだけの一文
//パーミッションを与えた後は不要
FormApp.getActiveForm();

....
}


FormApp変数は宣言不要。上記コードを含めるだけでいい。

デバッグを起動して、権限を尋ねるダイアログを表示させる。





自動返信メールがキター

2016年2月3日水曜日

Git - 特定ファイルのコミットを取り消してまるっと前の状態へ戻したいとき

前回コミットした中に含まれていた、ある特定のファイルについて、それだけまるっと前のバージョンへ戻したいなと思ったら。


1. そのファイルのコミット履歴を確認
git log -p [ファイルパス]


差分などが表示される詳細なログのうち、戻したいバージョンのコミット番号だけ確認してコピーしておく。


2. コミット番号を指定して、そのファイルを指定したコミット時点まで戻す
git checkout [コミット番号] [ファイルパス]

2016年1月21日木曜日

iMacのPCリサイクルマークシールはどこにある?

iMac本体にはPCリサイクルマークシールが付いていない。”購入時に同梱”されているらしい。いざ回収にお願いしようと思って、「どこにあるんだ〜」となったので備忘録。


結論。

「キーボードケースの中の取説の束の中に有りました!」



右側「Hello」の言葉が載っている冊子の束の中に発見。





回収の申し込みにはリサイクルマークのシールに付いているIDが必要で、それが無いと有料回収になります。再発行もできないので、こういうケースは捨ててはなりませぬな。

2015年12月17日木曜日

Android Wear で Notification をすぐに表示するために必要なこと

スマホで表示される通知が Android Wear でどのように表示されるのかを確認したいのに、なぜか Wear で表示されないという状況に嵌ったので、その解決メモ。

環境

  • Handheld:Nexus 5(OS:6.0.1 Marshmallow)
  • Wearable:Sony SmartWatch 3(OS:5.1.1)
    • 通知表示:すべて
    • シアターモード:OFF

※Nexus 5 と SmartWatch 3 は接続確立済み


接続が確立されていたら、スマホ側で表示された通知は Wear 側でも表示されるはず。それがスマホでは表示されているのに Wear は無反応。


解決

原因は、Wear へ届く通知には優先順位があり、バイブする or 音を鳴らす 通知でない限りは、すぐに Wear へ通知が届かないからだった。

バイブや音を鳴らさない通知の場合、5分後など適当な間隔で Wear へ通知が届き、しれっとカードが表示されているという状態になる。それに気づかなかったので、通知が届かない〜!!とハマった。

すぐに Wear へ通知を出したい場合は、バイブや音を鳴らすように注意するべし!


以下、動作確認に使用したサンプルコード。

2015年8月6日木曜日

Amazon.comへ法人でTax Informationを登録する

Amazon Apps & Services Developer Portal へデベロッパー登録するためには、amazon.comのアカウントが必要になる。

amazon.co.jpでアカウントを作っていても、amazon.comとamazon.co.jpではアカウントは共通化されていないので、amazon.co.jpのアカウントはamazon.comでは使用できない。amazon.comでは新規にアカウントを作成する必要がある。

更に、amazon.comでアカウントを作成する際、広告やIn-App課金などを想定している場合は、Tax Identityへの登録も要求される。やり方は、Tax Information Interviewに答えながらフォームを埋めていく形になるのだが、これがなかなか厄介だった。


Tax Information Interview




米国市民か?に「No」を選択。「Save and continue」ボタンをクリック。




Type of beneficial owner(受益権所有者のタイプ):Corporation
Place of organization:Japan
を選択。

Organization nameには、企業名を入力。
主に個人事業主用のオプションなので入力は必要ないかなと思ったけれど、一応、代表者名をDisregarded entity nameへ入力。

「Are you an agent acting as an intermediary?(仲介業者的なことをするエージェントなの?)」には「No」を選択。




Type of beneficial ownerにCorporationを選択すると、住所などを入力するフォームが表示されるので入力する。

Mailing address(郵便を出す時の宛先住所)はPermanent addressと同じなので、「Same as permanent address」を選択。

Tax identification number(TIN)、つまり納税者番号は日本には無いので、「I do not have a U.S. TIN or a foreign (non-U.S.) income tax identification number」を選択。

「Save and continue」ボタンをクリックして次に進むと、ここまでの入力確認(レビュー)が表示される。




非米国法人の基本的なフォーマット「W-8BEN-E」の証明書類が選択されている。

ここでは、確認のみで編集は出来ないので、修正をする場合はページ下の「Previous」ボタンをクリックして、戻って修正する。




Type of beneficial ownerにCorporationがチェックされていることを確認。




次に進むには「Save and continue」ボタンをクリック。




「I consent to electronic receipt of my information reporting documentation(税務情報を電子的に送ることに同意する)」を選択。

「I consent to provide my electronic signature(電子署名を提供することに同意する)」を選択。更に、Electronic signatureについての詳細が表示されるので、全てにチェックを入れる。






「 Signature of individual authorized to sign for beneficial owner」の欄には代表者名を入力。

「E-mail address」にはamazon.comへ登録したものと同じメールアドレスを入力。

「I certify that I have the capacity to sign for the entity identified on line 1 of this form.」をチェック。このフォームの1行目とは企業名が入力されている部分。その企業を代表してサインする能力が私にあることを証明するという内容。そこまで断定的に言われると、ちょっとビビってしまいますが、チェックしましょう(^_^;)

「Submit」ボタンをクリックすると、登録完了!


登録完了後の確認

Developer ConsoleからログインしてSETTING > Tax Identity を確認してみましょう。



Validationは、1時間もせずに完了しました。

2015年7月24日金曜日

ソーシャルボタンのアイコンをSVGファイル化する

ソーシャル系のガジェットやスクリプトは、多数のリソースを読み込むなど、ユーザーの体感スピードを劣化される。デザインも統一感が、なかなか出せない。

よって、自作しよう!となった時に、ソーシャルボタンのSVGファイルが欲しくなって調査した忘備録。


1. Twitter、Facebook、Google+ のSVGファイル作成


これら3つは、font-awesomeの中にアイコンがあるので、「encharm/Font-Awesome-SVG-PNG」を使う。


環境



手順


Font-Awesome-SVG-PNG のインストール

$ npm install -g font-awesome-svg-png

インストール確認

$ npm ls --depth=0 -g

インストールしたディレクトリ下の、bower_components/font-awesome-svg-png/には、black/ と white/ のディレクトリがあるので、黒や白のSGVファイルが欲しい場合は、そこからゲットすればよい。

赤など、黒白以外のカラーのSVGファイルが欲しい場合は、コマンドで書き出せる。

SVGファイルを書き出し

オプションの例
  • --dest: 出力パス
  • --color: 赤
  • --sizes: 80px
  • --icons: Twitter, Facebook, Google+
  • --no-png: SVGオンリー
$ font-awesome-svg-png --dest font-awesome --color red --sizes 80 --icons twitter,facebook,google-plus --no-png
※--iconsの指定では、カンマの後ろにスペースを挟まない


2. はてブ、LINE、Pocket のSVGファイル作成


これら3つは、font-awesomeの中にアイコンが無い。既にアイコンのパスをトレースしてSVG化してくれている方がいたので、ありがたく頂戴する。


参考



補足


これ、font-awesomeにあったっけ?という時は、font-awesomeのサイトで検索するとよい。

2015年4月6日月曜日

GAE/J上のGoogle Cloud EndpointsをバックエンドにしてSupersonicのdataアクセスを試してみた


Supersonic data セットアップ



1. データプロバイダの追加


プロジェクトディレクトリ下でconnectを発動し、プロジェクトのコネクトスクリーンを開く。
$ steroids connect

コネクトスクリーンの"Data"タブで「Custom Provider」を選択。




NAMEに任意のデータプロバイダ名を入力。
BASE URLは「https://YOUR_APP.appspot.com/_ah/api/」まで設定。v1などバージョンナンバーまでのパスにしない(後述のリソース追加設定のため)。



また、Google Cloud Endpointsはhttpsアクセスしか認めていないので、http://ではなく、https://にしておく。


2. リソースの追加


「ADD NEW RESOURCE」ボタンをクリックして、リソースを設定する。

NAMEに任意のリソース名を入力。
URL PATHは「yourApi/v1/」を入力。
例えば、API名が「playlistApi」、バージョンが「v1」の場合、URL PATHは「playlistApi/v1/」になる。



前述のプロバイダのBASE URLで「〜/v1/」まで含めてしまうと、必須入力であるリソースのURL PATHに何も設定できなくなるので、プロバイダのBASE URLを「api/」までと設定した。

これにより、リソースへのパスは、BASE URL + RESORCE URL PATH = 「https://YOUR_APP.appspot.com/_ah/api/playlistApi/v1/」となる。


3. アクションのカスタマイズ


「CUSTOMIZE ACTIONS」ボタンをクリックして、get/postなどのリクエストの、URL PATHやROOT KEYSを設定する。

例えば、コレクションを取得するgetのAPIパスが「playlist」の場合、URL PATHは「playlist」になる。



これにより、完成したエンドポイントへのパスは、BASE URL + RESOURCE URL PATH + ACTION URL PATH = 「https://YOUR_APP.appspot.com/_ah/api/playlistApi/v1/playlist」となる。


更に、Google Cloud Endpointsのコレクションのルートは「items」なので、ROOT KEYSに「items」を指定する。


「RELOAD MODEL FROM API」ボタンをクリックすると、エンドポイントとの通信が行われる。無事にコレクションが取得できると、最新のエンティティモデルの内容が表示される。





デバイスから data access 確認



トップ画面には、「https://YOUR_APP.appspot.com/_ah/api/playlistApi/v1/playlist」へアクセスした結果が表示されている。




API Explorerで、dataStoreにエンティティを1つ追加してみる。




デバイスで起動しているアプリ側のリストも、数秒後に、自動的に更新される。





10秒間隔で同期が発動する



ログを見ると、ほぼ10秒間隔でエンドポイントが叩かれている。




データバインディングはいいのだが、10秒おきにプルリクエストが発動するのでは、GAEのdataStoreのRead Quotaの消費が激しくなってしまう。

2015年4月現在の無料枠では、Read Operationsのリミットは50,000回/日なので、

( 60秒 / 10秒間隔 ) x 60分 = 360回/時

1時間アプリを起動しているだけで、360回のRead Operationsが発生する。

50,000回 / 360回 = 138.889

例えば、同時に138人が1時間アプリを起動しているだけで、50,000回のRead Operationsのクオータはすぐに消費されてしまう。


データバインディングは、supersonic.data APIのwhenChanged()で実現されている。
Playlist.all().whenChanged( function (playlists) {
        $scope.$apply( function () {
          $scope.playlists = playlists;
          $scope.showSpinner = false;
        });
    });

クオータの消費を避けたいなら、whenChanged()を使わずに、適切なタイミングで自作でデータの同期を行う必要があるなと。


参考:Supersonic Guides - Supersonic Framework

2015年4月3日金曜日

AppGyverのSupersonicを使ってdataアクセスするアプリを作っていて"Module 'common' is not available!"のエラーが出たら

AppGyverが提供しているハイブリッドアプリ開発のフレームワークSupersonic。

Supersonicを使ってアプリを作っていて、バックエンドのデータへアクセスする部分を組み込んだ途端、

Error: [$injector:nomod] Module 'common' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.

というエラーが出て、ハマってしまったので、その備忘録。



アクセスしようとしたデータもnot found状態になる。





ここで要求されている「common」モジュールは、プロジェクトのディレクトリ下のapp/common/。

Supersonic Guides - Supersonic Framework Utilising the common module
The common module also already contains a layout file (located at app/common/views/layout.html), which has all the basic dependecies of your project declared, such as supersonic.js, steroids.js and cordova.js. Because common is declared as a dependency to all modules by default, the layout file in there can be used to throughout the app just by removing any layout files from the module itself.

Supersonicを使ったアプリのレイアウトなど基本的な部分で使われているモジュール。

自分のプロジェクトには、確かにそのディレクトリが無い。


レイアウトという時点で、もしやと思ったのは、シンプルページアプリケーションとして、プロジェクトを作ったことがボトルネックになっているのではないか、マルチページアプリケーションとして、そもそもプロジェクトを作っていないと、このcommonモジュールはapp下にデプロイされないんじゃないか?ということ。


試しに、プロジェクトをマルチページで、一から作りなおしてみる。

$ steroids create プロジェクト名

続くアプリタイプの選択で、デフォルトのマルチページタイプを選択。



改めて、新しく作ったプロジェクトで、データのリポジトリを作り、テストデータを入れてみたら、今度はエラーが出ない!

テストデータも表示されている。




commonモジュールが無いエラーで嵌ったら、プロジェクトをマルチページタイプで作りなおせばOKです。


シングルページタイプのアプリでSueprsonicのdata APIが使えないということでは無いと思います。

あくまでも、Supersonicのチュートリアルを試していて、データのハンドリングをやりたいと思っただけなのに、commonモジュールにはまってしまった場合の対処方法です。

2015年3月14日土曜日

Android端末でとったスクリーンショットが「フォト」で同期されないと思ったら

Android端末でとったスクリーンショットが、PCのブラウザから「フォト」へアクセスしても見当たらないなと思ったら、自動バックアップがオンになっていても、スクリーンショットのディレクトリはデフォルトでは同期の対象外になっていました。

スクリーンショットのディレクトリを同期対象に含めるには以下の方法で。

Android端末で「フォト」を起動。「メニュー」から「端末内」を選択。


Screenshotsのクラウドアイコンがオフになっているので、タップしてオンに変更。


同期対象になりました。


以降は、スクリーンショットを撮る度に、PCのブラウザからGoogle+ の「フォト」へアクセスすると、すぐに画像が表示されます。

2015年2月27日金曜日

npm installしたパッケージの名前だけを取得する

ローカルにインストールしたパッケージの名前だけを取得したいときのコマンド。

$ npm ls --depth=0

実行すると、以下のように、トップレベルの名前だけ取得できる。
$ npm ls --depth=0
├── browser-sync@2.2.1
├── gulp@3.8.11
├── gulp-autoprefixer@2.1.0
├── gulp-concat@2.5.2
├── gulp-imagemin@2.2.1
├── gulp-less@3.0.1
├── gulp-minify-css@0.4.6
├── gulp-minify-html@1.0.0
├── gulp-newer@0.5.0
├── gulp-plumber@0.6.6
├── gulp-sass@1.3.3
├── gulp-uglify@1.1.0
├── rimraf@2.2.8
└── run-sequence@1.0.2

トップレベルだけでなく、もう一階層深く知りたいときは
$ npm ls --depth=1

つまりdepthを増やしていけばいい。


グローバルにインストールされたパッケージを取得したいときは-gを付けるだけ。
$ npm ls --depth=0 -g
/usr/local/lib
└── npm@2.5.1

MacでNode.jsとnpmをアンインストールするには

Node.jsをMacからアンインストールするには、以下のシェルスクリプトを実行する。
lsbom -f -l -s -pf /var/db/receipts/org.nodejs.pkg.bom \
| while read i; do
  sudo rm /usr/local/${i}
done
# uninstall node
sudo rm -rf /usr/local/lib/node \
     /usr/local/lib/node_modules \
     /var/db/receipts/org.nodejs.*;

# uninstall npm
sudo rm -rf ~/.npm;

# uninstall check
node -v;
npm -v;

最後にnodeやnpmコマンドを実行して失敗することを確認している。


上記のコードを、「uninstall-node.sh」などのファイル名で保存して、実行権限を付与。
$ chmod +x ./uninstall-node.sh

実行すると、Node.js、npmのアンインストールが完了する。
$ ./uninstall-node.sh


再インストールするには

公式のダウンロードサイトからpkgをダウンロードしてNode.jsを再インストールする。homebrewやnodebrewなどからインストールする方法もあるが、公式のダウンロードサイトからpkgをダウンロードしてインストールする方法の方が確実だと感じた。

再インストールが完了したら、念の為バージョン確認。
$ node -v
v0.12.0
$ npm -v
2.5.1


アンインストールに至った経緯

そもそも、Node.jsを再インストールして白紙からやり直そうと思ったのは、npm install 実行中に色々とエラーが発生したから。

例えば、gulp-sass@1.3.3をインストールでは、以下のエラーが発生。
Error: `libsass` bindings not found. Try reinstalling `node-sass`?
    at getBinding (/path/to/my/project/node_modules/gulp-sass/node_modules/node-sass/lib/index.js:21:11)
    at Object.<anonymous> (/path/to/my/project/node_modules/gulp-sass/node_modules/node-sass/lib/index.js:181:23)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Module.require (module.js:364:17)
    at require (module.js:380:17)
    at Object.<anonymous> (/path/to/my/project/node_modules/gulp-sass/index.js:3:17)
    at Module._compile (module.js:456:26)

更に、browser-sync@2.2.1のタスク実行では、以下のエラーが発生。
dyld: Symbol not found: _node_module_register
  Referenced from: /path/to/my/project/node_modules/browser-sync/node_modules/socket.io/node_modules/engine.io/node_modules/ws/build/Release/bufferutil.node
  Expected in: dynamic lookup


stackoverflowを見ると、Node.jsのバージョンが関係しているようだったので、クリーンな状態に一旦戻してからnpm installを試してみようと。

クリーンな状態から、再度、npm installを実行すると、gulp-sassもbrowser-syncも、問題なくローカルにインストールできた。

2015年2月26日木曜日

Git - gulpのパッケージをgitで共有したくなかったら

gulp、そしてgulpで利用するパッケージだけで、かなりディレクトリ容量が膨らむ。
これをgitで共有するのは嫌だなあと思って試行錯誤。

とりあえず、グローバルとローカルの両方にgulpをインストールするのではなく、グローバルだけにできないかと思ったが、逆(ローカルのみインストールする)はあっても、グローバルのみで動かす設定は発見できず。

gulp.jsのフローを見る限り、ローカルへのインストールは必須のようだし。
if (!env.modulePath) {
    gutil.log(
      chalk.red('Local gulp not found in'),
      chalk.magenta(tildify(env.cwd))
    );
    gutil.log(chalk.red('Try running: npm install gulp'));
    process.exit(1);
  }

このフローの通り、ローカルにgulpがインストールされていないと、gulpの実行はできない。
[13:20:52] Local gulp not found in ~/git/プロジェクト名
[13:20:52] Try running: npm install gulp


gulpが、グローバルとローカルとの、両方でインストールが必須なのには構造的な理由がある。

グローバルにインストールされたgulpは、ローカルにインストールされたgulpを実行するためのもの。このグローバルとローカルの2つのモジュールの中身は完全に同じもので、まったく同じモジュールをグローバルとローカルにインストールすることになる。

gulpコマンドが実行されると、gulp.jsのrequireで、ローカルのgulpがインポートされる。

そして、実際の処理(gulpfile.jsに書かれたタスク)はrequireされたローカルのgulpモジュールが全て行う。つまり、gulpは、自分自身が自分自身と同じモジュールをrequireして使うという仕組みで動いている。


では、ローカルへのインストールは仕方がないとしても、gitで共有したくない問題はどうにかできないかと思ったら、

gitなどでプロジェクト共有している場合、この node_modules を共有していると結構重かったりしますし、 package.json が共有されていたらあとで $ npm install をしたら必要なものが入ってくるため、 node_modules のディレクトリの共有は不要です。
Node.js 製の タスクランナー gulp.js を使ってみる - HAM MEDIA MEMO


ハッ!その手があったか!思わずポンと手を打ってしまった。


.gitignoreでnode_modulesをignoreすればいいじゃないですか。


gitからクローンした際は、npm installすればパッケージはインストールされるんだし。


ですよねぇ。これって常識だったのかしら。


追記

.gitignoreのサンプルは以下。一行だけだけれど。

/node_modules/

上記内容をプロジェクト直下にファイル名「.gitignore」で保存。
「node_modules」の前の/で、プロジェクトと同階層であることを明示。
「node_modules」の後の/で、ディレクトリであることを明示。

つまり、「/node_modules/」で、プロジェクト直下のnode_modulesディレクトリを無視する、という設定になる。

2014年12月19日金曜日

Google APIs Client Library for JavaScriptを使いながらfieldsを指定するには

Google APIs Client Library for JavaScriptを使っていて、必要ないレスポンスデータを削除する為にfieldsを指定するときの忘備録。

検索やデータ取得などのGET系では、パラメータの一部として指定すればいい。

gapi.client.drive.files.list({
  'fields': 'items(id,title),selfLink',
  'q': "title='" + folderTitle + "' and trashed=false"
}).execute(function(folder) {
  console.debug(folder.selfLink);
});


作成、更新などのPUT系では、Request bodyをresponseフィールドに定義するが、fieldsは、responseの中ではなく、外に指定する。この点が注意ポイント。

gapi.client.drive.files.insert({
  'fields': 'id',
  'resource': {
    mimeType: 'application/vnd.google-apps.folder',
    title: folderTitle
  }
}).execute(function(folder) {
  console.debug(folder.id);
});

2014年11月20日木曜日

DartアプリケーションをGoogle App EngineのManaged VMsで動かすまで

Google App Engine(GAE)のManaged VMs(MVMs)でDockerがサポートされた。これまでLimited Previewだったが、今月のGCP Liveでオープンになった。

あわせて、GAEのMVMs上でDartアプリケーションを動かすことができるように、GoogleがDartアプリケーションのランタイムイメージを提供してくれたので、それをベースイメージにして、自分で作ったDartアプリケーションをGAEへデプロイできるようになった。

以下、GAEのMVMsって何?という全体像の把握から、DockerやDartの環境構築、アプリのデプロイまでの一連の流れをまとめてみた。

参考:



1. 全体のイメージを掴む

Dart and Google Cloud Platform | Dart: Structured web apps
まずは、GAEのMVMsでDartアプリケーションを動かすってどういうこと?を消化。上記のページに概要がまとまっている。動画でも、6分程度だが、内容が分かりやすくまとめられているのでチェックすることをオススメ。

内容的に少しかぶるが、GAEのMVMsについての説明も目を通した。
Managed VMs - Google App Engine — Google Cloud Platform


要点:

これまでのGAEは、特定の言語、Python、Java、GoやPHPのランタイムをベースにしたWeb Serverと自分のコードが動くサンドボックス。環境は変えられず、全てはGoogleに管理されていた。

MVMsだと、カスタマイズしたGoogle Compute Engine(GCE)のVirtual Machines(VMs)上でGAEのような手軽さで、自分のコードを動かすことができるよ、GCEのIaaS的な柔軟性とGAEのPaaS的な管理の手軽さ、どちらもおいしいとこどりができるよ、というもの。

MVMsは、GAEのインスタンスを、GCE上で動かしてくれる。


MVMsが登場した背景やメリットについては、トップゲートさんの以下のページが参考になる。
GAE Managed VMs誕生までの歴史を振り返る | 株式会社トップゲート(Google Cloud Platform Partner / Google技術者集団)



2. Dartの開発環境の構築

Dart のツールやDart Editorをダウンロード。
https://www.dartlang.org/codelabs/darrrt/#set-up

解凍して、ローカルマシーンにdartディレクトリを配置したら、SDKのbin/までPATHをはる。

例えばMacでは、.bash_profileに以下を追加。
$ export PATH=$PATH:<installation directory>/dart/dart-sdk/bin



3. boot2dockerをインストール

Mac:Releases · boot2docker/osx-installer
上記のリンクからパッケージをダウンロードして、boot2dockerをインストールする。

boot2dockerは、VirtualBoxのためにパッケージされたLinux VMイメージ。このイメージの中には、boot2dockerのコマンドやDockerのデーモンが含まれている。



VirtualBoxは、DockerのVMデーモンをホストするもの。DockerコンテナはVirtualBox上で走る。なお、GAEのMVMsが対応しているのは、VirtualBox 4.3.10以上。

boot2dockerの最新は、v1.3.1(Docker v1.3.1、Linux v3.16.4)。VirtualBox v4.3.18-r96516を含んでいるので、インストールが完了すると、VirtualBoxもインストールされる。Macの場合、boot2dockerコマンドも、dockerコマンドも、/usr/local/binにインストールされる。



4. Docker設定

boot2dockerにはDocker v1.3.1が含まれているが、boot2dockerのVM内部で、Docker v1.3.0を要求するそうで、以下のコマンドを走らせて、その辺の設定を行う。

$ mkdir ~/.boot2docker
$ echo 'ISOURL = "https://github.com/boot2docker/boot2docker/releases/download/v1.3.0/boot2docker.iso"' > ~/.boot2docker/profile
$ boot2docker init
 



5. boot2docker起動

boot2docker起動。
$ boot2docker up

正常に起動したらセットアップ情報が表示される。GAEのMVMsでアプリケーション開発をするためには、必要ないので無視。
$ boot2docker up
Waiting for VM and Docker daemon to start...
.....o
Started.
Writing /Users/hogehoge/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Users/hogehoge/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Users/hogehoge/.boot2docker/certs/boot2docker-vm/key.pem

To connect the Docker client to the Docker daemon, please set:
    export DOCKER_TLS_VERIFY=1
    export DOCKER_HOST=tcp://192.168.59.103:2376
    export DOCKER_CERT_PATH=/Users/hogehoge/.boot2docker/certs/boot2docker-vm



6. Dockerイメージを取得

以下のコマンドを実行して、環境変数をセットアップ。
$ $(boot2docker shellinit)

次に、以下のコマンドを実行して、Googleが提供している幾つかのDockerイメージをダウンロードする。
$ docker pull google/docker-registry

イメージが無事に取得できたかどうか確認。google/dartリポジトリからのイメージも取得できているはず。
$ docker images
REPOSITORY    TAG    IMAGE ID    CREATED    VIRTUAL SIZE
google/dart  latest  cd7baf4008f8  2 weeks ago  243.6 MB
google/docker-registry  latest  5d4bb763edd7  3 weeks ago  428.8 MB

確認のため、Dart VMのバージョンナンバーを出力。VMが走っていないといけないので、gogole/dartをrunする。
$ docker run google/dart /usr/bin/dart --version
Dart VM version: 1.7.2 (Tue Oct 14 12:12:42 2014) on "linux_x64"


Dockerについて、最低限知っておくとよい知識:

Docker:コンテナ型の仮想化を実現するためのソフトウェア。VMwareなどのサーバの仮想化と違い、扱う単位がマシンではなくプロセスであることがポイント。サーバ管理で必要なあれこれは不要。

リポジトリ:コンテナを管理する単位。

コンテナ:プロセス空間やネットワーク等が外部から隔離された空間。コンテナの中でプロセスが動く。

イメージ:コンテナのテンプレート。イメージを元にして新しいコンテナを作る。


仮想化の単位がマシンイメージからコンテナイメージに移ってきた感じ。
Dockerの基本については、TECHSCOREさんのブログが参考になる。
» 隔離の技術Dockerの考え方と使い方の基本 TECHSCORE BLOG



7. Google Developer Consoleでプロジェクトを作成

Google Developers Console

プロジェクト名とプロジェクトIDを決定する。IDは後で変えられないので、よく考えて決める。

プロジェクト作成後、GAEやGCE周りの設定が必要なんじゃないかと試行錯誤したが、結局必要なことは単にプロジェクトを作るだけだった。



8. Google Cloud SDKをインストール

Mac:ターミナルで以下のコマンドを入力。インストールが終了したら、ターミナルを再起動。
$ curl https://sdk.cloud.google.com | bash

Google Cloud SDKを使うにはgmailアカウントを設定する認証作業が必要。
$ gcloud auth login

プロジェクトIDを設定。
$ gcloud config set project <my-project-id>

コンポーネントを最新にする。
$ gcloud components update app

設定を確認。
$ gcloud config list
[core]
account = xxx@gmail.com
disable_usage_reporting = False
project = <my-project-id>
user_output_enabled = True



9. Dart App Engineプロジェクトを作成

Dart App Engineプロジェクトとして、helloworldを作ってみる。ディレクトリ場所はお好みで。
$ mkdir helloworld

app.yamlファイルを作成。これはDart App Engineアプリケーションの設定ファイル。ディレクトリのトップ、ここではhelloworldディレクトリ直下、に配置する。
$ cd helloworld
$ vi app.yaml
app.yaml中身
version: helloworld
runtime: custom
vm: true
api_version: 1

続いて、Dockerfileファイルを作成。これも配置はディレクトリトップ。
$ vi Dockerfile
Dockerfile中身
FROM google/dart-runtime

更に、pubspec.yamlファイルを作成。pubspec.yamlは、Dartプログラムが依存するパッケージなどを定義する。これもこれも配置はディレクトリトップ。
$ vi pubspec.yaml
pubspec.yaml中身
name: helloworld
version: 0.1.0
author: Misaho Otsuka
dependencies:
  appengine: '>=0.2.1 < 0.3.0'



10. Dartプログラムを作成

サーバーサイドのDartプログラムを作成。配置はbin/下。ファイル名はserver.dart。Dart App Engineのプログラムは、bin/server.dartから実行を開始するので、このファイルは必須。
$ mkdir bin
server.dartの中身
import 'dart:io';
import 'package:appengine/appengine.dart';

main() {
  runAppEngine((HttpRequest request) {
    request.response..write('こんにちは、世界!')
                    ..close();
  });
}

runAppEngine()は、トップレベルメソッド。Dartのランタイムを走らせ、App Engineへ接続する。引数はコールバック関数。この例では、HttpRequestを使ってレスポンスに「こんにちは、世界!」と出力し、レスポンスをクローズしている。


Dartプログラムは、Dart Editorで編集してもいい。



最終的な階層構造。





11. pub getを実行

Dart App Engineのプロジェクトに必要なライブラリをインストールする。helloworldディレクトリ下で実行。
$ pub get
Resolving dependencies... 
Got dependencies!



12. Dartのランタイムイメージをダウンロード

これは1度だけ実行すれば良い。
$ docker pull google/dart-runtime

イメージがゲットできたことを一応、確認。
$ docker images
REPOSITORY    TAG    IMAGE ID    CREATED    VIRTUAL SIZE
google/dart-runtime  latest  e2ab3ccbce58  2 weeks ago  243.6 MB
google/dart  latest  cd7baf4008f8  2 weeks ago  243.6 MB
google/docker-registry  latest  5d4bb763edd7  3 weeks ago  428.8 MB



13. ローカルで実行確認

$ gcloud preview app run app.yaml
コンテナが起動した後は、ヘルスチェクが定期的に何度も呼び出される。
$ gcloud preview app run app.yaml
Module [default] found in file [/Users/hogehoge/git/mvm-sample/helloworld/app.yaml]
INFO: Looking for the Dockerfile in /Users/hogehoge/git/mvm-sample/helloworld
INFO: Using Dockerfile found in /Users/hogehoge/git/mvm-sample/helloworld
INFO: Skipping SDK update check.
INFO: Starting API server at: http://localhost:52671
INFO: Health checks starting for instance 0.
WARNING: Health check for instance 0 is not ready yet.
INFO: Building image test-mvmdart.default.helloworld...
INFO: Starting module "default" running at: http://localhost:8080
INFO: Starting admin server at: http://localhost:8000
INFO: Image test-mvmdart.default.helloworld built, id = 6a65a3d6cb36
INFO: Creating container...
INFO: Container 25f2f396d660bd8411e150e48ae05290922b3bebda07acaf8e90007a577aa045 created.
INFO: default: "GET /_ah/start HTTP/1.1" 200 2
INFO: default: "GET /_ah/health?IsLastSuccessful=no HTTP/1.1" 200 2
INFO: default: "GET /_ah/health?IsLastSuccessful=yes HTTP/1.1" 200 2
INFO: default: "GET /_ah/health?IsLastSuccessful=yes HTTP/1.1" 200 2

localhost:8080へアクセスすると、実行結果が確認できる。




「こんにちは、世界!」を「Hello, world!」へ変更して保存すると、




ブラウザでリロードすれば、変更が反映されていることが確認できる。




アプリケーションへの変更は監視されている。






ローカルサーバーを停止したい場合は、Control-C。gcloudコマンドでrunしたローカルサーバーをstopするコマンドは、無い。

ローカルで実行確認をすると、現在のプロジェクトの内容を元に、<my-project-id>.default.<app.yamlで定義したversion>という名前で、コンテナが作られる。





14. GAE上にデプロイ

$ gcloud preview app deploy app.yaml

注意:

デプロイし終わるまで結構時間がかかる!大体3分ほど。しかもヘルスチェクが5秒おきに30回程度実行される。でも待っていればヘルスチェクも終わって完全にデプロイ作業が完了する。デプロイが終わらないと、URLへアクセスしても、503になるだけなので辛抱。

$ gcloud preview app deploy app.yaml
Updating module [default] from file [/Users/hogehoge/git/mvm-sample/helloworld/app.yaml]
06:34 PM Host: appengine.google.com
{bucket: vm-containers.test-mvmdart.appspot.com, path: /containers}

06:36 PM Host: appengine.google.com
06:36 PM Application: test-mvmdart (was: None); version: helloworld
06:36 PM 
Starting update of app: test-mvmdart, version: helloworld
06:36 PM Getting current resource limits.
06:36 PM Scanning files on local disk.
06:36 PM Scanned 500 files.
06:36 PM Cloning 530 application files.
06:36 PM Starting deployment.
06:36 PM Checking if deployment succeeded.
06:36 PM Deployment successful.
06:36 PM Checking if updated app version is serving.
06:36 PM Will check again in 5 seconds.

... 中略

06:39 PM Will check again in 5 seconds.
06:39 PM Checking if updated app version is serving.
06:39 PM Enough VMs ready (2/2 ready).
06:39 PM Completed update of app: test-mvmdart, version: helloworld


デプロイが完了したら、

http://helloworld.<my_project_id>.appspot.com/

へアクセス。ローカルと同じ実行結果が確認できる。





Google Developer Consoleを確認すると、自動的に、GAEのインスタンス設定が行われ、GCEのVMインスタンスも設定されていることが確認できる。

GAEのダッシュボード。「Google管理」となっているのがMVMsの印。




GCEのVMインスタンスは自動的に2つ作成され、どちらもマシンタイプ、ゾーンは以下のようになっていた。





Docker お掃除コマンド Tips:

デプロイを行うと、localhost...をリポジトリとするイメージや<none>なイメージ、busyboxのイメージが増える。




停止したコンテナを消したり、不要なイメージを削除したりするお掃除コマンドは必須。

・停止したコンテナのリストを取得して削除する
docker rm $(docker ps -a -q)

・タグが<none>イメージを削除する
docker rmi $(docker images | grep "^<none>" | awk "{print $3}")

・デプロイする度に増えるlocalhost...をリポジトリとするイメージを削除する
docker rmi $(docker images | grep "localhost*" | awk "{print $3}")




結論: どれくらい手間?

Dart on App Engine、どれくらい管理やデプロイに手間がかかるか?というと、結局、GAEとあまり変わらない

一連の作業はプロセスが幾つもあって時間がかかる印象だが、一度環境を作ってしまえば、後は、サーバーサイドのDartプログラム書いて、runして、deployするだけ。仮想化も、サーバレベルでなく、コンテナレベルなので、プロジェクトに関係ないメンテ作業なども必要ない。


結論: どれくらいお金かかる?

手間はGAEと変わらないのだが、費用はGAEベースではなく、GCEベースなので、アクセスがなくてもインスタンスを削除しない限り費用が発生する。この点は注意。

今のところ、このhelloworld程度のプログラムをデプロイして、かかった費用は

3日間で$1.66

(内訳:Storage Pd Capacityに$0.03、Generic Small instance with 1 VCPU, no scratch diskに$1.63)

ということは、1日あたり$0.5程度。今のレートでは約50円。1ヶ月では約1,500円。悪くないんじゃない?という感じ。

2014年11月11日火曜日

Dart事始め

Dartの「GET STARTED」の一連のステップをやってみた。


「GET STARTED」で最終的にできあがるWebアプリの仕様

  • テキストフィールドへ名前が入力されると、右側のバッジ部分に「名前 the 称号」と海賊っぽく表示する

  • テキストフィールドが空の場合は、ボタンを押す度に「名前 the 称号」の組み合わせをランダムに表示する

  • テキストフィールドが空かどうかで、ボタンのラベルを変える
  • テキストフィールドへ入力すると、ボタンを無効化する
  • テキストフィールドへ入力する度に「名前 the 称号」の組み合わせをランダムに表示する
  • 「名前 the 称号」が変わる度に、名前と称号をローカルストレージへ保存する



Dartで書くときのポイントについて色々と気づき

  • importでライブラリ全体をインポート。特定のクラスをインポートしたい場合はshowで
  • final, staticなどはJavaと同じ意味で使用可能
  • privateキーワードは無い。_を付けた変数がprivate扱いになる
  • プログラムの実行主体はvoid main()
  • 入出力ストリームの監視はlisten()で
  • JavaScriptのPromisesと同じ扱いでFutureオブジェクトが使える。then()やcatchError()で、onSuccessとonErrorの扱いを切り分ける
  • カスケードオペレーター(..)で、あるオブジェクトの複数のプロパティへアクセスできる
  • window.localStorage['キー']でローカルストレージへアクセス
  • List<String>などGenericsが使用可能
  • getterは、戻り値 get プロパティと定義する。例えば、getPirateName()はString get pirateName
  • => はreturn expr;の省略形として使える
  • $は文字列内で変数を明示する時に使える(print('Error occurred: $error');)
  • コンストラクタも定義できるPirateName(){}だけでなく、PirateName.fromJSON(){}といった定義も可能
  • オプションの引数はcurly brackets({})で指定可能
  • 型変換はasで(e.target as InputElement)
  • querySelector()などでidを渡してDOM操作することになる。値のバインディングは Angularなどで別途自分でやる



「GET STARTED」で最終的にできあがるWebアプリのリソース

  • piratebadge.css
  • piratebadge.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Pirate badge</title>
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="piratebadge.css">
  </head>
  <body>
    <h1>Pirate badge</h1>
    
    <div class="widgets">
      <div>
        <input type="text" id="inputName" maxlength="15" disabled>
      </div>
      <div>
        <button id="generateButton" disabled>
          Aye! Gimme a name!
        </button>
      </div>
    </div>
    <div class="badge">
      <div class="greeting">
        Arrr! Me name is
      </div>
      <div class="name">
        <span id="badgeName"> </span>
      </div>
    </div>
    <script type="application/dart" src="piratebadge.dart"></script>
    <script src="packages/browser/dart.js"></script>
  </body>
</html>

  • piratenames.json : 名前と称号のリストを含んだJSONファイル
{ "names": [ "Anne", "Bette", "Cate", "Dawn",
        "Elise", "Faye", "Ginger", "Harriot",
        "Izzy", "Jane", "Kaye", "Liz",
        "Maria", "Nell", "Olive", "Pat",
        "Queenie", "Rae", "Sal", "Tam",
        "Uma", "Violet", "Wilma", "Xana",
        "Yvonne", "Zelda",
        "Abe", "Billy", "Caleb", "Davie",
        "Eb", "Frank", "Gabe", "House",
        "Icarus", "Jack", "Kurt", "Larry",
        "Mike", "Nolan", "Oliver", "Pat",
        "Quib", "Roy", "Sal", "Tom",
        "Ube", "Val", "Walt", "Xavier",
        "Yvan", "Zeb"],
  "appellations": [ "Awesome", "Captain",
        "Even", "Fighter", "Great", "Hearty",
        "Jackal", "King", "Lord",
        "Mighty", "Noble", "Old", "Powerful",
        "Quick", "Red", "Stalwart", "Tank",
        "Ultimate", "Vicious", "Wily", "aXe", "Young",
        "Brave", "Eager",
        "Kind", "Sandy",
        "Xeric", "Yellow", "Zesty"]}

  • piratebadge.dart : Dartプログラム
// Import Dart Library.
import 'dart:html';
// 'show' keyword, which imports only the specified classes.
import 'dart:math' show Random;
import 'dart:convert' show JSON;
import 'dart:async' show Future;

// Declare button element as a global variable.
ButtonElement genButton;

// Declare span element as a global variable.
SpanElement badgeNameElement;

// key-value pairs string save to local storage.
final String TREASURE_KEY = 'pirateName';

// App starts here.
void main() {
  // Stash the input element in a local variable.
  InputElement inputField = querySelector('#inputName');
  // Listen input streams.
  inputField.onInput.listen(updateBadge);
  genButton = querySelector('#generateButton');
  genButton.onClick.listen(generateBadge);
  badgeNameElement = querySelector('#badgeName');

  // Call the function, which returns a Future(silimar to JavaScript Promises).
  // Using underscore (_) as a parameter name indicates that the parameter is ignored.
  PirateName.readyThePirates().then((_) {
    // on success
    inputField.disabled = false; // enable
    genButton.disabled = false; // enable
    // Retrieve the name from local storage.
    setBadgeName(getBadgeNameFromStorage());
  }).catchError((error) {
    // '$' indicates that this is a variable, not a string.
    print('Error initializing pirate names: $error');
    badgeNameElement.text = 'Arrrr! No names.';
  });
}

void updateBadge(Event e) {
  // use 'as' to type casting
  String inputName = (e.target as InputElement).value;
  setBadgeName(new PirateName(firstName: inputName));
  if (inputName.trim().isEmpty) {
    // The cascade operator (..) allows you to perform multiple
    // operations on the members of a single object.
    genButton
        ..disabled = false
        ..text = 'Aye! Gimme a name!';
  } else {
    genButton
        ..disabled = true
        ..text = 'Arrr! Write yer name!';
  }
}

void setBadgeName(PirateName newName) {
  if (newName == null) {
    return;
  }
  querySelector('#badgeName').text = newName.pirateName;
  // Save the name to local storage.
  window.localStorage[TREASURE_KEY] = newName.jsonString;
}

void generateBadge(Event e) {
  setBadgeName(new PirateName());
}

// The function retrieves the name from local storage and
// creates a PirateName object from it.
PirateName getBadgeNameFromStorage() {
  String storedName = window.localStorage[TREASURE_KEY];
  if (storedName != null) {
    return new PirateName.fromJSON(storedName);
  } else {
    return null;
  }
}

// declare class
class PirateName {
  // final variables cannot change.
  static final Random indexGen = new Random();
  // Declare generic type—List.
  static List<String> names = [];
  static List<String> appellations = [];

  // Private variables start with underscore (_).
  // Dart has no private keyword.
  String _firstName;
  String _appellation;

  // Provide a constructor for the class.
  // curly brackets{} indicates optional parameters.
  PirateName({String firstName, String appellation}) {
    if (firstName == null) {
      _firstName = names[indexGen.nextInt(names.length)];
    } else {
      _firstName = firstName;
    }
    if (appellation == null) {
      _appellation = appellations[indexGen.nextInt(appellations.length)];
    } else {
      _appellation = appellation;
    }
  }

  // The constructor creates a new PirateName instance
  // from a JSON-encoded string.
  PirateName.fromJSON(String jsonString) {
    Map storedName = JSON.decode(jsonString);
    _firstName = storedName['f'];
    _appellation = storedName['a'];
  }

  // Declare a class level method.
  static Future readyThePirates() {
    // If you spell miss the json file name,
    // executes PirateName.readyThePirates().catchError() in main().
    var path = 'piratenames.json';
    // getString() returns Future is used as arguments
    // to _parsePirateNamesFromJSON().
    // then() is a callback function is called when the Future completes successfully.
    return HttpRequest.getString(path).then(_parsePirateNamesFromJSON);
  }

  // Declare a instance and private method.
  static _parsePirateNamesFromJSON(String jsonString) {
    Map pirateNames = JSON.decode(jsonString);
    names = pirateNames['names'];
    appellations = pirateNames['appellations'];
  }

  // Provide a getter for the private variables
  // The fat arrow ( => expr; ) syntax is a shorthand for { return expr; }.
  String get pirateName => _firstName.isEmpty ? '' : '$_firstName the $_appellation';

  // Add a getter to the PirateName class that encodes a pirate name in a JSON string.
  String get jsonString => JSON.encode({
    "f": _firstName,
    "a": _appellation
  });
}


実行は以下から確認できる。

Step6 Run the app.

2014年10月30日木曜日

GitHubでプルリクエストがマージされた後にすること

GitHubで初めてプルリクエストを送ってみました(・∀・)

少しでも、fork元のリポジトリに貢献できるって嬉しいですね。初めての快感です。

送ったプルリクエストは早速マージしてもらったのですが、その後、自分の作業用ブランチやforkした後の自分のリポジトリって、どのように処置したらいいの?と戸惑いました。次回またそうならないために忘備録としてメモ。


1. ローカルのmasterへcheckout

変更作業を開始する前の、forkしてきた初期のステータスを参照している段階へ戻ります。

$ git checkout master


2. ローカルの作業用ブランチを削除

必要なくなったローカルの作業用ブランチを削除します。なんやかんや聞かれても面倒なので、-Dオプションを付けて、強制的に削除してしまいます。

$ git branch -D fixTypo

必要ないブランチが複数ある場合は、それぞれ削除していきます。

ローカルにある、プルリクエストを送るために使ったブランチも、削除します。


3. プルリクエスト済みのGitHub上にあるリモートブランチを削除

$ git push origin :fixTypo


4. ローカルでfork元のリポジトリからプル

ここから先はSourceTreeで作業しました。


fork元のリポジトリからプルを実行します。

プルする時に「プルする元のリポジトリ」をGitHub上の自分のリポジトリではなく、fork元のリポジトリを指します。そのために、プルダウンをクリックして、「カスタム」を選択して、パスを変更します。

単に変更内容を吸いたいだけなので、「マージしてコミットする」などのチェックボックスは全てオフにしておく方が無難です。

fork元は、自分が送ったプルリクエストをマージして更新済みなので、プルが済むと、自分が編集を始める前の状態との差分が出てきます。


5. GitHub上の自分のリモートリポジトリへプッシュ

ローカルのmasterへプルした、最新のfork元のリポジトリの内容を、リモートのGitHub上の自分のリポジトリへプッシュします。


これで、fork元のリポジトリの状態と、GitHub上の自分のリポジトリの状態とが一致します。




もう、このリポジトリはwatchしないなという場合は、リポジトリを削除してもいいと思いますが、なにかしらまだチェックしておきたいかもという場合は、自分のリポジトリが古いバージョンをさしたままにならないようにプルしておくといいと思います。