アニメ風レンダリングが簡単にできるBlenderのFreestyleを試してみた。
Blender2.67から利用できるようになったFreestyleレンダリングが面白そうだったので、以前作ってみた動画で試してみました。
以前作った動画
今までにBlenderとpythonを使って下記のような記事と動画を作りました。
Blenderの物理シミュレーションをpythonスクリプトで作成 - 株式会社CFlatの明後日スタイルのブログ
ミルククラウンをBlenderのpythonスクリプトで作成 - 株式会社CFlatの明後日スタイルのブログ
カオス的挙動をBlenderのpythonスクリプトで作成 - 株式会社CFlatの明後日スタイルのブログ
Freestyleでレンダリング
FreestyleでレンダリングするにはBlenderのバージョンを2.67以降に上げて、動画作成前に下記のコードを挿入するだけです。 bpy.data.scenes["Scene"].render.use_freestyle = True
ちなみに下記のコードを2回呼ばないと駄目だった不思議な現象はバージョンを上げると直っていました。 bpy.ops.object.forcefield_toggle()
実際の動画
下記がFreestyleでレンダリングした動画です。手作りアニメ風に仕上がっていて、物理シミュレーションなんて使っていないように見えるから不思議です。
Blenderの物理シミュレーション結果をFreestyleでレンダリング - YouTube
Blenderの物理シミュレーション結果をFreestyleでレンダリング2 - YouTube
流体シミュレーションのFreestyleレンダリング
ミルククラウンの動画についてはこんな問題があって修正に手間が掛かりそうだったので保留にしました。気が向けば作成するかもしれません。
Ansibleで自社Wordpressを移行を自動化
先日弊社Webサイト、http://www.cflat-inc.com のサーバーをAWSからさくらVPSに移行しました。元々t1.microを使っていたのですが、さすがに非力すぎて時折アクセスできないこともありました。かといってsmallを使うとスペックの割にコストがかかると考えさくらVPSを採用しました。
サーバーが決まったら次は環境の移行を行いますが、失敗することなく同一の環境を構築するためにまずはVirtualBoxの仮想マシンで動作を検証し、問題がなければさくらVPSでセットアップを行うという流れを取ることにしました。つまり2度セットアップを行うことになります。
2度同じセットアップを行うのはミスが入り込む余地だらけですし、そもそも面倒なのでいわゆるプロビジョニングツールを使うことにしました。PuppetとかChefに代表されるアレですね。今回はその中でも(機能的に)軽量であるという噂のAnsibleをトライアルするいい機会だと考え、Ansibleを使って移行を行うことにしました。
ちなみにAnsibleはhomebrewでインストールすることができます。↓でOK。
$ brew install ansible
Ansible 最終形
Ansibleでは、playbookというYAML形式の設定ファイルのようなものに処理を色々と書いておいて、playbookを指定して実行することで対象のサーバーを操作することができます。
今回最終的には次のようなコマンドを実行することになります。
$ ansible-playbook -i hosts all.yml
これはhosts
というファイルに書いてあるサーバーに対してall.yml
に書いてある処理をしてね、という意味になります。
まずhosts
ですが、次のようになっています。
# hosts [server] # 192.168.33.11 cfalt2
コメントアウトしている一つ目のIPはVagrantで立ちあげたVirtualBoxのIPアドレスです。テスト時はこのコメントを外します。
2つ目のcflat2
とあるのは、~/.ssh/config
に書いてある設定の中のホスト名です。本番サーバーを意味します。
次にplaybookのall.yml
はこうなっています。
# all.yml - include: ssh-iptables.yml - include: yum.yml - include: lamp.yml - include: mysql-secure-installation.yml - include: mysql-initialize.yml - include: prepare-contents.yml
playbookでは他のplaybookをinclude
することができるようになっています。all.yml
ではもう少し細かい単位に分けたplaybookをincludeするだけにしてあります。書かれている内容は上から順番に実行されます。
ざっくり言うと、
- iptablesの設定して
- yumの設定して
- Apache、PHP、MySQL入れて
- MySQLのインストールして
- MySQLの初期設定して(ここは↑とまとめてもよかった)
- 元サイトのコンテンツを入れる
という流れになっています。
各playbook解説
playbookを載せると長くなるかもしれませんが、せっかくの事例紹介なので全部お見せします。
ssh-iptables.yml
まずはSSHとiptablesの設定をするplaybook。
- name: change ssh settings and iptables hosts: server sudo: yes tasks: - name: create iptables template: src=files/iptables.j2 dest=/etc/sysconfig/iptables - name: disallow root SSH access lineinfile: dest=/etc/ssh/sshd_config regexp="^#PermitRootLogin " line="PermitRootLogin no" state=present - name: disallow password authentication lineinfile: dest=/etc/ssh/sshd_config regexp="^#PasswordAuthentication " line="PasswordAuthentication no" state=present # NOTE: Combine two tasks to keep ansible access with port 22 - name: restart sshd and restart iptables shell: service sshd restart && /etc/init.d/iptables restart
いきなりAnsible固有の色々がでてきたのでここで多少解説をいれておきます。
項目 | 意味 |
---|---|
name | その部分の名前、確かログに出るぐらいの意味 |
hosts | 対象ホスト、hostsの[server]セクションにある全てのhostを対象にする |
sudo | rootで実行するかどうか |
tasks | 処理を書くところ |
template | テンプレートファイルをdestにコピーする。変数展開も使える。 |
lineinfile | destファイルに指定した行があることを確約したり、置換したりする |
shell | shellコマンドを実行する |
このplaybookの中でtemplate
が参照しているfiles/iptables.j2
にiptablesの設定を書いておきます。
yum.yml
続いてyumのリポジトリを追加します。OSに最初から設定されているリポジトリだけでは足りなかったので。書くのが遅れましたがOSはCentOS 6.3です。
command
とyum
とかありますが、なんとなく読めると思います。
- name: yum setting hosts: server sudo: yes tasks: - name: update yum command: yum -y update # repository - name: add repository 'rpmforge-repo' yum: name=http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.3-1.el6.rf.x86_64.rpm state=present - name: add repository 'remi-repo' yum: name=http://rpms.famillecollet.com/enterprise/remi-release-6.rpm state=present
lamp.yml
- name: Installing Apache, PHP, MySQL hosts: server sudo: yes vars: httpd_port: 80 mysql_port: 3306 tasks: # Apache - name: install apache yum: name=httpd enablerepo=remi,epel,rpmforge state=present - name: start httpd service: name=httpd state=started enabled=yes - name: add iptables rule for httpd lineinfile: dest=/etc/sysconfig/iptables regexp="{{ httpd_port }}" line="-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport {{ httpd_port }} -j ACCEPT" insertbefore="^# add end" state=present notify: restart iptables # PHP - name: install php yum: name={{ item }} enablerepo=remi,epel,rpmforge state=present with_items: - php - php-mysql # MySQL - name: install mysql yum: name={{ item }} enablerepo=remi,epel,rpmforge state=present with_items: - mysql - mysql-server - MySQL-python - name: add iptables rule for mysql lineinfile: dest=/etc/sysconfig/iptables regexp="{{ mysql_port }}" line="-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport {{ mysql_port }} -j ACCEPT" insertbefore="^# add end" state=present notify: restart iptables handlers: - name: restart iptables service: name=iptables state=restarted
長いですね。ここで見るべきはvars
とhandlers
、notify
、with_items
あたりでしょうか。
項目 | 意味 |
---|---|
vars | 変数を使える。あとから参照するには{{ variable }} などとする |
handlers | notifyとセットで使う。他のタスクから呼び出せる共通の処理。 |
notify | handlersに書いた処理の名前を指定することでその処理を呼び出せる。 |
with_items | 複数の項目に同じコマンドを実行したいときに使う。項目は{{ item }}として参照できる。 |
mysql-secure-installation.yml
これもなんとなく読めると思います。
- rootパスワードの設定
template
で.my.cnf
ファイルをコピー
などを行います。
- name: MySQL setup hosts: server sudo: yes vars: root_db_password: XXXXXXXX mysql_root_password: XXXXXXXXX tasks: - name: ensure mysql is running and starts on boot service: name=mysqld state=started enabled=true # Need to do this for idempotency, see # http://ansible.cc/docs/modules.html#mysql-user - name: update mysql root password for all root accounts mysql_user: name=root host=localhost password={{ root_db_password }} - name: copy .my.cnf file with root password credentials template: src=files/.my.cnf dest=/root/.my.cnf owner=root mode=0600 - name: update mysql root password for all root accounts mysql_user: name=root host={{ item }} password={{ root_db_password }} with_items: - 127.0.0.1 - ::1 - name: ensure anonymous users are not in the database mysql_user: name='' host={{ item }} state=absent with_items: - localhost - 192.168.33.11 - name: remove the test database mysql_db: name=test state=absent
mysql-initialize.yml
ここではサイト用のMySQLユーザーやデータベースを作っています。
- name: Creating DB User hosts: server vars: dbname: dbname dbuser: dbuser loginuser: root loginpassword: XXXXXXXX tasks: - name: create database for wordpress mysql_db: db={{ dbname }} state=present encoding=utf8 login_user={{ loginuser }} login_password={{ loginpassword }} - name: create a user for wordpress mysql_user: name={{ dbuser }} password="YYYYYYYY" priv={{ dbname }}.*:ALL state=present login_user={{ loginuser }} login_password={{ loginpassword }}
prepare-contents.yml
このplaybookではコンテンツをサーバーにセットアップします。
ウェブサイトにはWordpressを用いています。このWordpressのファイル(テーマやプラグイン含む)はgitで管理しているのでgitから取得します。また、データベースに入っているデータ類はあらかじめmysqldumpなどでエクスポートしておきます。
- name: instal Git and clone wordpress from git repo hosts: server sudo: yes tasks: # Git - name: install git yum: name=git enablerepo=remi,epel,rpmforge state=present - name: create contents dir file: path=/var/www/contents state=directory owner=apache group=apache - name: clone from repo git: repo=your_repo dest=/var/www/path/to/dir - name: copy vhost template: src=files/virtual.conf dest=/etc/httpd/conf.d/virtual.conf owner=root mode=0644 notify: restart httpd # initial data - name: copy database.sql to server copy: src=files/database.sql dest=/tmp - name: import database.sql to db mysql_db: name=dbname state=import target=/tmp/database.sql handlers: - name: restart httpd service: name=httpd state=restarted
以上のplaybookを全て実行するとサーバーのセットアップからコンテンツの移行までを一括して行ってくれます。
$ ansible-playbook -i hosts all.yml
この状態でサーバーにアクセスすると元サイトと同じ状態になっていることがわかると思います。あとはドメインの設定を変更して、新サーバーを指すようにしてあげれば完了です。
Ansibleのようなツールを使うことで仮想環境でテストしてから本番に適用というのがとても楽ですし、次に同じようなの移行が必要になっても、最後のコンテンツ部分(gitとかsql)の部分を新しくしておくだけで、あとの手順はコマンド一発になり非常に便利です。サーバーの状態がコードとして書かれているというのもよく言われるメリットですね。
Ansibleを使うことで学習コストがかかりましたが、手作業でやってしまおうという誘惑にまけなくてよかったと感じています。
Ansible、Chefに比べるとシンプルで使いやすいという印象をうけました。今回のような小さめのプロジェクトではAnsible優位な気がします。
CUDA6.0用にCPUとGPUの速度比較コードを修正
以前の記事でCUDAプログラミングする際に、CPUとGPUを切り替える方法を書いたのですが、 その2日後にCUDA6.0が正式リリースされ、Unified Memoryという 新しいメモリ確保の仕組みが提供されました。
メモリ確保が簡単に
CUDA5.5まではCPUとGPUの両方にメモリ領域を確保して、それぞれのメモリからメモリへデータを転送するという作業をユーザーがする必要がありました。 CUDA6.0からはCPUとGPUで使用するメモリを1変数で管理できるようになりました。当然データを転送する関数をユーザーが呼ぶ必要もありません。
やるべきことはGPUで使用する変数を用意してcudaMallocManaged()で領域を確保し、使い終わったらcudaFree()で解放するだけです。 メモリ管理が簡単になったので、今回は下記のようなコンストラクタとデストラクタでメモリの確保と解放を行うCpuGpuDataクラスを作ってみました。
SwitchableCPUGPU.cuh
#pragma once #include "Timer.cuh" #define USE_GPU #ifdef USE_GPU #define SWITCHABLE_DEVICE __device__ #define SWITCHABLE_GLOBAL __global__ #define SWITCHABLE_TIMER CudaEventTimer #else #define SWITCHABLE_DEVICE #define SWITCHABLE_GLOBAL #define SWITCHABLE_TIMER Timer #endif template <class T> class CpuGpuData { public: CpuGpuData( const int iSize ) { cudaMallocManaged( &m_data, sizeof(T)*iSize ); } ~CpuGpuData() { cudaFree( m_data ); } T* m_data; };
Timerクラスは前回と同じです。
Timer.cuh
#pragma once #include <string> #include <time.h> class CudaEventTimer { public : CudaEventTimer( const std::string& message ) : m_message( message ) { cudaEventCreate(&m_start); cudaEventCreate(&m_end); cudaEventRecord( m_start, 0 ); } ~CudaEventTimer() { cudaEventRecord( m_end, 0 ); cudaEventSynchronize( m_end ); float time; cudaEventElapsedTime( &time, m_start, m_end ); printf("%s = %f sec.\n",m_message.c_str(), time*0.001); cudaEventDestroy( m_start ); cudaEventDestroy( m_end ); } private: cudaEvent_t m_start; cudaEvent_t m_end; std::string m_message; }; class Timer { public : Timer( const std::string& message ) : m_message( message ) { m_start = clock(); } ~Timer() { m_end = clock(); printf("%s = %f sec.\n",m_message.c_str(), (double)(m_end - m_start)/CLOCKS_PER_SEC); } private: clock_t m_start; clock_t m_end; std::string m_message; };
コードもすっきり
下記が前回と同じ処理を行うテストコードです。メモリの確保と解放をクラスが行っているのですっきりしています。
#include <stdio.h> #include "SwitchableCPUGPU.cuh" #include <iostream> #include <math.h> SWITCHABLE_GLOBAL void Calculate( float* matrixA, float* matrixB, float* matrixC, int iLength, int col = 0, int row = 0 ) { #ifdef USE_GPU row = blockIdx.x * blockDim.x + threadIdx.x; col = blockIdx.y * blockDim.y + threadIdx.y; if ( row > iLength || col > iLength ) return; #endif float target = 0.0f; for ( int i = 0 ; i < iLength ; ++i ) { target += matrixA[row*iLength + i] * matrixB[i*iLength + col]; } matrixC[row*iLength + col] = target; } int main() { // 行列のサイズ決定 const int iLength = 1024; const int iSize = iLength * iLength; CpuGpuData<float> matrixA( iSize ); CpuGpuData<float> matrixB( iSize ); CpuGpuData<float> matrixC( iSize ); for ( int col = 0; col < iLength ; ++col ){ for ( int row = 0; row < iLength ; ++row ){ matrixA.m_data[col*iLength + row] = rand() % (1000); matrixB.m_data[col*iLength + row] = rand() % (1000); matrixC.m_data[col*iLength + row] = 0.0f; } } // ここから時間計測 SWITCHABLE_TIMER t("time"); // 行列計算 #ifdef USE_GPU const int iThread = 16; dim3 thread( iThread, iThread ); const int iBlock = ( iLength + iThread - 1 )/iThread; dim3 block( iBlock, iBlock ); Calculate<<<block, thread>>>( matrixA.m_data, matrixB.m_data, matrixC.m_data, iLength ); cudaDeviceSynchronize(); #else #pragma omp parallel for for ( int i = 0 ; i < iLength ; ++i ) { for ( int j = 0 ; j < iLength ; ++j ) { Calculate( matrixA.m_data, matrixB.m_data, matrixC.m_data, iLength, i, j ); } } #endif return 0; }
実行結果
実行結果も載せておきます。このプログラムでは前回の結果とほぼ同じです。 Unified Memoryの実行速度についてはこちら で詳しく調査されています。メモリの転送が自動で行われてしまうので、注意しないと遅くなるようです。
GPU:0.149291s CPU OpenMP:1.231000s CPU 並列化無し:7.213000s
GPU計算の高速化
今回のプログラムでも全く行っていませんが、GPU計算には色々な高速化手法があります。 興味のある方は下記の書籍等を参考にして下さい。
setTimeout() vs ハッカー、仁義なき戦い
早速ですが、以下のHTMLを見て下さい……。
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>サンプル1</title> <style> #counter { font-size: 3em; font-family: monospace; color: blue; } </style> <script type="text/javascript" src="http://code.jquery.com/jquery-2.1.1.min.js"></script> <script type="text/javascript"> (function(){ $(document).ready(function() { $('#start').on('click', function(){ var counter = 0; $('#time').text(counter); var timer = setInterval(function() { ++counter; $('#time').text(counter); if (counter == 10) clearInterval(timer); }, 1000); }); }); })(); </script> </head> <body> <div> <p> <button id="start">計測開始!</button> </p> <p> <span id="counter"><span id="time">-</span>秒経過</span> </p> </div> </body> </html>
実行してみれば……というか実行しなくてもわかりますが、経過秒数を数えてゆくだけの単純なスクリプトです。
複数回押下への対策をしていないとか、本当はsetInterval()
だと正確な秒数を数えられないとかありますが、今回はsetInterval()
あるいはsetTimeout()
(以下、setTimeout()
系関数)の動作について述べたいだけなので気にしない方針で。
兎に角、ゲームか何かのアニメーション処理を、これらの関数を使って行なおうとしている、とお考え下さい。
この時、もしもユーザーがsetTimeout()
系関数が行なう処理を不正に飛ばそうとした時、ウェブサイトはこの事を検出できるでしょうか?
というのが、今回の話題です。
Round 1
まず、上記のサンプル1を開いたら、JavaScriptコンソールなり、Firebugなり、兎に角デバッグ用コンソールを開きます。 そして、次のJavaScriptを実行してみましょう:
var setInterval2=window.setInterval;window.setInterval=function(a,b){ return setInterval2(a, b/100); }
なんという事でしょう、「計測開始!」ボタンを押したら、もの凄い速度で処理が進むようになりました!
どうやらwindow.setInterval
は読み取り専用プロパティではないようで、ユーザーが上書きできてしまうようです。
だがしかし!
デバッグ用コンソールを開いて上記のコードを打ち込むためには、手動でどんなに頑張ったところでたかが知れている!
ページを開いた瞬間に、兎に角window.setInterval
を変数に保存するようにしてはどうか。
(function(){ var setInterval2 = window.setInterval; // <====== $(document).ready(function() { $('#start').on('click', function(){ var counter = 0; $('#time').text(counter); var timer = setInterval2(function() { // <====== ++counter; $('#time').text(counter); if (counter == 10) clearInterval(timer); }, 1000); }); }); })();
ふっ……私の才能が怖いな。
Round 2
……などと安心していると当然足を掬われまして、世の中にはユーザースクリプトなるものが存在します。
代表的なものはFirefoxのGreaseMonkeyですが、他のブラウザにも同じスクリプトを使えるようなアドオン(あるいはブラウザの組み込み機能)が用意されています。
そして、恐ろしい事に……metadataブロックに@run-at document-start
を追加すると、HTMLを読み込んだ瞬間(!)にスクリプトが実行されてしまいます!
// ==UserScript== // @name VS サンプル2 // @namespace http://localhost/test/blog/sample // @include http://localhost/test/blog/sample*.htm // @run-at document-start // @version 1 // ==/UserScript== (function(){ var setInterval2 = window.setInterval; window.setInterval = function(a,b){ return setInterval2(a, b/100); }; })();
この時点では、DOMツリーの構築も完了していません。
DOMツリーが構築されていないという事は、scriptタグも未実行です。
すなわち、サンプル2でwindow.setInterval
を変数に保存した時には、既に書き換え済みのものになっています。console.log(window.setInterval.toString());
とでもしてみれば一目瞭然。
……んん?
という事は、window.setInterval.toString()
でネイティブコードかどうか判別すれば良いのでは!?
// サンプル3 (function(){ var setInterval2 = window.setInterval; if (!isNativeFunction(setInterval2, "setInterval")) { $(document).ready(function() { $('body').children().remove() ; $('body').append('<p class="error">setInterval()が不正です。</p>'); }); return; } $(document).ready(function() { $('#start').on('click', function(){ var counter = 0; $('#time').text(counter); var timer = setInterval2(function() { ++counter; $('#time').text(counter); if (counter == 10) clearInterval(timer); }, 1000); }); }); function isNativeFunction(func, name) { var match = func.toString().match(/^function (\S+)\(\)\s*{\s*\[native code\]\s*}$/); return (match && match[1] === name); } })();
Round 3
……だが甘い! ならば、window.setInterval
を書き換えると同時に、window.setInterval.toString
や、まで書き換えてしまえばどうなるのか!?
(function(){ var setInterval2 = window.setInterval; window.setInterval = function(a,b){ return setInterval2(a, b/100); }; window.setInterval.toString = function() { return setInterval2.toString(); }; window.setInterval.toSource = function() { return setInterval2.toSource(); }; // toSource未対応ブラウザへの対応などは省略 window.setInterval.hasOwnProperty = function(x) { return setInterval2.hasOwnProperty(x); }; })();
なんて事! hasOwnProperty
でのチェックまで封じられてしまった!
……いいや、我々にはまだ、for in
なる力強い味方が残っている。組み込み型のtoString
はDontEnum属性がついているから、toString
がfor in
で列挙できたならtoString
が改竄されているということ。propertyIsEnumerable
メソッドを使った場合にはそれ自身が上書き済みの可能性があるが、for in
であればその心配もない!!
function isNativeFunction(func, name) { for (var o in func) { if (o === "toString") return false; } var match = func.toString().match(/^function (\S+)\(\)\s*{\s*\[native code\]\s*}$/); return (match && match[1] === name); }
勝利!!
Round 4
……ところでさ。
たぶん、アドオンを自作したりしたら特定URLでだけwindow.setTimeout
系関数が高速に動作するとかできますよね。
仮にアドオンでは無理だとしても、オープンソースのブラウザのソースコードを弄れば簡単ですよね。
他にも、上手いことプロキシを噛ませてチェックコードを取り除いてやったりするとか、とか、いくらでもやりようはありますよね。
結論:そんなに時間を誤摩化されたくないWebアプリケーションなら、素直にサーバー側プログラムでどうにかしましょう。
下手な高速化で地獄を見たお話 ~ XCode編 ~
こういうエラーが起きました。
「Could not insert new outlet connection: Could not find any information for the class named」というエラー。
第一手:IDEIndexDisableという犯人
スタックオーバーフローさん等でいろいろ解決方法が載っており、それらを片っ端から実行していったのですが、ダメ。 XCodeをアップデートしてもダメ。
そんな時この記事で一つ見逃していた箇所を発見。 ios6 - Could not insert new outlet connection - Stack Overflow
I recently came across this problem. I soon realized that the cause had been my own doing. I had previously disabled XCode indexing (which used to take forever & eat up my RAM), using the below code in a terminal window:
defaults write com.apple.dt.XCode IDEIndexDisable 1 To revert XCode to its default state, i used the following line in a terminal window:
defaults write com.apple.dt.XCode IDEIndexDisable 0 Voila! All's well again..
ほほう。。。。これは。。。。過去に
terminalを開いて defaults write com.apple.dt.XCode IDEIndexDisable 1 これで、おっけい。(・∀・)
みたいな記事を参考に、インデックス機能をオフにしたことがあるという記憶がヨミガエってきたのでした。 犯人はこれだ!!
よっしゃ、
また、Indexingを戻したかったら defaults write com.apple.dt.XCode IDEIndexDisable 0 こうしたらできましたー バンザイ!(゚∀゚)アヒャヒャ
みたいな記事の言う通りに戻してみよう!!
えいっ!!
とやるもこれが戻らない。(゚∀゚)アヒャヒャ
第二手:Preferencesは弱かった
なんで戻らないんだ? ターミナルからでなく、ライブラリ/Preferences/com.apple.dt.XCode.plistを直接書き換えてやったら? これだと、0に書き換えた直後にまた1に戻る。保存もしているのに。 つまり、別のところにも同じ情報持ってるってことですね。Preferencesより強い何かがいるってことですね。
XCodeのアップデートしても効果なかったのだから、アプリケーションフォルダは犯人から除外してよさそうですね。 探してやると、ライブラリ/Cashces/com.apple.dt.Xcodeというやつと、ライブラリ/Saved Application State/com.apple.dt.Xcode.savedStateというやつがいた。 お前たちか、僕をOS再インストールまで追い込もうとしている可愛い子たちは、よしよし。 お前らを削除してこれで気分爽快、綺麗サッパリ、すっきりもっきりだ。
えいっ!!
とやるもこれが戻らない。(゚∀゚)アヒャヒャ
第三手:ゴミは捨てよう
なぜだ、なぜなんだ、どうしてなんだ。
どうして戻らないんだ、この設定は。
もはや打つ手がないぞ。
と思った最後の起死回生の一手、会心の一撃、それは「ゴミ箱を空にすること」。
ゴミ箱行ってもリンク切れてなかったなんて。。。
Xcodeで余計なCleanを避けるスクリプト
XCodeでフォルダ内のファイルを全てプロジェクトに追加する方法には、二種類あります。
一つは「Create groups for any added folders」、もう一つは「Create folder references for any added folders」です。フォルダの色が黄色か青色か、という区別の仕方もアリ。
この二つの違いは以下の通りです:
- Create groups ...(黄色)
指定したフォルダと同様にグループ階層を作成し、その中に個別のファイルを追加します。
フォルダ内に新たにファイルを追加しても、プロジェクトには一切影響がありません。
リソースにこの方法でファイルを追加した場合、使用時にはフォルダ名を含まずファイル名のみで指定します。 - Create folder references ...(青色)
指定したフォルダそのものをプロジェクトに追加します。
フォルダ内容が変更された場合、自動でXCode上にその変更が反映されます。
リソースにこの方法でファイルを追加した場合、使用時にはフォルダ名を含んだファイル名で指定します。
ですので、例えば「これから画像が増えて行くけれど、画像が出来る度にプロジェクトに登録してやるのは面倒くさいぞ」という場合には、「Create folder references ...」の方を選べばOKです。
……と思いきや
ですが実は、ここに一つ落とし穴があるのでした。
XCodeでは当然ながら、ビルド時に変更のないファイルをチェックし、不要なコンパイルを抑制する処理を行なっています。しかし、「Create folder references ...」で作った青フォルダの中のファイルを更新した場合、フォルダそのものには全く変更が加わっていないことに注意です。 つまり……ファイルをいくら更新しても、リソースが更新されません。 この状況で正しくリソースの変更を反映するためには、ビルド済みのモジュールから、該当のリソースを削除してやる必要があります。
一番簡単なのは、一度Product > Cleanとする方法です……が、これでは折角のオブジェクトファイルまで削除してしまうため、巨大なプロジェクトでは再ビルドに恐ろしく時間がかかってしまいます。リソースだけを差し替えるためにソースを全てコンパイルし直すというのは、控えめに言っても愚かの一言です。
そんなわけで、次のようなスクリプト、rmapp.shを作ってみました:
#!/bin/bash if [ $# -ne 1 ]; then echo "Usage: ./$0 projectname" exit 1 fi for path in ~/Library/Developer/Xcode/DerivedData/$1-*/Build/Products/* do if test -e "${path}/$1.app" ; then echo "rm -rf \"${path}/$1.app\"" rm -rf "${path}/$1.app" fi done exit 0
使い方は、次の通りです:
./rmapp TestProject
やっている事としては、関連するっぽいappを兎に角片っ端から探して削除しているだけです。 同名の別プロジェクトがあればそちらも削除してしまいますが、Cleanするのと比べればappの作成処理にかかる時間など微々たるものなので、気にする必要はないでしょう。
Parse.comのREST APIを使ったPush通知
複数のアプリに対してParse.comを介してPush通知を送りたい場合、Webインターフェースでポチポチやるのは効率が悪く、とても辛い作業になってしまいます。Parse.comのサイトはレスポンスが少し遅いなと感じる程度である上に、1回1回が退屈な作業を何度も繰り返すことになるのでミスも入り込み易いのです。当然ですが、自動化・半自動化をするのが望ましいでしょう。
複数種類のiPhoneアプリを同時開発するときのTips - 株式会社CFlatの明後日スタイルのブログ
REST API用のKEY
REST APIを使うためには、アプリケーションIDとREST API用のアプリケーションキーが必要です。これらのキーを自動化スクリプトなり、専用のファイルなり、どこかに転載しておきます。
なお、iOS用のアプリケーションキーとREST API用のアプリケーションキーは異なります。
Rubyクライアント
REST APIはcurl
のようなコマンドから呼び出すことも可能ですが(Push Developer Guide | Parse)、スクリプト言語から呼び出した方が楽だと個人的には思います。
私が常用しているRubyにはparse-ruby-client
というgemがあったのでこちらを使うことにしました。
使い方
user_1
というチャンネルに該当するiOSデバイスにデータを送る、という処理を次のコードで実現できます。
require "parse-ruby-client" Parse.init(application_id: id, application_key: key) data = { :alert => "This is a notification from Parse" } push = Parse::Push.new(data, "user_1") push.type = "ios" push.save
全デバイスに送る
先ほどの例はとあるチャンネルに該当するユーザーにのみ通知を出す方法なんですが、今回はチャンネルではなくユーザー全体を対象にしようとしていました。
そこでチャンネルを空にしてやればいいだろうとParse::Push.new(data, "")
やParse::Push.new(data, nil)
などを試したところ、これでは0人に対してのpush通知を発行するという意味になるらしく失敗に終わりました。
どうやらREST API本体に存在しているwhere
というパラメータに空のjsonを渡せばいいということでした。このラッパーの場合はこのように書く必要があります。
require "parse-ruby-client" Parse.init(application_id: id, application_key: key) data = { :alert => "This is a notification from Parse" } push = Parse::Push.new(data) push.where = {} push.save
データファイルのサンプル
複数のアプリケーションにプッシュ通知を送るスクリプトですが、実際にはRakeタスクとして定義しました。前提としては、アプリケーションIDやREST用のキーはyamlファイルに書いてあります。
app1: parse_id: APP_ID_1 parse_rest_key: APP_REST_KEY_1 ... app2: parse_id: APP_ID_2 parse_rest_key: APP_REST_KEY_2 ... app3: ...
タスク本体はこんな感じです。
require "yaml" require "parse-ruby-client" task :push do config = YAML.load_file( File.expand_path("../../data.yml", __FILE__) ) config.keys.each_with_index do |key| parse_id = config[key]["parse_id"] rest_key = config[key]["parse_rest_key"] data = { alert: "alert title", message: "alert message" } # ParseのREST APIを使うコード Parse.init(application_id: parse_id, api_key: rest_key) push = Parse::Push.new(data) push.where = {} push.save end end
これで半自動化できるようになりました。通知内容がハードコードされているので何回も使うのには適しません。そのような場合は通知内容の部分を分離し、引数のjsonファイルから読み込むなどの工夫が必要となります。